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/List.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FlatListProps,
|
|
5
|
+
LayoutChangeEvent,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
View,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
|
|
10
|
+
import { GestureDetector } from "react-native-gesture-handler";
|
|
11
|
+
import Animated, {
|
|
12
|
+
scrollTo,
|
|
13
|
+
useAnimatedReaction,
|
|
14
|
+
useAnimatedRef,
|
|
15
|
+
useAnimatedScrollHandler,
|
|
16
|
+
useComposedEventHandler,
|
|
17
|
+
useSharedValue,
|
|
18
|
+
} from "react-native-reanimated";
|
|
19
|
+
|
|
20
|
+
import { ListScroller, useCollapsibleTabsContext } from "./Context";
|
|
21
|
+
import { useTabSelfContext } from "./Tab";
|
|
22
|
+
import { useStableCallback } from "./useStableCallback";
|
|
23
|
+
|
|
24
|
+
export type ListProps<T> = Omit<FlatListProps<T>, "CellRendererComponent">;
|
|
25
|
+
|
|
26
|
+
const List = <T,>({
|
|
27
|
+
onLayout,
|
|
28
|
+
onContentSizeChange,
|
|
29
|
+
...props
|
|
30
|
+
}: ListProps<T>) => {
|
|
31
|
+
const {
|
|
32
|
+
listGestures,
|
|
33
|
+
activeTabIndex,
|
|
34
|
+
activeListOffset,
|
|
35
|
+
registerListScroller,
|
|
36
|
+
} = useCollapsibleTabsContext();
|
|
37
|
+
const { index } = useTabSelfContext();
|
|
38
|
+
const selfOffset = useSharedValue(0);
|
|
39
|
+
const listRef = useAnimatedRef<Animated.FlatList<T>>();
|
|
40
|
+
|
|
41
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
42
|
+
selfOffset.value = event.contentOffset.y;
|
|
43
|
+
if (activeTabIndex.value === index)
|
|
44
|
+
activeListOffset.value = event.contentOffset.y;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const composedScrollEvent = useComposedEventHandler(
|
|
48
|
+
props.onScroll ? [onScroll, props.onScroll] : [onScroll],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
useAnimatedReaction(
|
|
52
|
+
() => activeTabIndex.value,
|
|
53
|
+
(value) => {
|
|
54
|
+
if (value === index) activeListOffset.value = selfOffset.value;
|
|
55
|
+
},
|
|
56
|
+
[index],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const scroller = useMemo(
|
|
60
|
+
(): ListScroller =>
|
|
61
|
+
(animated = true) => {
|
|
62
|
+
"worklet";
|
|
63
|
+
scrollTo(listRef, 0, 0, animated);
|
|
64
|
+
},
|
|
65
|
+
[listRef],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
registerListScroller(index, scroller);
|
|
70
|
+
return () => registerListScroller(index, null);
|
|
71
|
+
}, [index, registerListScroller, scroller]);
|
|
72
|
+
|
|
73
|
+
const stableLayout = useStableCallback(onLayout);
|
|
74
|
+
const stableContentSizeChange = useStableCallback(onContentSizeChange);
|
|
75
|
+
|
|
76
|
+
const handleLayout = useCallback(
|
|
77
|
+
(event: LayoutChangeEvent) => {
|
|
78
|
+
stableLayout?.(event);
|
|
79
|
+
},
|
|
80
|
+
[stableLayout],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleContentSizeChange = useCallback(
|
|
84
|
+
(width: number, height: number) => {
|
|
85
|
+
stableContentSizeChange?.(width, height);
|
|
86
|
+
},
|
|
87
|
+
[stableContentSizeChange],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View style={styles.view} collapsable={false}>
|
|
92
|
+
<GestureDetector gesture={listGestures[index]}>
|
|
93
|
+
<Animated.FlatList
|
|
94
|
+
ref={listRef}
|
|
95
|
+
scrollEventThrottle={16}
|
|
96
|
+
showsVerticalScrollIndicator
|
|
97
|
+
directionalLockEnabled
|
|
98
|
+
keyboardShouldPersistTaps="handled"
|
|
99
|
+
{...props}
|
|
100
|
+
onScroll={composedScrollEvent}
|
|
101
|
+
onLayout={handleLayout}
|
|
102
|
+
onContentSizeChange={handleContentSizeChange}
|
|
103
|
+
/>
|
|
104
|
+
</GestureDetector>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const styles = StyleSheet.create({
|
|
110
|
+
view: { position: "relative" },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
List.displayName = "CollapsibleTabs.List";
|
|
114
|
+
|
|
115
|
+
export default memo(List) as <T>(props: ListProps<T>) => ReactElement;
|
package/src/Pager.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ReactNode, memo, useCallback, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import { StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
4
|
+
|
|
5
|
+
import { GestureDetector } from "react-native-gesture-handler";
|
|
6
|
+
import PagerView, {
|
|
7
|
+
PagerViewOnPageScrollEventData,
|
|
8
|
+
PagerViewOnPageSelectedEvent,
|
|
9
|
+
PagerViewProps,
|
|
10
|
+
} from "react-native-pager-view";
|
|
11
|
+
import Animated, {
|
|
12
|
+
useAnimatedStyle,
|
|
13
|
+
useEvent,
|
|
14
|
+
useHandler,
|
|
15
|
+
} from "react-native-reanimated";
|
|
16
|
+
|
|
17
|
+
import { useCollapsibleTabsContext } from "./Context";
|
|
18
|
+
|
|
19
|
+
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView);
|
|
20
|
+
|
|
21
|
+
type OnPageScrollWorklet = (
|
|
22
|
+
event: PagerViewOnPageScrollEventData,
|
|
23
|
+
context: Record<string, unknown>,
|
|
24
|
+
) => void;
|
|
25
|
+
|
|
26
|
+
type PageScrollHandlers = {
|
|
27
|
+
onPageScroll?: OnPageScrollWorklet;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function usePageScrollHandler(
|
|
31
|
+
handlers: PageScrollHandlers,
|
|
32
|
+
dependencies?: unknown[],
|
|
33
|
+
) {
|
|
34
|
+
const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
|
|
35
|
+
return useEvent<PagerViewOnPageScrollEventData>(
|
|
36
|
+
(event) => {
|
|
37
|
+
"worklet";
|
|
38
|
+
const { onPageScroll } = handlers;
|
|
39
|
+
if (onPageScroll && event.eventName.endsWith("onPageScroll"))
|
|
40
|
+
onPageScroll(event, context);
|
|
41
|
+
},
|
|
42
|
+
["onPageScroll"],
|
|
43
|
+
doDependenciesDiffer,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type PagerProps = {
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
style?: StyleProp<ViewStyle>;
|
|
50
|
+
height?: number;
|
|
51
|
+
getHeight?: (
|
|
52
|
+
staticHeaderHeight: number,
|
|
53
|
+
stickyHeaderHeight: number,
|
|
54
|
+
) => number;
|
|
55
|
+
} & PagerViewProps;
|
|
56
|
+
|
|
57
|
+
const Pager = ({
|
|
58
|
+
children,
|
|
59
|
+
style,
|
|
60
|
+
height,
|
|
61
|
+
getHeight,
|
|
62
|
+
...pagerProps
|
|
63
|
+
}: PagerProps) => {
|
|
64
|
+
const {
|
|
65
|
+
headerOffset,
|
|
66
|
+
listPanGesture,
|
|
67
|
+
activeTabIndex,
|
|
68
|
+
activeListOffset,
|
|
69
|
+
pageDecimal,
|
|
70
|
+
pagerRef,
|
|
71
|
+
staticHeightValue,
|
|
72
|
+
stickyHeightValue,
|
|
73
|
+
} = useCollapsibleTabsContext();
|
|
74
|
+
|
|
75
|
+
const listHeight =
|
|
76
|
+
height ?? getHeight?.(staticHeightValue, stickyHeightValue) ?? null;
|
|
77
|
+
|
|
78
|
+
const pageScrollHandlers = useMemo<PageScrollHandlers>(
|
|
79
|
+
() => ({
|
|
80
|
+
onPageScroll: (event) => {
|
|
81
|
+
"worklet";
|
|
82
|
+
pageDecimal.value = event.position + event.offset;
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
[pageDecimal],
|
|
86
|
+
);
|
|
87
|
+
const onPageScroll = usePageScrollHandler(pageScrollHandlers);
|
|
88
|
+
|
|
89
|
+
const onPageSelected = useCallback(
|
|
90
|
+
(event: PagerViewOnPageSelectedEvent) => {
|
|
91
|
+
activeTabIndex.value = event.nativeEvent.position;
|
|
92
|
+
activeListOffset.value = 0;
|
|
93
|
+
},
|
|
94
|
+
[activeListOffset, activeTabIndex],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const animatedStyle = useAnimatedStyle(
|
|
98
|
+
() => ({
|
|
99
|
+
...(listHeight != null ? { height: listHeight } : null),
|
|
100
|
+
transform: [{ translateY: headerOffset.value }],
|
|
101
|
+
}),
|
|
102
|
+
[listHeight],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<GestureDetector gesture={listPanGesture}>
|
|
107
|
+
<Animated.View
|
|
108
|
+
style={[listHeight == null && styles.flex1, animatedStyle]}
|
|
109
|
+
>
|
|
110
|
+
<AnimatedPagerView
|
|
111
|
+
ref={pagerRef}
|
|
112
|
+
orientation="horizontal"
|
|
113
|
+
overScrollMode="never"
|
|
114
|
+
style={[styles.flex1, style]}
|
|
115
|
+
onPageScroll={
|
|
116
|
+
onPageScroll as unknown as PagerViewProps["onPageScroll"]
|
|
117
|
+
}
|
|
118
|
+
onPageSelected={onPageSelected}
|
|
119
|
+
{...pagerProps}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</AnimatedPagerView>
|
|
123
|
+
</Animated.View>
|
|
124
|
+
</GestureDetector>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
Pager.displayName = "CollapsibleTabs.Pager";
|
|
129
|
+
|
|
130
|
+
const styles = StyleSheet.create({
|
|
131
|
+
flex1: { flex: 1 },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export default memo(Pager);
|
package/src/Root.tsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ReactNode, forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { Gesture } from 'react-native-gesture-handler';
|
|
6
|
+
import PagerView from 'react-native-pager-view';
|
|
7
|
+
import { clamp, useAnimatedReaction, useDerivedValue, useSharedValue, withDecay, withSpring } from 'react-native-reanimated';
|
|
8
|
+
import { scheduleOnRN } from 'react-native-worklets';
|
|
9
|
+
|
|
10
|
+
import { CollapsibleTabsContextProvider, CollapsibleTabsContextValue, ItemLayout, ListScroller } from './Context';
|
|
11
|
+
|
|
12
|
+
export type CollapsibleTabsRootRef = {
|
|
13
|
+
scrollToViewTop: (animated?: boolean) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type RootProps = {
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
pageLength: number;
|
|
19
|
+
initialStaticHeight?: number;
|
|
20
|
+
initialStickyHeight?: number;
|
|
21
|
+
offsetAdjustment?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DECELERATION = Platform.OS === 'android' ? 0.985 : 0.998;
|
|
25
|
+
const HEIGHT_EPSILON = 0.5;
|
|
26
|
+
|
|
27
|
+
type RootInnerProps = Required<Pick<RootProps, 'initialStaticHeight' | 'initialStickyHeight' | 'offsetAdjustment'>> & Pick<RootProps, 'children' | 'pageLength'>;
|
|
28
|
+
|
|
29
|
+
const RootInner = memo(
|
|
30
|
+
forwardRef<CollapsibleTabsRootRef, RootInnerProps>(({ initialStaticHeight, initialStickyHeight, pageLength, offsetAdjustment, children }, ref) => {
|
|
31
|
+
const headerOffset = useSharedValue(0);
|
|
32
|
+
const staticHeight = useSharedValue(initialStaticHeight);
|
|
33
|
+
const stickyHeight = useSharedValue(initialStickyHeight);
|
|
34
|
+
const offsetAdjustmentShared = useDerivedValue(() => offsetAdjustment, [offsetAdjustment]);
|
|
35
|
+
|
|
36
|
+
const activeTabIndex = useSharedValue(0);
|
|
37
|
+
const [activeTabIndexValue, setActiveTabIndexValue] = useState(0);
|
|
38
|
+
useAnimatedReaction(
|
|
39
|
+
() => activeTabIndex.value,
|
|
40
|
+
(next, prev) => {
|
|
41
|
+
if (next !== prev) scheduleOnRN(setActiveTabIndexValue, next);
|
|
42
|
+
},
|
|
43
|
+
[]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const pageDecimal = useSharedValue(0);
|
|
47
|
+
const pagerRef = useRef<PagerView | null>(null);
|
|
48
|
+
const [itemLayout, setItemLayout] = useState<ItemLayout[]>([]);
|
|
49
|
+
const listScrollersRef = useRef<Map<number, ListScroller>>(new Map());
|
|
50
|
+
|
|
51
|
+
const registerButton = useCallback((config: ItemLayout) => {
|
|
52
|
+
setItemLayout((prev) => {
|
|
53
|
+
const next = prev.slice();
|
|
54
|
+
next[config.index] = config;
|
|
55
|
+
return next;
|
|
56
|
+
});
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const registerListScroller = useCallback((index: number, scroller: ListScroller | null) => {
|
|
60
|
+
if (scroller) listScrollersRef.current.set(index, scroller);
|
|
61
|
+
else listScrollersRef.current.delete(index);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
useImperativeHandle(
|
|
65
|
+
ref,
|
|
66
|
+
() => ({
|
|
67
|
+
scrollToViewTop: (animated = true) => {
|
|
68
|
+
const scroller = listScrollersRef.current.get(activeTabIndex.value);
|
|
69
|
+
scroller?.(animated);
|
|
70
|
+
headerOffset.value = withSpring(0, { duration: 250, dampingRatio: 1, mass: 4 });
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
[activeTabIndex, headerOffset]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const [staticHeightValue, setStaticHeightValue] = useState(initialStaticHeight);
|
|
77
|
+
const [stickyHeightValue, setStickyHeightValue] = useState(initialStickyHeight);
|
|
78
|
+
|
|
79
|
+
const updateStaticHeight = useCallback(
|
|
80
|
+
(height: number) => {
|
|
81
|
+
staticHeight.value = height;
|
|
82
|
+
setStaticHeightValue((prev) => {
|
|
83
|
+
if (Math.abs(prev - height) < HEIGHT_EPSILON) return prev;
|
|
84
|
+
return height;
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
[staticHeight]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const updateStickyHeight = useCallback(
|
|
91
|
+
(height: number) => {
|
|
92
|
+
stickyHeight.value = height;
|
|
93
|
+
setStickyHeightValue((prev) => {
|
|
94
|
+
if (Math.abs(prev - height) < HEIGHT_EPSILON) return prev;
|
|
95
|
+
return height;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
[stickyHeight]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const touchX = useSharedValue(0);
|
|
102
|
+
const touchY = useSharedValue(0);
|
|
103
|
+
const isVertical = useSharedValue(false);
|
|
104
|
+
const activeListOffset = useSharedValue(0);
|
|
105
|
+
const listGestures = useMemo(() => Array.from({ length: pageLength }, () => Gesture.Native().cancelsTouchesInView(true)), [pageLength]);
|
|
106
|
+
|
|
107
|
+
const listPanGesture = useMemo(() => {
|
|
108
|
+
return Gesture.Pan()
|
|
109
|
+
.manualActivation(true)
|
|
110
|
+
.maxPointers(1)
|
|
111
|
+
.minPointers(1)
|
|
112
|
+
.onTouchesDown((evt) => {
|
|
113
|
+
const touch = evt.allTouches[0];
|
|
114
|
+
touchX.value = touch.x;
|
|
115
|
+
touchY.value = touch.y;
|
|
116
|
+
isVertical.value = false;
|
|
117
|
+
})
|
|
118
|
+
.onTouchesMove((evt, state) => {
|
|
119
|
+
const touch = evt.allTouches[0];
|
|
120
|
+
const toTop = touch.y > touchY.value;
|
|
121
|
+
const isHorizontal = Math.abs(touch.x - touchX.value) > 5;
|
|
122
|
+
const vertical = Math.abs(touch.y - touchY.value) > 5;
|
|
123
|
+
|
|
124
|
+
if (vertical) isVertical.value = true;
|
|
125
|
+
if (isHorizontal && !isVertical.value) return state.fail();
|
|
126
|
+
if (!vertical) return;
|
|
127
|
+
|
|
128
|
+
const minOffset = -(staticHeight.value - offsetAdjustmentShared.value);
|
|
129
|
+
if (toTop && activeListOffset.value === 0) state.activate();
|
|
130
|
+
else if (!toTop && headerOffset.value > minOffset) state.activate();
|
|
131
|
+
})
|
|
132
|
+
.onChange((evt) => {
|
|
133
|
+
isVertical.value = true;
|
|
134
|
+
const minOffset = -(staticHeight.value - offsetAdjustmentShared.value);
|
|
135
|
+
headerOffset.value = clamp(headerOffset.value + evt.changeY, minOffset, 0);
|
|
136
|
+
})
|
|
137
|
+
.onEnd((evt) => {
|
|
138
|
+
const toTop = evt.translationY > 0;
|
|
139
|
+
const isHeaderPartialShown = headerOffset.value !== 0;
|
|
140
|
+
const minOffset = -(staticHeight.value - offsetAdjustmentShared.value);
|
|
141
|
+
const isFast = Math.abs(evt.velocityY) > 800;
|
|
142
|
+
|
|
143
|
+
if (isFast) {
|
|
144
|
+
if (toTop && isHeaderPartialShown) {
|
|
145
|
+
headerOffset.value = withSpring(0, { duration: 250, dampingRatio: 1, mass: 4, overshootClamping: false, velocity: evt.velocityY });
|
|
146
|
+
} else if (!toTop && isHeaderPartialShown) {
|
|
147
|
+
headerOffset.value = withSpring(minOffset, { duration: 250, dampingRatio: 1, mass: 4, overshootClamping: false, velocity: evt.velocityY });
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
headerOffset.value = withDecay({ velocity: evt.velocityY, rubberBandEffect: false, clamp: [minOffset, 0], deceleration: DECELERATION });
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
.simultaneousWithExternalGesture(...listGestures);
|
|
154
|
+
}, [activeListOffset, headerOffset, isVertical, listGestures, offsetAdjustmentShared, staticHeight, touchX, touchY]);
|
|
155
|
+
|
|
156
|
+
const ctxValue = useMemo(
|
|
157
|
+
(): CollapsibleTabsContextValue => ({
|
|
158
|
+
headerOffset,
|
|
159
|
+
staticHeight,
|
|
160
|
+
stickyHeight,
|
|
161
|
+
offsetAdjustment: offsetAdjustmentShared,
|
|
162
|
+
activeTabIndex,
|
|
163
|
+
activeTabIndexValue,
|
|
164
|
+
pageDecimal,
|
|
165
|
+
listPanGesture,
|
|
166
|
+
listGestures,
|
|
167
|
+
pagerRef,
|
|
168
|
+
itemLayout,
|
|
169
|
+
registerButton,
|
|
170
|
+
registerListScroller,
|
|
171
|
+
staticHeightValue,
|
|
172
|
+
stickyHeightValue,
|
|
173
|
+
activeListOffset,
|
|
174
|
+
updateStaticHeight,
|
|
175
|
+
updateStickyHeight,
|
|
176
|
+
}),
|
|
177
|
+
[activeListOffset, activeTabIndex, activeTabIndexValue, headerOffset, itemLayout, listGestures, listPanGesture, offsetAdjustmentShared, pageDecimal, registerButton, registerListScroller, staticHeight, staticHeightValue, stickyHeight, stickyHeightValue, updateStaticHeight, updateStickyHeight]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return <CollapsibleTabsContextProvider {...ctxValue}>{children}</CollapsibleTabsContextProvider>;
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
RootInner.displayName = 'CollapsibleTabs.RootInner';
|
|
185
|
+
|
|
186
|
+
const Root = forwardRef<CollapsibleTabsRootRef, RootProps>((props, ref) => (
|
|
187
|
+
<RootInner ref={ref} initialStaticHeight={props.initialStaticHeight ?? 0} initialStickyHeight={props.initialStickyHeight ?? 0} pageLength={props.pageLength} offsetAdjustment={props.offsetAdjustment ?? 0}>
|
|
188
|
+
{props.children}
|
|
189
|
+
</RootInner>
|
|
190
|
+
));
|
|
191
|
+
|
|
192
|
+
Root.displayName = 'CollapsibleTabs.Root';
|
|
193
|
+
|
|
194
|
+
export default memo(Root);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LayoutChangeEvent,
|
|
5
|
+
ScrollViewProps as RNScrollViewProps,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
View,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
|
|
10
|
+
import { GestureDetector } from "react-native-gesture-handler";
|
|
11
|
+
import Animated, {
|
|
12
|
+
scrollTo,
|
|
13
|
+
useAnimatedReaction,
|
|
14
|
+
useAnimatedRef,
|
|
15
|
+
useAnimatedScrollHandler,
|
|
16
|
+
useComposedEventHandler,
|
|
17
|
+
useSharedValue,
|
|
18
|
+
} from "react-native-reanimated";
|
|
19
|
+
|
|
20
|
+
import { ListScroller, useCollapsibleTabsContext } from "./Context";
|
|
21
|
+
import { useTabSelfContext } from "./Tab";
|
|
22
|
+
import { useStableCallback } from "./useStableCallback";
|
|
23
|
+
|
|
24
|
+
export type ScrollViewProps = RNScrollViewProps;
|
|
25
|
+
|
|
26
|
+
const ScrollView = ({
|
|
27
|
+
onLayout,
|
|
28
|
+
onContentSizeChange,
|
|
29
|
+
...props
|
|
30
|
+
}: ScrollViewProps) => {
|
|
31
|
+
const {
|
|
32
|
+
listGestures,
|
|
33
|
+
activeTabIndex,
|
|
34
|
+
activeListOffset,
|
|
35
|
+
registerListScroller,
|
|
36
|
+
} = useCollapsibleTabsContext();
|
|
37
|
+
const { index } = useTabSelfContext();
|
|
38
|
+
const selfOffset = useSharedValue(0);
|
|
39
|
+
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
40
|
+
|
|
41
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
42
|
+
selfOffset.value = event.contentOffset.y;
|
|
43
|
+
if (activeTabIndex.value === index)
|
|
44
|
+
activeListOffset.value = event.contentOffset.y;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const composedScrollEvent = useComposedEventHandler(
|
|
48
|
+
props.onScroll ? [onScroll, props.onScroll] : [onScroll],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
useAnimatedReaction(
|
|
52
|
+
() => activeTabIndex.value,
|
|
53
|
+
(value) => {
|
|
54
|
+
if (value === index) activeListOffset.value = selfOffset.value;
|
|
55
|
+
},
|
|
56
|
+
[index],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const scroller = useMemo(
|
|
60
|
+
(): ListScroller =>
|
|
61
|
+
(animated = true) => {
|
|
62
|
+
"worklet";
|
|
63
|
+
scrollTo(scrollRef, 0, 0, animated);
|
|
64
|
+
},
|
|
65
|
+
[scrollRef],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
registerListScroller(index, scroller);
|
|
70
|
+
return () => registerListScroller(index, null);
|
|
71
|
+
}, [index, registerListScroller, scroller]);
|
|
72
|
+
|
|
73
|
+
const stableLayout = useStableCallback(onLayout);
|
|
74
|
+
const stableContentSizeChange = useStableCallback(onContentSizeChange);
|
|
75
|
+
|
|
76
|
+
const handleLayout = useCallback(
|
|
77
|
+
(event: LayoutChangeEvent) => {
|
|
78
|
+
stableLayout?.(event);
|
|
79
|
+
},
|
|
80
|
+
[stableLayout],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleContentSizeChange = useCallback(
|
|
84
|
+
(width: number, height: number) => {
|
|
85
|
+
stableContentSizeChange?.(width, height);
|
|
86
|
+
},
|
|
87
|
+
[stableContentSizeChange],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View style={styles.view} collapsable={false}>
|
|
92
|
+
<GestureDetector gesture={listGestures[index]}>
|
|
93
|
+
<Animated.ScrollView
|
|
94
|
+
ref={scrollRef}
|
|
95
|
+
scrollEventThrottle={16}
|
|
96
|
+
showsVerticalScrollIndicator
|
|
97
|
+
directionalLockEnabled
|
|
98
|
+
keyboardShouldPersistTaps="handled"
|
|
99
|
+
{...props}
|
|
100
|
+
// onScroll={onScroll as RNScrollViewProps["onScroll"]}
|
|
101
|
+
onScroll={composedScrollEvent}
|
|
102
|
+
onLayout={handleLayout}
|
|
103
|
+
onContentSizeChange={handleContentSizeChange}
|
|
104
|
+
/>
|
|
105
|
+
</GestureDetector>
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const styles = StyleSheet.create({
|
|
111
|
+
view: { position: "relative", flex: 1 },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
ScrollView.displayName = "CollapsibleTabs.ScrollView";
|
|
115
|
+
|
|
116
|
+
export default memo(ScrollView) as (props: ScrollViewProps) => ReactElement;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { LayoutChangeEvent, ViewProps } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import Animated from 'react-native-reanimated';
|
|
6
|
+
|
|
7
|
+
import { useCollapsibleTabsContext } from './Context';
|
|
8
|
+
|
|
9
|
+
const StaticHeader = ({ children, onLayout, ...props }: ViewProps) => {
|
|
10
|
+
const { updateStaticHeight, staticHeightValue } = useCollapsibleTabsContext();
|
|
11
|
+
|
|
12
|
+
const handleLayout = (evt: LayoutChangeEvent) => {
|
|
13
|
+
const height = evt.nativeEvent.layout.height;
|
|
14
|
+
if (__DEV__ && (!staticHeightValue || Math.abs(staticHeightValue - height) > 10)) {
|
|
15
|
+
console.info(`Set initialStaticHeight=${height} to reduce first-render flicker.`);
|
|
16
|
+
}
|
|
17
|
+
updateStaticHeight(height);
|
|
18
|
+
onLayout?.(evt);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Animated.View {...props} onLayout={handleLayout}>
|
|
23
|
+
{children}
|
|
24
|
+
</Animated.View>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
StaticHeader.displayName = 'CollapsibleTabs.StaticHeader';
|
|
29
|
+
|
|
30
|
+
export default memo(StaticHeader);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { LayoutChangeEvent, ViewProps } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import Animated from 'react-native-reanimated';
|
|
6
|
+
|
|
7
|
+
import { useCollapsibleTabsContext } from './Context';
|
|
8
|
+
|
|
9
|
+
const StickyHeader = ({ children, onLayout, ...props }: ViewProps) => {
|
|
10
|
+
const { updateStickyHeight, stickyHeightValue } = useCollapsibleTabsContext();
|
|
11
|
+
|
|
12
|
+
const handleLayout = (evt: LayoutChangeEvent) => {
|
|
13
|
+
const height = evt.nativeEvent.layout.height;
|
|
14
|
+
if (__DEV__ && (!stickyHeightValue || Math.abs(stickyHeightValue - height) > 10)) {
|
|
15
|
+
console.info(`Set initialStickyHeight=${height} to reduce first-render flicker.`);
|
|
16
|
+
}
|
|
17
|
+
updateStickyHeight(height);
|
|
18
|
+
onLayout?.(evt);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Animated.View {...props} onLayout={handleLayout}>
|
|
23
|
+
{children}
|
|
24
|
+
</Animated.View>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
StickyHeader.displayName = 'CollapsibleTabs.StickyHeader';
|
|
29
|
+
|
|
30
|
+
export default memo(StickyHeader);
|