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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FlashList as ShopifyFlashList,
|
|
5
|
+
FlashListProps as ShopifyFlashListProps,
|
|
6
|
+
FlashListRef as ShopifyFlashListRef,
|
|
7
|
+
} from "@shopify/flash-list";
|
|
8
|
+
import {
|
|
9
|
+
LayoutChangeEvent,
|
|
10
|
+
ScrollViewProps,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
View,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
GestureDetector,
|
|
17
|
+
ScrollView as RNGHScrollView,
|
|
18
|
+
} from "react-native-gesture-handler";
|
|
19
|
+
import Animated, {
|
|
20
|
+
AnimatedProps,
|
|
21
|
+
scrollTo,
|
|
22
|
+
useAnimatedReaction,
|
|
23
|
+
useAnimatedRef,
|
|
24
|
+
useAnimatedScrollHandler,
|
|
25
|
+
useComposedEventHandler,
|
|
26
|
+
useSharedValue,
|
|
27
|
+
} from "react-native-reanimated";
|
|
28
|
+
|
|
29
|
+
import { ListScroller, useCollapsibleTabsContext } from "./Context";
|
|
30
|
+
import { useTabSelfContext } from "./Tab";
|
|
31
|
+
import { useStableCallback } from "./useStableCallback";
|
|
32
|
+
|
|
33
|
+
const AnimatedFlashList = Animated.createAnimatedComponent(
|
|
34
|
+
ShopifyFlashList,
|
|
35
|
+
) as <T>(
|
|
36
|
+
props: AnimatedProps<
|
|
37
|
+
ShopifyFlashListProps<T> & {
|
|
38
|
+
ref?: React.Ref<ShopifyFlashListRef<T>> | undefined;
|
|
39
|
+
}
|
|
40
|
+
>,
|
|
41
|
+
) => ReactElement;
|
|
42
|
+
|
|
43
|
+
export type CollapsibleFlashListProps<T> = Omit<
|
|
44
|
+
ShopifyFlashListProps<T>,
|
|
45
|
+
"renderScrollComponent"
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
const CollapsibleFlashList = <T,>({
|
|
49
|
+
onLayout,
|
|
50
|
+
onContentSizeChange,
|
|
51
|
+
|
|
52
|
+
...props
|
|
53
|
+
}: CollapsibleFlashListProps<T>) => {
|
|
54
|
+
const {
|
|
55
|
+
listGestures,
|
|
56
|
+
activeTabIndex,
|
|
57
|
+
activeListOffset,
|
|
58
|
+
registerListScroller,
|
|
59
|
+
} = useCollapsibleTabsContext();
|
|
60
|
+
const { index } = useTabSelfContext();
|
|
61
|
+
const selfOffset = useSharedValue(0);
|
|
62
|
+
const listRef = useAnimatedRef<any>();
|
|
63
|
+
|
|
64
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
65
|
+
selfOffset.value = event.contentOffset.y;
|
|
66
|
+
if (activeTabIndex.value === index)
|
|
67
|
+
activeListOffset.value = event.contentOffset.y;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const composedScrollEvent = useComposedEventHandler(
|
|
71
|
+
props.onScroll ? [onScroll, props.onScroll] : [onScroll],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const myListGesture = listGestures[index];
|
|
75
|
+
|
|
76
|
+
const renderScrollComponent = useCallback(
|
|
77
|
+
(scrollProps: ScrollViewProps) => (
|
|
78
|
+
<GestureDetector gesture={myListGesture}>
|
|
79
|
+
<Animated.ScrollView {...scrollProps} />
|
|
80
|
+
</GestureDetector>
|
|
81
|
+
),
|
|
82
|
+
[myListGesture],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
useAnimatedReaction(
|
|
86
|
+
() => activeTabIndex.value,
|
|
87
|
+
(value) => {
|
|
88
|
+
if (value === index) activeListOffset.value = selfOffset.value;
|
|
89
|
+
},
|
|
90
|
+
[index],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const scroller = useMemo(
|
|
94
|
+
(): ListScroller =>
|
|
95
|
+
(animated = true) => {
|
|
96
|
+
"worklet";
|
|
97
|
+
scrollTo(listRef, 0, 0, animated);
|
|
98
|
+
},
|
|
99
|
+
[listRef],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
registerListScroller(index, scroller);
|
|
104
|
+
return () => registerListScroller(index, null);
|
|
105
|
+
}, [index, registerListScroller, scroller]);
|
|
106
|
+
|
|
107
|
+
const stableLayout = useStableCallback(onLayout);
|
|
108
|
+
const stableContentSizeChange = useStableCallback(onContentSizeChange);
|
|
109
|
+
|
|
110
|
+
const handleLayout = useCallback(
|
|
111
|
+
(event: LayoutChangeEvent) => {
|
|
112
|
+
stableLayout?.(event);
|
|
113
|
+
},
|
|
114
|
+
[stableLayout],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const handleContentSizeChange = useCallback(
|
|
118
|
+
(width: number, height: number) => {
|
|
119
|
+
stableContentSizeChange?.(width, height);
|
|
120
|
+
},
|
|
121
|
+
[stableContentSizeChange],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<View style={styles.view} collapsable={false}>
|
|
126
|
+
<AnimatedFlashList<T>
|
|
127
|
+
ref={listRef}
|
|
128
|
+
scrollEventThrottle={16}
|
|
129
|
+
showsVerticalScrollIndicator
|
|
130
|
+
directionalLockEnabled
|
|
131
|
+
keyboardShouldPersistTaps="handled"
|
|
132
|
+
{...props}
|
|
133
|
+
renderScrollComponent={renderScrollComponent}
|
|
134
|
+
onScroll={composedScrollEvent}
|
|
135
|
+
onLayout={handleLayout}
|
|
136
|
+
onContentSizeChange={handleContentSizeChange}
|
|
137
|
+
/>
|
|
138
|
+
</View>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const styles = StyleSheet.create({
|
|
143
|
+
view: { position: "relative", flex: 1 },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
CollapsibleFlashList.displayName = "CollapsibleTabs.CollapsibleFlashList";
|
|
147
|
+
|
|
148
|
+
export default memo(CollapsibleFlashList) as <T>(
|
|
149
|
+
props: CollapsibleFlashListProps<T>,
|
|
150
|
+
) => ReactElement;
|
package/src/Header.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { memo, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Platform, ViewProps } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
6
|
+
import Animated, { clamp, useAnimatedStyle, withDecay } from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
import { useCollapsibleTabsContext } from './Context';
|
|
9
|
+
|
|
10
|
+
const DECELERATION = Platform.OS === 'android' ? 0.985 : 0.998;
|
|
11
|
+
|
|
12
|
+
const Header = ({ children, style, ...props }: ViewProps) => {
|
|
13
|
+
const { headerOffset, staticHeight, offsetAdjustment } = useCollapsibleTabsContext();
|
|
14
|
+
|
|
15
|
+
const panGesture = useMemo(
|
|
16
|
+
() =>
|
|
17
|
+
Gesture.Pan()
|
|
18
|
+
.activeOffsetY([-5, 5])
|
|
19
|
+
.failOffsetX([-5, 5])
|
|
20
|
+
.maxPointers(1)
|
|
21
|
+
.onChange((evt) => {
|
|
22
|
+
const minOffset = -(staticHeight.value - offsetAdjustment.value);
|
|
23
|
+
headerOffset.value = clamp(headerOffset.value + evt.changeY, minOffset, 0);
|
|
24
|
+
})
|
|
25
|
+
.onEnd((evt) => {
|
|
26
|
+
const minOffset = -(staticHeight.value - offsetAdjustment.value);
|
|
27
|
+
headerOffset.value = withDecay({ velocity: evt.velocityY, rubberBandEffect: false, clamp: [minOffset, 0], deceleration: DECELERATION });
|
|
28
|
+
}),
|
|
29
|
+
[headerOffset, offsetAdjustment, staticHeight]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: headerOffset.value }] }), []);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<GestureDetector gesture={panGesture}>
|
|
36
|
+
<Animated.View {...props} style={[style, animatedStyle]} collapsable={false}>
|
|
37
|
+
{children}
|
|
38
|
+
</Animated.View>
|
|
39
|
+
</GestureDetector>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Header.displayName = 'CollapsibleTabs.Header';
|
|
44
|
+
|
|
45
|
+
export default memo(Header);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PixelRatio,
|
|
5
|
+
StyleProp,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ViewProps,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
|
|
11
|
+
import Animated, {
|
|
12
|
+
AnimatedStyle,
|
|
13
|
+
interpolate,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
useDerivedValue,
|
|
16
|
+
} from "react-native-reanimated";
|
|
17
|
+
|
|
18
|
+
import { useCollapsibleTabsContext } from "./Context";
|
|
19
|
+
|
|
20
|
+
type CommonIndicatorProps = {
|
|
21
|
+
style?: AnimatedStyle<StyleProp<ViewStyle>>;
|
|
22
|
+
color?: string;
|
|
23
|
+
borderRadius?: number;
|
|
24
|
+
} & ViewProps;
|
|
25
|
+
|
|
26
|
+
export const MaterialIndicator = memo(
|
|
27
|
+
({
|
|
28
|
+
style,
|
|
29
|
+
color = "#111827",
|
|
30
|
+
borderRadius = 999,
|
|
31
|
+
...props
|
|
32
|
+
}: CommonIndicatorProps) => {
|
|
33
|
+
const { itemLayout, pageDecimal } = useCollapsibleTabsContext();
|
|
34
|
+
|
|
35
|
+
const data = useDerivedValue(() => {
|
|
36
|
+
if (!itemLayout || itemLayout.length === 0)
|
|
37
|
+
return { input: [], width: [], translateX: [] };
|
|
38
|
+
const input = new Array<number>(itemLayout.length);
|
|
39
|
+
const translateX = new Array<number>(itemLayout.length);
|
|
40
|
+
const width = new Array<number>(itemLayout.length);
|
|
41
|
+
for (let index = 0; index < itemLayout.length; index += 1) {
|
|
42
|
+
const item = itemLayout[index];
|
|
43
|
+
input[index] = index;
|
|
44
|
+
translateX[index] = item.x;
|
|
45
|
+
width[index] = item.width;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
input,
|
|
49
|
+
translateX,
|
|
50
|
+
width,
|
|
51
|
+
};
|
|
52
|
+
}, [itemLayout]);
|
|
53
|
+
|
|
54
|
+
const animatedStyles = useAnimatedStyle(() => {
|
|
55
|
+
if (
|
|
56
|
+
data.value.input.length === 0 ||
|
|
57
|
+
data.value.width.length === 0 ||
|
|
58
|
+
data.value.translateX.length === 0
|
|
59
|
+
)
|
|
60
|
+
return { opacity: 0, width: 0 };
|
|
61
|
+
if (data.value.input.length === 1)
|
|
62
|
+
return {
|
|
63
|
+
opacity: 1,
|
|
64
|
+
width: data.value.width[0],
|
|
65
|
+
transform: [{ translateX: data.value.translateX[0] }],
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
opacity: 1,
|
|
69
|
+
width: interpolate(
|
|
70
|
+
pageDecimal.value,
|
|
71
|
+
data.value.input,
|
|
72
|
+
data.value.width,
|
|
73
|
+
"clamp",
|
|
74
|
+
),
|
|
75
|
+
transform: [
|
|
76
|
+
{
|
|
77
|
+
translateX: interpolate(
|
|
78
|
+
pageDecimal.value,
|
|
79
|
+
data.value.input,
|
|
80
|
+
data.value.translateX,
|
|
81
|
+
"clamp",
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Animated.View
|
|
90
|
+
style={[
|
|
91
|
+
styles.indicator,
|
|
92
|
+
{ backgroundColor: color, borderRadius },
|
|
93
|
+
style,
|
|
94
|
+
animatedStyles,
|
|
95
|
+
]}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
export const SegmentIndicator = memo(
|
|
103
|
+
({
|
|
104
|
+
style,
|
|
105
|
+
color = "#ffffff",
|
|
106
|
+
borderRadius = 999,
|
|
107
|
+
...props
|
|
108
|
+
}: CommonIndicatorProps) => {
|
|
109
|
+
const { itemLayout, pageDecimal } = useCollapsibleTabsContext();
|
|
110
|
+
|
|
111
|
+
const data = useDerivedValue(() => {
|
|
112
|
+
if (!itemLayout || itemLayout.length === 0)
|
|
113
|
+
return { input: [], width: [], translateX: [], height: [] };
|
|
114
|
+
const input = new Array<number>(itemLayout.length);
|
|
115
|
+
const translateX = new Array<number>(itemLayout.length);
|
|
116
|
+
const width = new Array<number>(itemLayout.length);
|
|
117
|
+
const height = new Array<number>(itemLayout.length);
|
|
118
|
+
for (let index = 0; index < itemLayout.length; index += 1) {
|
|
119
|
+
const item = itemLayout[index];
|
|
120
|
+
input[index] = index;
|
|
121
|
+
translateX[index] = item.x;
|
|
122
|
+
width[index] = item.width;
|
|
123
|
+
height[index] = item.height;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
input,
|
|
127
|
+
translateX,
|
|
128
|
+
width,
|
|
129
|
+
height,
|
|
130
|
+
};
|
|
131
|
+
}, [itemLayout]);
|
|
132
|
+
|
|
133
|
+
const animatedStyles = useAnimatedStyle(() => {
|
|
134
|
+
const input = data.value.input;
|
|
135
|
+
const width = data.value.width;
|
|
136
|
+
const translateX = data.value.translateX;
|
|
137
|
+
const height = data.value.height;
|
|
138
|
+
if (input.length === 0 || width.length === 0 || translateX.length === 0)
|
|
139
|
+
return { opacity: 0, width: 0 };
|
|
140
|
+
if (input.length === 1)
|
|
141
|
+
return {
|
|
142
|
+
opacity: 1,
|
|
143
|
+
width: width[0],
|
|
144
|
+
transform: [{ translateX: translateX[0] }],
|
|
145
|
+
height: height[0],
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
opacity: 1,
|
|
149
|
+
width: interpolate(pageDecimal.value, input, width, "clamp"),
|
|
150
|
+
transform: [
|
|
151
|
+
{
|
|
152
|
+
translateX: interpolate(
|
|
153
|
+
pageDecimal.value,
|
|
154
|
+
input,
|
|
155
|
+
translateX,
|
|
156
|
+
"clamp",
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
height: height[0],
|
|
161
|
+
};
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Animated.View
|
|
166
|
+
style={[
|
|
167
|
+
styles.segment,
|
|
168
|
+
{ backgroundColor: color, borderRadius },
|
|
169
|
+
style,
|
|
170
|
+
animatedStyles,
|
|
171
|
+
]}
|
|
172
|
+
{...props}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
MaterialIndicator.displayName = "CollapsibleTabs.MaterialIndicator";
|
|
179
|
+
SegmentIndicator.displayName = "CollapsibleTabs.SegmentIndicator";
|
|
180
|
+
|
|
181
|
+
const styles = StyleSheet.create({
|
|
182
|
+
indicator: {
|
|
183
|
+
height: PixelRatio.roundToNearestPixel(3),
|
|
184
|
+
position: "absolute",
|
|
185
|
+
left: 0,
|
|
186
|
+
bottom: 0,
|
|
187
|
+
},
|
|
188
|
+
segment: {
|
|
189
|
+
position: "absolute",
|
|
190
|
+
left: 0,
|
|
191
|
+
zIndex: 0,
|
|
192
|
+
},
|
|
193
|
+
});
|
package/src/Lazy.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { StyleProp, StyleSheet, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
|
6
|
+
|
|
7
|
+
import { useStableCallback } from './useStableCallback';
|
|
8
|
+
|
|
9
|
+
type AnimatedViewProps = ComponentProps<typeof Animated.View>;
|
|
10
|
+
type LazyViewProps = Omit<AnimatedViewProps, 'children' | 'entering' | 'exiting' | 'style'>;
|
|
11
|
+
|
|
12
|
+
export type LazyPlaceholderInfo = {
|
|
13
|
+
focused: boolean;
|
|
14
|
+
canMount: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type LazyProps = {
|
|
18
|
+
focused: boolean;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
placeholder?: ReactNode;
|
|
21
|
+
renderPlaceholder?: (info: LazyPlaceholderInfo) => ReactNode;
|
|
22
|
+
placeholderStyle?: StyleProp<ViewStyle>;
|
|
23
|
+
placeholderProps?: LazyViewProps & { style?: StyleProp<ViewStyle> };
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
containerProps?: LazyViewProps & { style?: StyleProp<ViewStyle> };
|
|
26
|
+
disableEntering?: boolean;
|
|
27
|
+
disableExiting?: boolean;
|
|
28
|
+
entering?: AnimatedViewProps['entering'] | null;
|
|
29
|
+
exiting?: AnimatedViewProps['exiting'] | null;
|
|
30
|
+
enteringDuration?: number;
|
|
31
|
+
enteringDelay?: number;
|
|
32
|
+
exitingDuration?: number;
|
|
33
|
+
duration?: number;
|
|
34
|
+
delay?: number;
|
|
35
|
+
onMount?: () => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function Lazy({
|
|
39
|
+
placeholder,
|
|
40
|
+
renderPlaceholder,
|
|
41
|
+
placeholderStyle,
|
|
42
|
+
placeholderProps,
|
|
43
|
+
containerProps,
|
|
44
|
+
disableEntering = false,
|
|
45
|
+
disableExiting = false,
|
|
46
|
+
entering,
|
|
47
|
+
exiting,
|
|
48
|
+
enteringDuration,
|
|
49
|
+
enteringDelay,
|
|
50
|
+
exitingDuration = 300,
|
|
51
|
+
focused,
|
|
52
|
+
duration = 200,
|
|
53
|
+
delay = 50,
|
|
54
|
+
onMount,
|
|
55
|
+
children,
|
|
56
|
+
style,
|
|
57
|
+
}: LazyProps) {
|
|
58
|
+
const [canMount, setCanMount] = useState(false);
|
|
59
|
+
const mountNotifiedRef = useRef(false);
|
|
60
|
+
const stableOnMount = useStableCallback(onMount);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (focused) {
|
|
64
|
+
setCanMount(true);
|
|
65
|
+
if (!mountNotifiedRef.current) {
|
|
66
|
+
mountNotifiedRef.current = true;
|
|
67
|
+
stableOnMount();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, [focused, stableOnMount]);
|
|
71
|
+
|
|
72
|
+
const placeholderPointerEvents = placeholderProps?.pointerEvents ?? 'box-none';
|
|
73
|
+
const { style: placeholderContainerStyle, pointerEvents: _placeholderPointerEvents, ...restPlaceholderProps } = placeholderProps ?? {};
|
|
74
|
+
const { style: containerStyle, ...restContainerProps } = containerProps ?? {};
|
|
75
|
+
|
|
76
|
+
const enteringAnimation =
|
|
77
|
+
disableEntering || entering === null ? undefined : entering ?? FadeIn.duration(enteringDuration ?? duration).delay(enteringDelay ?? delay);
|
|
78
|
+
const exitingAnimation = disableExiting || exiting === null ? undefined : exiting ?? FadeOut.duration(exitingDuration);
|
|
79
|
+
|
|
80
|
+
if (!canMount) {
|
|
81
|
+
return (
|
|
82
|
+
<Animated.View
|
|
83
|
+
{...restPlaceholderProps}
|
|
84
|
+
exiting={exitingAnimation}
|
|
85
|
+
style={[styles.placeholder, placeholderStyle, placeholderContainerStyle]}
|
|
86
|
+
pointerEvents={placeholderPointerEvents}
|
|
87
|
+
>
|
|
88
|
+
{renderPlaceholder ? renderPlaceholder({ focused, canMount }) : placeholder}
|
|
89
|
+
</Animated.View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Animated.View {...restContainerProps} entering={enteringAnimation} style={[styles.container, style, containerStyle]}>
|
|
95
|
+
{children}
|
|
96
|
+
</Animated.View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const styles = StyleSheet.create({
|
|
101
|
+
container: {
|
|
102
|
+
width: '100%',
|
|
103
|
+
flex: 1,
|
|
104
|
+
height: '100%',
|
|
105
|
+
},
|
|
106
|
+
placeholder: {
|
|
107
|
+
paddingVertical: 12,
|
|
108
|
+
width: '100%',
|
|
109
|
+
},
|
|
110
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LegendList,
|
|
5
|
+
LegendListProps as LegendListLibProps,
|
|
6
|
+
LegendListRef,
|
|
7
|
+
} from "@legendapp/list/react-native";
|
|
8
|
+
import { LayoutChangeEvent, StyleSheet, View } from "react-native";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
GestureDetector,
|
|
12
|
+
ScrollView as RNGHScrollView,
|
|
13
|
+
} from "react-native-gesture-handler";
|
|
14
|
+
import Animated, {
|
|
15
|
+
AnimatedProps,
|
|
16
|
+
useAnimatedReaction,
|
|
17
|
+
useAnimatedRef,
|
|
18
|
+
useAnimatedScrollHandler,
|
|
19
|
+
useComposedEventHandler,
|
|
20
|
+
useSharedValue,
|
|
21
|
+
} from "react-native-reanimated";
|
|
22
|
+
|
|
23
|
+
import { ListScroller, useCollapsibleTabsContext } from "./Context";
|
|
24
|
+
import { useTabSelfContext } from "./Tab";
|
|
25
|
+
import { useStableCallback } from "./useStableCallback";
|
|
26
|
+
|
|
27
|
+
const AnimatedLegendList = Animated.createAnimatedComponent(LegendList) as <T>(
|
|
28
|
+
props: AnimatedProps<
|
|
29
|
+
LegendListLibProps<T> & {
|
|
30
|
+
ref?: React.Ref<LegendListRef> | undefined;
|
|
31
|
+
}
|
|
32
|
+
>,
|
|
33
|
+
) => ReactElement;
|
|
34
|
+
|
|
35
|
+
export type CollapsibleLegendListProps<T> = LegendListLibProps<T>;
|
|
36
|
+
|
|
37
|
+
const CollapsibleLegendList = <T,>({
|
|
38
|
+
onLayout,
|
|
39
|
+
onContentSizeChange,
|
|
40
|
+
...props
|
|
41
|
+
}: CollapsibleLegendListProps<T>) => {
|
|
42
|
+
const {
|
|
43
|
+
listGestures,
|
|
44
|
+
activeTabIndex,
|
|
45
|
+
activeListOffset,
|
|
46
|
+
registerListScroller,
|
|
47
|
+
} = useCollapsibleTabsContext();
|
|
48
|
+
const { index } = useTabSelfContext();
|
|
49
|
+
const selfOffset = useSharedValue(0);
|
|
50
|
+
const listRef = useAnimatedRef<any>();
|
|
51
|
+
|
|
52
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
53
|
+
selfOffset.value = event.contentOffset.y;
|
|
54
|
+
if (activeTabIndex.value === index)
|
|
55
|
+
activeListOffset.value = event.contentOffset.y;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const composedScrollEvent = useComposedEventHandler(
|
|
59
|
+
props.onScroll ? [onScroll, props.onScroll] : [onScroll],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
useAnimatedReaction(
|
|
63
|
+
() => activeTabIndex.value,
|
|
64
|
+
(value) => {
|
|
65
|
+
if (value === index) activeListOffset.value = selfOffset.value;
|
|
66
|
+
},
|
|
67
|
+
[index],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const scroller = useMemo(
|
|
71
|
+
(): ListScroller =>
|
|
72
|
+
(animated = true) => {
|
|
73
|
+
(listRef.current as unknown as LegendListRef)?.scrollToOffset({
|
|
74
|
+
offset: 0,
|
|
75
|
+
animated,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
[listRef],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
registerListScroller(index, scroller);
|
|
83
|
+
return () => registerListScroller(index, null);
|
|
84
|
+
}, [index, registerListScroller, scroller]);
|
|
85
|
+
|
|
86
|
+
const stableLayout = useStableCallback(onLayout);
|
|
87
|
+
const stableContentSizeChange = useStableCallback(onContentSizeChange);
|
|
88
|
+
|
|
89
|
+
const handleLayout = useCallback(
|
|
90
|
+
(event: LayoutChangeEvent) => {
|
|
91
|
+
stableLayout?.(event);
|
|
92
|
+
},
|
|
93
|
+
[stableLayout],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const handleContentSizeChange = useCallback(
|
|
97
|
+
(width: number, height: number) => {
|
|
98
|
+
stableContentSizeChange?.(width, height);
|
|
99
|
+
},
|
|
100
|
+
[stableContentSizeChange],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View style={styles.view} collapsable={false}>
|
|
105
|
+
<GestureDetector gesture={listGestures[index]}>
|
|
106
|
+
<AnimatedLegendList<T>
|
|
107
|
+
ref={listRef}
|
|
108
|
+
scrollEventThrottle={16}
|
|
109
|
+
showsVerticalScrollIndicator
|
|
110
|
+
directionalLockEnabled
|
|
111
|
+
keyboardShouldPersistTaps="handled"
|
|
112
|
+
{...props}
|
|
113
|
+
onScroll={composedScrollEvent}
|
|
114
|
+
onLayout={handleLayout}
|
|
115
|
+
onContentSizeChange={handleContentSizeChange}
|
|
116
|
+
/>
|
|
117
|
+
</GestureDetector>
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const styles = StyleSheet.create({
|
|
123
|
+
view: { position: "relative" },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
CollapsibleLegendList.displayName = "CollapsibleTabs.CollapsibleLegendList";
|
|
127
|
+
|
|
128
|
+
export default memo(CollapsibleLegendList) as <T>(
|
|
129
|
+
props: CollapsibleLegendListProps<T>,
|
|
130
|
+
) => ReactElement;
|