react-native-collapsible-tabs-reanimated 0.1.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/lib/commonjs/Bar.js +247 -0
- package/lib/commonjs/Bar.js.map +1 -0
- package/lib/commonjs/Button.js +150 -0
- package/lib/commonjs/Button.js.map +1 -0
- package/lib/commonjs/Context.js +21 -0
- package/lib/commonjs/Context.js.map +1 -0
- package/lib/commonjs/FlashList.js +91 -0
- package/lib/commonjs/FlashList.js.map +1 -0
- package/lib/commonjs/Header.js +54 -0
- package/lib/commonjs/Header.js.map +1 -0
- package/lib/commonjs/Indicator.js +156 -0
- package/lib/commonjs/Indicator.js.map +1 -0
- package/lib/commonjs/Lazy.js +87 -0
- package/lib/commonjs/Lazy.js.map +1 -0
- package/lib/commonjs/LegendList.js +86 -0
- package/lib/commonjs/LegendList.js.map +1 -0
- package/lib/commonjs/List.js +83 -0
- package/lib/commonjs/List.js.map +1 -0
- package/lib/commonjs/Pager.js +93 -0
- package/lib/commonjs/Pager.js.map +1 -0
- package/lib/commonjs/Root.js +169 -0
- package/lib/commonjs/Root.js.map +1 -0
- package/lib/commonjs/ScrollView.js +85 -0
- package/lib/commonjs/ScrollView.js.map +1 -0
- package/lib/commonjs/StaticHeader.js +37 -0
- package/lib/commonjs/StaticHeader.js.map +1 -0
- package/lib/commonjs/StickyHeader.js +37 -0
- package/lib/commonjs/StickyHeader.js.map +1 -0
- package/lib/commonjs/Tab.js +86 -0
- package/lib/commonjs/Tab.js.map +1 -0
- package/lib/commonjs/flash-list.js +14 -0
- package/lib/commonjs/flash-list.js.map +1 -0
- package/lib/commonjs/index.js +128 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/legend-list.js +14 -0
- package/lib/commonjs/legend-list.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/useStableCallback.js +15 -0
- package/lib/commonjs/useStableCallback.js.map +1 -0
- package/lib/module/Bar.js +242 -0
- package/lib/module/Bar.js.map +1 -0
- package/lib/module/Button.js +145 -0
- package/lib/module/Button.js.map +1 -0
- package/lib/module/Context.js +16 -0
- package/lib/module/Context.js.map +1 -0
- package/lib/module/FlashList.js +86 -0
- package/lib/module/FlashList.js.map +1 -0
- package/lib/module/Header.js +49 -0
- package/lib/module/Header.js.map +1 -0
- package/lib/module/Indicator.js +151 -0
- package/lib/module/Indicator.js.map +1 -0
- package/lib/module/Lazy.js +82 -0
- package/lib/module/Lazy.js.map +1 -0
- package/lib/module/LegendList.js +81 -0
- package/lib/module/LegendList.js.map +1 -0
- package/lib/module/List.js +78 -0
- package/lib/module/List.js.map +1 -0
- package/lib/module/Pager.js +87 -0
- package/lib/module/Pager.js.map +1 -0
- package/lib/module/Root.js +165 -0
- package/lib/module/Root.js.map +1 -0
- package/lib/module/ScrollView.js +80 -0
- package/lib/module/ScrollView.js.map +1 -0
- package/lib/module/StaticHeader.js +32 -0
- package/lib/module/StaticHeader.js.map +1 -0
- package/lib/module/StickyHeader.js +32 -0
- package/lib/module/StickyHeader.js.map +1 -0
- package/lib/module/Tab.js +81 -0
- package/lib/module/Tab.js.map +1 -0
- package/lib/module/flash-list.js +4 -0
- package/lib/module/flash-list.js.map +1 -0
- package/lib/module/index.js +44 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/legend-list.js +4 -0
- package/lib/module/legend-list.js.map +1 -0
- package/lib/module/useStableCallback.js +11 -0
- package/lib/module/useStableCallback.js.map +1 -0
- package/lib/typescript/Bar.d.ts +22 -0
- package/lib/typescript/Bar.d.ts.map +1 -0
- package/lib/typescript/Button.d.ts +32 -0
- package/lib/typescript/Button.d.ts.map +1 -0
- package/lib/typescript/Context.d.ts +37 -0
- package/lib/typescript/Context.d.ts.map +1 -0
- package/lib/typescript/FlashList.d.ts +6 -0
- package/lib/typescript/FlashList.d.ts.map +1 -0
- package/lib/typescript/Header.d.ts +7 -0
- package/lib/typescript/Header.d.ts.map +1 -0
- package/lib/typescript/Indicator.d.ts +11 -0
- package/lib/typescript/Indicator.d.ts.map +1 -0
- package/lib/typescript/Lazy.d.ts +36 -0
- package/lib/typescript/Lazy.d.ts.map +1 -0
- package/lib/typescript/LegendList.d.ts +6 -0
- package/lib/typescript/LegendList.d.ts.map +1 -0
- package/lib/typescript/List.d.ts +6 -0
- package/lib/typescript/List.d.ts.map +1 -0
- package/lib/typescript/Pager.d.ts +15 -0
- package/lib/typescript/Pager.d.ts.map +1 -0
- package/lib/typescript/Root.d.ts +14 -0
- package/lib/typescript/Root.d.ts.map +1 -0
- package/lib/typescript/ScrollView.d.ts +6 -0
- package/lib/typescript/ScrollView.d.ts.map +1 -0
- package/lib/typescript/StaticHeader.d.ts +7 -0
- package/lib/typescript/StaticHeader.d.ts.map +1 -0
- package/lib/typescript/StickyHeader.d.ts +7 -0
- package/lib/typescript/StickyHeader.d.ts.map +1 -0
- package/lib/typescript/Tab.d.ts +31 -0
- package/lib/typescript/Tab.d.ts.map +1 -0
- package/lib/typescript/flash-list.d.ts +3 -0
- package/lib/typescript/flash-list.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +69 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/legend-list.d.ts +3 -0
- package/lib/typescript/legend-list.d.ts.map +1 -0
- package/lib/typescript/useStableCallback.d.ts +2 -0
- package/lib/typescript/useStableCallback.d.ts.map +1 -0
- package/package.json +112 -0
- package/src/Bar.tsx +359 -0
- package/src/Button.tsx +219 -0
- package/src/Context.tsx +44 -0
- package/src/FlashList.tsx +150 -0
- package/src/Header.tsx +45 -0
- package/src/Indicator.tsx +193 -0
- package/src/Lazy.tsx +110 -0
- package/src/LegendList.tsx +130 -0
- package/src/List.tsx +115 -0
- package/src/Pager.tsx +134 -0
- package/src/Root.tsx +194 -0
- package/src/ScrollView.tsx +116 -0
- package/src/StaticHeader.tsx +30 -0
- package/src/StickyHeader.tsx +30 -0
- package/src/Tab.tsx +89 -0
- package/src/flash-list.ts +2 -0
- package/src/index.ts +54 -0
- package/src/legend-list.ts +2 -0
- package/src/useStableCallback.ts +11 -0
package/src/Bar.tsx
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { ReactNode, memo, useCallback, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
ScrollViewProps,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
View,
|
|
9
|
+
ViewProps,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
} from "react-native";
|
|
12
|
+
|
|
13
|
+
import { Pressable, ScrollView } from "react-native-gesture-handler";
|
|
14
|
+
import Animated, {
|
|
15
|
+
SharedValue,
|
|
16
|
+
interpolate,
|
|
17
|
+
scrollTo,
|
|
18
|
+
useAnimatedReaction,
|
|
19
|
+
useAnimatedRef,
|
|
20
|
+
useAnimatedScrollHandler,
|
|
21
|
+
useAnimatedStyle,
|
|
22
|
+
useDerivedValue,
|
|
23
|
+
useSharedValue,
|
|
24
|
+
withTiming,
|
|
25
|
+
} from "react-native-reanimated";
|
|
26
|
+
|
|
27
|
+
import { useCollapsibleTabsContext } from "./Context";
|
|
28
|
+
import { scheduleOnUI } from "react-native-worklets";
|
|
29
|
+
|
|
30
|
+
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
|
|
31
|
+
|
|
32
|
+
export type BarProps = Omit<
|
|
33
|
+
ScrollViewProps,
|
|
34
|
+
"contentContainerStyle" | "style"
|
|
35
|
+
> & {
|
|
36
|
+
fullWidth?: boolean;
|
|
37
|
+
scrollButtons?: boolean;
|
|
38
|
+
left?: ReactNode;
|
|
39
|
+
right?: ReactNode;
|
|
40
|
+
scrollContainerStyle?: ScrollViewProps["style"];
|
|
41
|
+
tabButtonsGap?: number;
|
|
42
|
+
children?: ReactNode;
|
|
43
|
+
containerProps?: ViewProps;
|
|
44
|
+
backgroundColor?: string;
|
|
45
|
+
scrollButtonBackgroundColor?: string;
|
|
46
|
+
scrollButtonIconColor?: string;
|
|
47
|
+
renderScrollButtonIcon?: (dir: "left" | "right") => ReactNode;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const Bar = ({
|
|
51
|
+
scrollEnabled = true,
|
|
52
|
+
fullWidth,
|
|
53
|
+
scrollButtons = true,
|
|
54
|
+
left,
|
|
55
|
+
right,
|
|
56
|
+
containerProps,
|
|
57
|
+
children,
|
|
58
|
+
scrollContainerStyle,
|
|
59
|
+
tabButtonsGap = 0,
|
|
60
|
+
backgroundColor = "#ffffff",
|
|
61
|
+
scrollButtonBackgroundColor = "rgba(255, 255, 255, 0.95)",
|
|
62
|
+
scrollButtonIconColor = "rgba(48, 48, 48, 0.7)",
|
|
63
|
+
renderScrollButtonIcon,
|
|
64
|
+
...props
|
|
65
|
+
}: BarProps) => {
|
|
66
|
+
const { pageDecimal, itemLayout } = useCollapsibleTabsContext();
|
|
67
|
+
const [barSize, setBarSize] = useState({
|
|
68
|
+
width: 0,
|
|
69
|
+
height: 0,
|
|
70
|
+
measured: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const contentWidth = useSharedValue(0);
|
|
74
|
+
const layoutWidth = useSharedValue(0);
|
|
75
|
+
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
76
|
+
const scrollX = useSharedValue(0);
|
|
77
|
+
const leftButtonOpacity = useSharedValue(0);
|
|
78
|
+
const rightButtonOpacity = useSharedValue(0);
|
|
79
|
+
|
|
80
|
+
useAnimatedReaction(
|
|
81
|
+
() => {
|
|
82
|
+
if (!scrollButtons || layoutWidth.value === 0 || contentWidth.value === 0)
|
|
83
|
+
return { showLeft: false, showRight: false };
|
|
84
|
+
const maxScroll = contentWidth.value - layoutWidth.value;
|
|
85
|
+
const isScrollable = maxScroll > 1;
|
|
86
|
+
return {
|
|
87
|
+
showLeft: isScrollable && scrollX.value > 10,
|
|
88
|
+
showRight: isScrollable && scrollX.value < maxScroll - 10,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
(curr, prev) => {
|
|
92
|
+
if (curr.showLeft !== prev?.showLeft)
|
|
93
|
+
leftButtonOpacity.value = withTiming(curr.showLeft ? 1 : 0);
|
|
94
|
+
if (curr.showRight !== prev?.showRight)
|
|
95
|
+
rightButtonOpacity.value = withTiming(curr.showRight ? 1 : 0);
|
|
96
|
+
},
|
|
97
|
+
[scrollButtons],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const onScroll = useAnimatedScrollHandler((evt) => {
|
|
101
|
+
scrollX.value = evt.contentOffset.x;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const scrollByButton = useCallback(
|
|
105
|
+
(dir: "left" | "right") => {
|
|
106
|
+
"worklet";
|
|
107
|
+
const step = layoutWidth.value / 2;
|
|
108
|
+
const current = scrollX.value;
|
|
109
|
+
const target = dir === "left" ? current - step : current + step;
|
|
110
|
+
const maxScroll = contentWidth.value - layoutWidth.value;
|
|
111
|
+
scrollTo(
|
|
112
|
+
scrollRef,
|
|
113
|
+
Math.max(0, Math.min(target, maxScroll > 0 ? maxScroll : 0)),
|
|
114
|
+
0,
|
|
115
|
+
true,
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
[contentWidth, layoutWidth, scrollRef, scrollX],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const handlePress = useCallback(
|
|
122
|
+
(dir: "left" | "right") => {
|
|
123
|
+
scheduleOnUI(scrollByButton, dir);
|
|
124
|
+
},
|
|
125
|
+
[scrollByButton],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const interpolationTable = useDerivedValue(() => {
|
|
129
|
+
if (itemLayout.length <= 1) return null;
|
|
130
|
+
const inputRange = new Array<number>(itemLayout.length);
|
|
131
|
+
const outputRange = new Array<number>(itemLayout.length);
|
|
132
|
+
const halfBar = barSize.width / 2;
|
|
133
|
+
for (let index = 0; index < itemLayout.length; index += 1) {
|
|
134
|
+
inputRange[index] = index;
|
|
135
|
+
const item = itemLayout[index];
|
|
136
|
+
outputRange[index] = +(item.x + item.width / 2 - halfBar).toFixed(2);
|
|
137
|
+
}
|
|
138
|
+
return { inputRange, outputRange };
|
|
139
|
+
}, [barSize.width, itemLayout]);
|
|
140
|
+
|
|
141
|
+
const centerOffset = useDerivedValue(() => {
|
|
142
|
+
if (!interpolationTable.value) return 0;
|
|
143
|
+
return interpolate(
|
|
144
|
+
+pageDecimal.value.toFixed(3),
|
|
145
|
+
interpolationTable.value.inputRange,
|
|
146
|
+
interpolationTable.value.outputRange,
|
|
147
|
+
"identity",
|
|
148
|
+
);
|
|
149
|
+
}, [interpolationTable]);
|
|
150
|
+
|
|
151
|
+
useAnimatedReaction(
|
|
152
|
+
() => Math.round(centerOffset.value),
|
|
153
|
+
(value, prev) => {
|
|
154
|
+
if (value !== prev) scrollTo(scrollRef, value, 0, true);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const contentContainerStyle = useMemo(
|
|
159
|
+
(): ViewStyle => ({ flex: fullWidth ? 1 : 0, gap: tabButtonsGap }),
|
|
160
|
+
[fullWidth, tabButtonsGap],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const onContentSizeChange = useCallback(
|
|
164
|
+
(width: number) => {
|
|
165
|
+
contentWidth.value = width;
|
|
166
|
+
},
|
|
167
|
+
[contentWidth],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const onLayout = useCallback(
|
|
171
|
+
(evt: LayoutChangeEvent) => {
|
|
172
|
+
layoutWidth.value = evt.nativeEvent.layout.width;
|
|
173
|
+
},
|
|
174
|
+
[layoutWidth],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const containerOnLayout = containerProps?.onLayout;
|
|
178
|
+
const onContainerLayout = useCallback(
|
|
179
|
+
(evt: LayoutChangeEvent) => {
|
|
180
|
+
const { width, height } = evt.nativeEvent.layout;
|
|
181
|
+
setBarSize((prev) =>
|
|
182
|
+
prev.width === width && prev.height === height
|
|
183
|
+
? prev
|
|
184
|
+
: { width, height, measured: true },
|
|
185
|
+
);
|
|
186
|
+
containerOnLayout?.(evt);
|
|
187
|
+
},
|
|
188
|
+
[containerOnLayout],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<View
|
|
193
|
+
{...containerProps}
|
|
194
|
+
style={[styles.containerRow, { backgroundColor }, containerProps?.style]}
|
|
195
|
+
onLayout={onContainerLayout}
|
|
196
|
+
collapsable={false}
|
|
197
|
+
>
|
|
198
|
+
{left}
|
|
199
|
+
<View style={[styles.scrollContainer, scrollContainerStyle]}>
|
|
200
|
+
{!!scrollButtons && (
|
|
201
|
+
<ScrollButton
|
|
202
|
+
dir="left"
|
|
203
|
+
buttonProgress={leftButtonOpacity}
|
|
204
|
+
handlePress={handlePress}
|
|
205
|
+
backgroundColor={scrollButtonBackgroundColor}
|
|
206
|
+
iconColor={scrollButtonIconColor}
|
|
207
|
+
renderIcon={renderScrollButtonIcon}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
<AnimatedScrollView
|
|
211
|
+
ref={scrollRef}
|
|
212
|
+
horizontal
|
|
213
|
+
showsHorizontalScrollIndicator={false}
|
|
214
|
+
scrollEventThrottle={16}
|
|
215
|
+
keyboardShouldPersistTaps="handled"
|
|
216
|
+
scrollEnabled={scrollEnabled}
|
|
217
|
+
contentContainerStyle={contentContainerStyle}
|
|
218
|
+
directionalLockEnabled
|
|
219
|
+
onContentSizeChange={onContentSizeChange}
|
|
220
|
+
onLayout={onLayout}
|
|
221
|
+
onScroll={onScroll}
|
|
222
|
+
bounces={false}
|
|
223
|
+
collapsable={false}
|
|
224
|
+
{...props}
|
|
225
|
+
>
|
|
226
|
+
{children}
|
|
227
|
+
</AnimatedScrollView>
|
|
228
|
+
{!!scrollButtons && (
|
|
229
|
+
<ScrollButton
|
|
230
|
+
dir="right"
|
|
231
|
+
buttonProgress={rightButtonOpacity}
|
|
232
|
+
handlePress={handlePress}
|
|
233
|
+
backgroundColor={scrollButtonBackgroundColor}
|
|
234
|
+
iconColor={scrollButtonIconColor}
|
|
235
|
+
renderIcon={renderScrollButtonIcon}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</View>
|
|
239
|
+
{right}
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
type ScrollButtonProps = {
|
|
245
|
+
dir: "left" | "right";
|
|
246
|
+
buttonProgress: SharedValue<number>;
|
|
247
|
+
handlePress: (dir: "left" | "right") => void;
|
|
248
|
+
backgroundColor: string;
|
|
249
|
+
iconColor: string;
|
|
250
|
+
renderIcon?: (dir: "left" | "right") => ReactNode;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const ScrollButton = memo(
|
|
254
|
+
({
|
|
255
|
+
dir,
|
|
256
|
+
buttonProgress,
|
|
257
|
+
handlePress,
|
|
258
|
+
backgroundColor,
|
|
259
|
+
iconColor,
|
|
260
|
+
renderIcon,
|
|
261
|
+
}: ScrollButtonProps) => {
|
|
262
|
+
const isLeft = dir === "left";
|
|
263
|
+
const width = useSharedValue(0);
|
|
264
|
+
|
|
265
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
266
|
+
const opacity = buttonProgress.value;
|
|
267
|
+
return {
|
|
268
|
+
opacity,
|
|
269
|
+
transform: [
|
|
270
|
+
{
|
|
271
|
+
translateX: interpolate(
|
|
272
|
+
opacity,
|
|
273
|
+
[0, 1],
|
|
274
|
+
[isLeft ? -width.value : width.value, 0],
|
|
275
|
+
),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
pointerEvents: opacity === 0 ? "none" : "auto",
|
|
279
|
+
};
|
|
280
|
+
}, [isLeft]);
|
|
281
|
+
|
|
282
|
+
const onLayoutButton = useCallback(
|
|
283
|
+
(evt: LayoutChangeEvent) => {
|
|
284
|
+
width.value = evt.nativeEvent.layout.width;
|
|
285
|
+
},
|
|
286
|
+
[width],
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const onPress = useCallback(() => handlePress(dir), [dir, handlePress]);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<Animated.View
|
|
293
|
+
style={[
|
|
294
|
+
isLeft
|
|
295
|
+
? scrollButtonStyles.containerLeft
|
|
296
|
+
: scrollButtonStyles.containerRight,
|
|
297
|
+
{ backgroundColor },
|
|
298
|
+
animatedStyle,
|
|
299
|
+
]}
|
|
300
|
+
onLayout={onLayoutButton}
|
|
301
|
+
>
|
|
302
|
+
<Pressable style={scrollButtonStyles.button} onPress={onPress}>
|
|
303
|
+
{renderIcon ? (
|
|
304
|
+
renderIcon(dir)
|
|
305
|
+
) : (
|
|
306
|
+
<Text style={[scrollButtonStyles.icon, { color: iconColor }]}>
|
|
307
|
+
{isLeft ? "<" : ">"}
|
|
308
|
+
</Text>
|
|
309
|
+
)}
|
|
310
|
+
</Pressable>
|
|
311
|
+
</Animated.View>
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const styles = StyleSheet.create({
|
|
317
|
+
containerRow: {
|
|
318
|
+
position: "relative",
|
|
319
|
+
flexDirection: "row",
|
|
320
|
+
alignItems: "center",
|
|
321
|
+
},
|
|
322
|
+
scrollContainer: {
|
|
323
|
+
flex: 1,
|
|
324
|
+
flexDirection: "row",
|
|
325
|
+
alignItems: "center",
|
|
326
|
+
justifyContent: "center",
|
|
327
|
+
position: "relative",
|
|
328
|
+
paddingHorizontal: 20,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const scrollButtonStyles = StyleSheet.create({
|
|
333
|
+
containerLeft: {
|
|
334
|
+
position: "absolute",
|
|
335
|
+
left: 0,
|
|
336
|
+
zIndex: 2,
|
|
337
|
+
height: "100%",
|
|
338
|
+
},
|
|
339
|
+
containerRight: {
|
|
340
|
+
position: "absolute",
|
|
341
|
+
right: 0,
|
|
342
|
+
zIndex: 2,
|
|
343
|
+
height: "100%",
|
|
344
|
+
},
|
|
345
|
+
button: {
|
|
346
|
+
paddingHorizontal: 12,
|
|
347
|
+
paddingVertical: 4,
|
|
348
|
+
height: "100%",
|
|
349
|
+
justifyContent: "center",
|
|
350
|
+
},
|
|
351
|
+
icon: {
|
|
352
|
+
fontSize: 16,
|
|
353
|
+
fontWeight: "700",
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
Bar.displayName = "CollapsibleTabs.Bar";
|
|
358
|
+
|
|
359
|
+
export default memo(Bar);
|
package/src/Button.tsx
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { ReactNode, memo, useCallback, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
StyleProp,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
TextProps,
|
|
8
|
+
TextStyle,
|
|
9
|
+
View,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
} from "react-native";
|
|
12
|
+
|
|
13
|
+
import { Pressable, PressableProps } from "react-native-gesture-handler";
|
|
14
|
+
import Animated, {
|
|
15
|
+
AnimatedStyle,
|
|
16
|
+
SharedValue,
|
|
17
|
+
interpolateColor,
|
|
18
|
+
useAnimatedStyle,
|
|
19
|
+
withTiming,
|
|
20
|
+
} from "react-native-reanimated";
|
|
21
|
+
|
|
22
|
+
import { useCollapsibleTabsContext } from "./Context";
|
|
23
|
+
|
|
24
|
+
type PressableEvent = Parameters<NonNullable<PressableProps["onPress"]>>[0];
|
|
25
|
+
|
|
26
|
+
export type RenderTabLabelProps = {
|
|
27
|
+
index: number;
|
|
28
|
+
name: string;
|
|
29
|
+
isActive: boolean;
|
|
30
|
+
style: StyleProp<TextStyle>;
|
|
31
|
+
animatedStyle: AnimatedStyle<StyleProp<TextStyle>>;
|
|
32
|
+
pageDecimal: SharedValue<number>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ButtonProps = {
|
|
36
|
+
index: number;
|
|
37
|
+
name: string;
|
|
38
|
+
style?: StyleProp<ViewStyle>;
|
|
39
|
+
fullWidth?: boolean;
|
|
40
|
+
activeStyle?: StyleProp<ViewStyle>;
|
|
41
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
42
|
+
activeLabelStyle?: StyleProp<TextStyle>;
|
|
43
|
+
labelProps?: Omit<TextProps, "style">;
|
|
44
|
+
contentWrapperStyle?: StyleProp<ViewStyle>;
|
|
45
|
+
activeLabelColor?: string;
|
|
46
|
+
inactiveLabelColor?: string;
|
|
47
|
+
children?: string | ((props: RenderTabLabelProps) => ReactNode);
|
|
48
|
+
} & Omit<PressableProps, "style" | "children">;
|
|
49
|
+
|
|
50
|
+
const Button = ({
|
|
51
|
+
index,
|
|
52
|
+
name,
|
|
53
|
+
onPress,
|
|
54
|
+
style,
|
|
55
|
+
fullWidth,
|
|
56
|
+
labelStyle,
|
|
57
|
+
labelProps,
|
|
58
|
+
activeLabelStyle,
|
|
59
|
+
activeStyle,
|
|
60
|
+
children,
|
|
61
|
+
contentWrapperStyle,
|
|
62
|
+
activeLabelColor = "#111827",
|
|
63
|
+
inactiveLabelColor = "#9ca3af",
|
|
64
|
+
...props
|
|
65
|
+
}: ButtonProps) => {
|
|
66
|
+
const {
|
|
67
|
+
activeTabIndex,
|
|
68
|
+
activeTabIndexValue,
|
|
69
|
+
pageDecimal,
|
|
70
|
+
pagerRef,
|
|
71
|
+
registerButton,
|
|
72
|
+
itemLayout,
|
|
73
|
+
} = useCollapsibleTabsContext();
|
|
74
|
+
const isActive = activeTabIndexValue === index;
|
|
75
|
+
|
|
76
|
+
const combinedStaticLabelStyle = useMemo(
|
|
77
|
+
(): StyleProp<TextStyle> => [
|
|
78
|
+
styles.label,
|
|
79
|
+
labelStyle,
|
|
80
|
+
activeLabelStyle && isActive && activeLabelStyle,
|
|
81
|
+
],
|
|
82
|
+
[activeLabelStyle, isActive, labelStyle],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const animatedLabelStyle = useAnimatedStyle(() => {
|
|
86
|
+
const color = interpolateColor(
|
|
87
|
+
pageDecimal.value,
|
|
88
|
+
[index - 1, index, index + 1],
|
|
89
|
+
[inactiveLabelColor, activeLabelColor, inactiveLabelColor],
|
|
90
|
+
);
|
|
91
|
+
return { color };
|
|
92
|
+
}, [activeLabelColor, inactiveLabelColor, index, pageDecimal]);
|
|
93
|
+
|
|
94
|
+
const handleOnPress = useCallback(
|
|
95
|
+
(event: PressableEvent) => {
|
|
96
|
+
onPress?.(event);
|
|
97
|
+
if (pagerRef.current) {
|
|
98
|
+
pagerRef.current.setPage(index);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
activeTabIndex.value = index;
|
|
102
|
+
pageDecimal.value = withTiming(index);
|
|
103
|
+
},
|
|
104
|
+
[activeTabIndex, index, onPress, pageDecimal, pagerRef],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const pressableLayoutRef = useRef<{ x: number; y: number } | null>(null);
|
|
108
|
+
const contentLayoutRef = useRef<{
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
width: number;
|
|
112
|
+
height: number;
|
|
113
|
+
} | null>(null);
|
|
114
|
+
|
|
115
|
+
const commitLayout = useCallback(() => {
|
|
116
|
+
const pressable = pressableLayoutRef.current;
|
|
117
|
+
const content = contentLayoutRef.current;
|
|
118
|
+
if (!pressable || !content) return;
|
|
119
|
+
registerButton({
|
|
120
|
+
width: content.width,
|
|
121
|
+
height: content.height,
|
|
122
|
+
x: pressable.x + content.x,
|
|
123
|
+
y: pressable.y + content.y,
|
|
124
|
+
name,
|
|
125
|
+
index,
|
|
126
|
+
});
|
|
127
|
+
}, [index, name, registerButton]);
|
|
128
|
+
|
|
129
|
+
const onLayout = useCallback(
|
|
130
|
+
(event: LayoutChangeEvent) => {
|
|
131
|
+
props.onLayout?.(event);
|
|
132
|
+
const { x, y } = event.nativeEvent.layout;
|
|
133
|
+
pressableLayoutRef.current = { x, y };
|
|
134
|
+
commitLayout();
|
|
135
|
+
},
|
|
136
|
+
[commitLayout, props],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const onContentLayout = useCallback(
|
|
140
|
+
(event: LayoutChangeEvent) => {
|
|
141
|
+
const { x, y, width, height } = event.nativeEvent.layout;
|
|
142
|
+
contentLayoutRef.current = { x, y, width, height };
|
|
143
|
+
commitLayout();
|
|
144
|
+
},
|
|
145
|
+
[commitLayout],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const isLast = itemLayout.length - 1 === index;
|
|
149
|
+
const isFirst = index === 0;
|
|
150
|
+
const mergedStyle = useMemo(
|
|
151
|
+
(): StyleProp<ViewStyle> => [
|
|
152
|
+
styles.pressable,
|
|
153
|
+
isFirst && { paddingLeft: 0 },
|
|
154
|
+
isLast && { paddingRight: 0 },
|
|
155
|
+
fullWidth && styles.fullWidth,
|
|
156
|
+
style,
|
|
157
|
+
isActive && activeStyle,
|
|
158
|
+
],
|
|
159
|
+
[activeStyle, fullWidth, isActive, isFirst, isLast, style],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Pressable
|
|
164
|
+
onPress={handleOnPress}
|
|
165
|
+
{...props}
|
|
166
|
+
onLayout={onLayout}
|
|
167
|
+
style={mergedStyle}
|
|
168
|
+
>
|
|
169
|
+
<View
|
|
170
|
+
onLayout={onContentLayout}
|
|
171
|
+
style={[styles.contentWrapper, contentWrapperStyle]}
|
|
172
|
+
>
|
|
173
|
+
{typeof children === "function" ? (
|
|
174
|
+
children({
|
|
175
|
+
isActive,
|
|
176
|
+
index,
|
|
177
|
+
name,
|
|
178
|
+
style: combinedStaticLabelStyle,
|
|
179
|
+
animatedStyle: animatedLabelStyle,
|
|
180
|
+
pageDecimal,
|
|
181
|
+
})
|
|
182
|
+
) : (
|
|
183
|
+
<Animated.Text
|
|
184
|
+
style={[combinedStaticLabelStyle, animatedLabelStyle]}
|
|
185
|
+
numberOfLines={1}
|
|
186
|
+
{...labelProps}
|
|
187
|
+
>
|
|
188
|
+
{children}
|
|
189
|
+
</Animated.Text>
|
|
190
|
+
)}
|
|
191
|
+
</View>
|
|
192
|
+
</Pressable>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const styles = StyleSheet.create({
|
|
197
|
+
pressable: {
|
|
198
|
+
paddingVertical: 16,
|
|
199
|
+
paddingHorizontal: 10,
|
|
200
|
+
},
|
|
201
|
+
fullWidth: {
|
|
202
|
+
flex: 1,
|
|
203
|
+
},
|
|
204
|
+
contentWrapper: {
|
|
205
|
+
position: "relative",
|
|
206
|
+
alignSelf: "center",
|
|
207
|
+
},
|
|
208
|
+
label: {
|
|
209
|
+
fontSize: 14,
|
|
210
|
+
lineHeight: 18,
|
|
211
|
+
fontWeight: "700",
|
|
212
|
+
fontVariant: ["tabular-nums"],
|
|
213
|
+
textAlign: "center",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
Button.displayName = "CollapsibleTabs.Button";
|
|
218
|
+
|
|
219
|
+
export default memo(Button);
|
package/src/Context.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ReactNode, RefObject, createContext, memo, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import PagerView from 'react-native-pager-view';
|
|
4
|
+
import { SharedValue } from 'react-native-reanimated';
|
|
5
|
+
|
|
6
|
+
export type ItemLayout = {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
index: number;
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ListScroller = (animated?: boolean) => void;
|
|
16
|
+
|
|
17
|
+
export type CollapsibleTabsContextValue = {
|
|
18
|
+
headerOffset: SharedValue<number>;
|
|
19
|
+
staticHeight: SharedValue<number>;
|
|
20
|
+
stickyHeight: SharedValue<number>;
|
|
21
|
+
offsetAdjustment: SharedValue<number>;
|
|
22
|
+
activeTabIndex: SharedValue<number>;
|
|
23
|
+
activeTabIndexValue: number;
|
|
24
|
+
activeListOffset: SharedValue<number>;
|
|
25
|
+
pageDecimal: SharedValue<number>;
|
|
26
|
+
listPanGesture: any;
|
|
27
|
+
listGestures: any[];
|
|
28
|
+
pagerRef: RefObject<PagerView | null>;
|
|
29
|
+
itemLayout: ItemLayout[];
|
|
30
|
+
registerButton: (config: ItemLayout) => void;
|
|
31
|
+
registerListScroller: (index: number, scroller: ListScroller | null) => void;
|
|
32
|
+
staticHeightValue: number;
|
|
33
|
+
stickyHeightValue: number;
|
|
34
|
+
updateStaticHeight: (height: number) => void;
|
|
35
|
+
updateStickyHeight: (height: number) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CollapsibleTabsContext = createContext({} as CollapsibleTabsContextValue);
|
|
39
|
+
|
|
40
|
+
export const useCollapsibleTabsContext = () => useContext(CollapsibleTabsContext);
|
|
41
|
+
|
|
42
|
+
export const CollapsibleTabsContextProvider = memo(({ children, ...props }: CollapsibleTabsContextValue & { children: ReactNode }) => {
|
|
43
|
+
return <CollapsibleTabsContext.Provider value={props}>{children}</CollapsibleTabsContext.Provider>;
|
|
44
|
+
});
|