react-navigation-reanimated-top-tabs 0.2.0

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.
Files changed (131) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +26 -0
  3. package/lib/commonjs/components/ReanimatedTopTab/AnimationHelper.js +76 -0
  4. package/lib/commonjs/components/ReanimatedTopTab/AnimationHelper.js.map +1 -0
  5. package/lib/commonjs/components/ReanimatedTopTab/ReanimatedTabView.js +125 -0
  6. package/lib/commonjs/components/ReanimatedTopTab/ReanimatedTabView.js.map +1 -0
  7. package/lib/commonjs/components/ReanimatedTopTab/types.js +8 -0
  8. package/lib/commonjs/components/ReanimatedTopTab/types.js.map +1 -0
  9. package/lib/commonjs/components/elements/TabBarBaseComponent.js +71 -0
  10. package/lib/commonjs/components/elements/TabBarBaseComponent.js.map +1 -0
  11. package/lib/commonjs/components/elements/TabBarLabelBaseComponent.js +48 -0
  12. package/lib/commonjs/components/elements/TabBarLabelBaseComponent.js.map +1 -0
  13. package/lib/commonjs/components/wrappers/GestureWrapper.js +100 -0
  14. package/lib/commonjs/components/wrappers/GestureWrapper.js.map +1 -0
  15. package/lib/commonjs/components/wrappers/ScreenWrapper.js +34 -0
  16. package/lib/commonjs/components/wrappers/ScreenWrapper.js.map +1 -0
  17. package/lib/commonjs/components/wrappers/ScrollView.js +58 -0
  18. package/lib/commonjs/components/wrappers/ScrollView.js.map +1 -0
  19. package/lib/commonjs/context/TopTabContext.hooks.js +48 -0
  20. package/lib/commonjs/context/TopTabContext.hooks.js.map +1 -0
  21. package/lib/commonjs/context/TopTabContext.js +86 -0
  22. package/lib/commonjs/context/TopTabContext.js.map +1 -0
  23. package/lib/commonjs/hooks/TopTab.hooks.js +120 -0
  24. package/lib/commonjs/hooks/TopTab.hooks.js.map +1 -0
  25. package/lib/commonjs/index.js +92 -0
  26. package/lib/commonjs/index.js.map +1 -0
  27. package/lib/commonjs/navigator/createReanimatedTopTabNavigator.js +141 -0
  28. package/lib/commonjs/navigator/createReanimatedTopTabNavigator.js.map +1 -0
  29. package/lib/commonjs/types.js +8 -0
  30. package/lib/commonjs/types.js.map +1 -0
  31. package/lib/module/components/ReanimatedTopTab/AnimationHelper.js +72 -0
  32. package/lib/module/components/ReanimatedTopTab/AnimationHelper.js.map +1 -0
  33. package/lib/module/components/ReanimatedTopTab/ReanimatedTabView.js +119 -0
  34. package/lib/module/components/ReanimatedTopTab/ReanimatedTabView.js.map +1 -0
  35. package/lib/module/components/ReanimatedTopTab/types.js +4 -0
  36. package/lib/module/components/ReanimatedTopTab/types.js.map +1 -0
  37. package/lib/module/components/elements/TabBarBaseComponent.js +64 -0
  38. package/lib/module/components/elements/TabBarBaseComponent.js.map +1 -0
  39. package/lib/module/components/elements/TabBarLabelBaseComponent.js +41 -0
  40. package/lib/module/components/elements/TabBarLabelBaseComponent.js.map +1 -0
  41. package/lib/module/components/wrappers/GestureWrapper.js +94 -0
  42. package/lib/module/components/wrappers/GestureWrapper.js.map +1 -0
  43. package/lib/module/components/wrappers/ScreenWrapper.js +28 -0
  44. package/lib/module/components/wrappers/ScreenWrapper.js.map +1 -0
  45. package/lib/module/components/wrappers/ScrollView.js +52 -0
  46. package/lib/module/components/wrappers/ScrollView.js.map +1 -0
  47. package/lib/module/context/TopTabContext.hooks.js +44 -0
  48. package/lib/module/context/TopTabContext.hooks.js.map +1 -0
  49. package/lib/module/context/TopTabContext.js +82 -0
  50. package/lib/module/context/TopTabContext.js.map +1 -0
  51. package/lib/module/hooks/TopTab.hooks.js +110 -0
  52. package/lib/module/hooks/TopTab.hooks.js.map +1 -0
  53. package/lib/module/index.js +11 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/navigator/createReanimatedTopTabNavigator.js +134 -0
  56. package/lib/module/navigator/createReanimatedTopTabNavigator.js.map +1 -0
  57. package/lib/module/types.js +4 -0
  58. package/lib/module/types.js.map +1 -0
  59. package/lib/typescript/commonjs/package.json +1 -0
  60. package/lib/typescript/commonjs/src/components/ReanimatedTopTab/AnimationHelper.d.ts +11 -0
  61. package/lib/typescript/commonjs/src/components/ReanimatedTopTab/AnimationHelper.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/src/components/ReanimatedTopTab/ReanimatedTabView.d.ts +14 -0
  63. package/lib/typescript/commonjs/src/components/ReanimatedTopTab/ReanimatedTabView.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/src/components/ReanimatedTopTab/types.d.ts +30 -0
  65. package/lib/typescript/commonjs/src/components/ReanimatedTopTab/types.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/src/components/elements/TabBarBaseComponent.d.ts +24 -0
  67. package/lib/typescript/commonjs/src/components/elements/TabBarBaseComponent.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/src/components/elements/TabBarLabelBaseComponent.d.ts +8 -0
  69. package/lib/typescript/commonjs/src/components/elements/TabBarLabelBaseComponent.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/src/components/wrappers/GestureWrapper.d.ts +2 -0
  71. package/lib/typescript/commonjs/src/components/wrappers/GestureWrapper.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/src/components/wrappers/ScreenWrapper.d.ts +6 -0
  73. package/lib/typescript/commonjs/src/components/wrappers/ScreenWrapper.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/src/components/wrappers/ScrollView.d.ts +6 -0
  75. package/lib/typescript/commonjs/src/components/wrappers/ScrollView.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/src/context/TopTabContext.d.ts +4 -0
  77. package/lib/typescript/commonjs/src/context/TopTabContext.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/src/context/TopTabContext.hooks.d.ts +5 -0
  79. package/lib/typescript/commonjs/src/context/TopTabContext.hooks.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/src/hooks/TopTab.hooks.d.ts +33 -0
  81. package/lib/typescript/commonjs/src/hooks/TopTab.hooks.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/src/index.d.ts +10 -0
  83. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/src/navigator/createReanimatedTopTabNavigator.d.ts +4 -0
  85. package/lib/typescript/commonjs/src/navigator/createReanimatedTopTabNavigator.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/src/types.d.ts +56 -0
  87. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  88. package/lib/typescript/module/package.json +1 -0
  89. package/lib/typescript/module/src/components/ReanimatedTopTab/AnimationHelper.d.ts +11 -0
  90. package/lib/typescript/module/src/components/ReanimatedTopTab/AnimationHelper.d.ts.map +1 -0
  91. package/lib/typescript/module/src/components/ReanimatedTopTab/ReanimatedTabView.d.ts +14 -0
  92. package/lib/typescript/module/src/components/ReanimatedTopTab/ReanimatedTabView.d.ts.map +1 -0
  93. package/lib/typescript/module/src/components/ReanimatedTopTab/types.d.ts +30 -0
  94. package/lib/typescript/module/src/components/ReanimatedTopTab/types.d.ts.map +1 -0
  95. package/lib/typescript/module/src/components/elements/TabBarBaseComponent.d.ts +24 -0
  96. package/lib/typescript/module/src/components/elements/TabBarBaseComponent.d.ts.map +1 -0
  97. package/lib/typescript/module/src/components/elements/TabBarLabelBaseComponent.d.ts +8 -0
  98. package/lib/typescript/module/src/components/elements/TabBarLabelBaseComponent.d.ts.map +1 -0
  99. package/lib/typescript/module/src/components/wrappers/GestureWrapper.d.ts +2 -0
  100. package/lib/typescript/module/src/components/wrappers/GestureWrapper.d.ts.map +1 -0
  101. package/lib/typescript/module/src/components/wrappers/ScreenWrapper.d.ts +6 -0
  102. package/lib/typescript/module/src/components/wrappers/ScreenWrapper.d.ts.map +1 -0
  103. package/lib/typescript/module/src/components/wrappers/ScrollView.d.ts +6 -0
  104. package/lib/typescript/module/src/components/wrappers/ScrollView.d.ts.map +1 -0
  105. package/lib/typescript/module/src/context/TopTabContext.d.ts +4 -0
  106. package/lib/typescript/module/src/context/TopTabContext.d.ts.map +1 -0
  107. package/lib/typescript/module/src/context/TopTabContext.hooks.d.ts +5 -0
  108. package/lib/typescript/module/src/context/TopTabContext.hooks.d.ts.map +1 -0
  109. package/lib/typescript/module/src/hooks/TopTab.hooks.d.ts +33 -0
  110. package/lib/typescript/module/src/hooks/TopTab.hooks.d.ts.map +1 -0
  111. package/lib/typescript/module/src/index.d.ts +10 -0
  112. package/lib/typescript/module/src/index.d.ts.map +1 -0
  113. package/lib/typescript/module/src/navigator/createReanimatedTopTabNavigator.d.ts +4 -0
  114. package/lib/typescript/module/src/navigator/createReanimatedTopTabNavigator.d.ts.map +1 -0
  115. package/lib/typescript/module/src/types.d.ts +56 -0
  116. package/lib/typescript/module/src/types.d.ts.map +1 -0
  117. package/package.json +204 -0
  118. package/src/components/ReanimatedTopTab/AnimationHelper.ts +94 -0
  119. package/src/components/ReanimatedTopTab/ReanimatedTabView.tsx +200 -0
  120. package/src/components/ReanimatedTopTab/types.ts +29 -0
  121. package/src/components/elements/TabBarBaseComponent.tsx +99 -0
  122. package/src/components/elements/TabBarLabelBaseComponent.tsx +63 -0
  123. package/src/components/wrappers/GestureWrapper.tsx +135 -0
  124. package/src/components/wrappers/ScreenWrapper.tsx +27 -0
  125. package/src/components/wrappers/ScrollView.tsx +59 -0
  126. package/src/context/TopTabContext.hooks.ts +55 -0
  127. package/src/context/TopTabContext.tsx +115 -0
  128. package/src/hooks/TopTab.hooks.ts +112 -0
  129. package/src/index.tsx +15 -0
  130. package/src/navigator/createReanimatedTopTabNavigator.tsx +192 -0
  131. package/src/types.ts +63 -0
@@ -0,0 +1,200 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { StyleSheet, useWindowDimensions, View } from 'react-native';
3
+ import {
4
+ Gesture,
5
+ GestureDetector,
6
+ GestureHandlerRootView,
7
+ } from 'react-native-gesture-handler';
8
+ import Animated, {
9
+ Extrapolation,
10
+ interpolate,
11
+ LinearTransition,
12
+ runOnJS,
13
+ useAnimatedStyle,
14
+ useDerivedValue,
15
+ useSharedValue,
16
+ } from 'react-native-reanimated';
17
+ import { AnimationHelper } from './AnimationHelper';
18
+ import type { ReanimatedTabViewTypes } from './types';
19
+
20
+ export interface ReanimatedTabViewProps {
21
+ LazyPlaceholder?: () => React.ReactNode;
22
+ lazy?: boolean;
23
+ navigationState: ReanimatedTabViewTypes.NavigationState;
24
+ onIndexChange: (index: number) => void;
25
+ percentageTrigger?: number;
26
+ positionInterpolation?: ReanimatedTabViewTypes.PositionInterpolation;
27
+ renderScene: (params: ReanimatedTabViewTypes.SceneProps) => React.ReactNode;
28
+ renderTabBar?: (
29
+ params: ReanimatedTabViewTypes.RenderTabsParams
30
+ ) => React.ReactNode;
31
+ }
32
+
33
+ export const ReanimatedTabView = React.memo<ReanimatedTabViewProps>(
34
+ ({
35
+ LazyPlaceholder = () => null,
36
+ lazy = false,
37
+ navigationState,
38
+ onIndexChange,
39
+ percentageTrigger = 0.4,
40
+ positionInterpolation,
41
+ renderScene,
42
+ renderTabBar,
43
+ }) => {
44
+ const { width } = useWindowDimensions();
45
+ const loadedScreens = useRef([
46
+ navigationState.routes[navigationState.index],
47
+ ]);
48
+ // const width = 160;
49
+ const scrollPosition = useSharedValue(navigationState.index);
50
+
51
+ const position = useDerivedValue(() => {
52
+ if (!positionInterpolation) {
53
+ return -scrollPosition.value;
54
+ }
55
+ return interpolate(
56
+ -scrollPosition.value,
57
+ positionInterpolation.input,
58
+ positionInterpolation.output,
59
+ Extrapolation.CLAMP
60
+ );
61
+ }, [positionInterpolation]);
62
+
63
+ useEffect(() => {
64
+ scrollPosition.value = AnimationHelper.animation(
65
+ -navigationState.index * width
66
+ );
67
+ }, [navigationState.index, scrollPosition, width]);
68
+
69
+ const minimumValueToChangeView = useMemo(
70
+ () => width * percentageTrigger,
71
+ [percentageTrigger, width]
72
+ );
73
+
74
+ const panGesture = React.useMemo(
75
+ () =>
76
+ Gesture.Pan()
77
+ .failOffsetY(-10)
78
+ .failOffsetY(10)
79
+ .activeOffsetX([-20, 20])
80
+ .onChange((event) => {
81
+ scrollPosition.value = AnimationHelper.onChange(
82
+ event,
83
+ scrollPosition.value,
84
+ width,
85
+ navigationState
86
+ );
87
+ })
88
+ .onEnd((event) => {
89
+ const state = AnimationHelper.onEnd(
90
+ event,
91
+ minimumValueToChangeView,
92
+ width,
93
+ navigationState
94
+ );
95
+ scrollPosition.value = AnimationHelper.animation(state.value);
96
+ runOnJS(onIndexChange)(state.index);
97
+ }),
98
+ [
99
+ minimumValueToChangeView,
100
+ navigationState,
101
+ onIndexChange,
102
+ scrollPosition,
103
+ width,
104
+ ]
105
+ );
106
+
107
+ const scrollPositionStyle = useAnimatedStyle(() => ({
108
+ transform: [
109
+ {
110
+ translateX: interpolate(
111
+ scrollPosition.value,
112
+ [-width * (navigationState.routes.length - 1), 0],
113
+ [-width * (navigationState.routes.length - 1), 0],
114
+ Extrapolation.CLAMP
115
+ ),
116
+ },
117
+ ],
118
+ }));
119
+
120
+ const jumpTo = React.useCallback(
121
+ (key: string) => {
122
+ const newIndex = navigationState.routes.findIndex((r) => r.key === key);
123
+ if (newIndex === -1) {
124
+ // throw
125
+ return;
126
+ }
127
+ scrollPosition.value = AnimationHelper.animation(-newIndex * width);
128
+ onIndexChange(newIndex);
129
+ },
130
+ [navigationState.routes, onIndexChange, scrollPosition, width]
131
+ );
132
+
133
+ const chooseRender = useCallback(
134
+ (params: ReanimatedTabViewTypes.SceneProps, useRenderScene = true) => (
135
+ <View key={`RNNTabView_${params.route.key}`} style={{ width }}>
136
+ {useRenderScene ? renderScene(params) : LazyPlaceholder()}
137
+ </View>
138
+ ),
139
+ [LazyPlaceholder, renderScene, width]
140
+ );
141
+
142
+ const Routes = useMemo(
143
+ () =>
144
+ navigationState.routes.map((route, index) => {
145
+ if (!lazy) {
146
+ return chooseRender({ jumpTo, route });
147
+ }
148
+ const screen = loadedScreens.current.find(
149
+ (loadedScreen) => loadedScreen?.key === route.key
150
+ );
151
+ if (screen !== undefined) {
152
+ return chooseRender({ jumpTo, route });
153
+ }
154
+ if (navigationState.index === index) {
155
+ loadedScreens.current.push(route);
156
+ return chooseRender({ jumpTo, route });
157
+ }
158
+ return chooseRender({ jumpTo, route }, false);
159
+ }),
160
+ [
161
+ navigationState.routes,
162
+ navigationState.index,
163
+ lazy,
164
+ chooseRender,
165
+ jumpTo,
166
+ ]
167
+ );
168
+
169
+ return (
170
+ <GestureHandlerRootView style={defaultStyles.flex}>
171
+ <Animated.View layout={LinearTransition} style={defaultStyles.flex}>
172
+ {renderTabBar ? renderTabBar({ navigationState, position }) : null}
173
+ <GestureDetector gesture={panGesture}>
174
+ <Animated.View
175
+ layout={LinearTransition}
176
+ style={[
177
+ scrollPositionStyle,
178
+ defaultStyles.flex,
179
+ { width: width * navigationState.routes.length - 1 },
180
+ defaultStyles.viewsContainer,
181
+ ]}
182
+ >
183
+ {Routes}
184
+ </Animated.View>
185
+ </GestureDetector>
186
+ </Animated.View>
187
+ </GestureHandlerRootView>
188
+ );
189
+ }
190
+ );
191
+
192
+ //NOTE: purposely use of StyleSheet - simplify migration to library
193
+ const defaultStyles = StyleSheet.create({
194
+ flex: {
195
+ flex: 1,
196
+ },
197
+ viewsContainer: {
198
+ flexDirection: 'row',
199
+ },
200
+ });
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+
4
+ export namespace ReanimatedTabViewTypes {
5
+ export interface Route {
6
+ key: string;
7
+ name: string;
8
+ tabBarLabel?: () => React.ReactNode;
9
+ title?: string;
10
+ }
11
+
12
+ export type NavigationState = { index: number; routes: Route[] };
13
+
14
+ export interface RenderTabsParams {
15
+ navigationState: NavigationState;
16
+ position: SharedValue<number>;
17
+ }
18
+
19
+ export interface SceneProps {
20
+ jumpTo?: (key: string) => void;
21
+ route: Route;
22
+ }
23
+
24
+ export interface SceneMapRoutes {
25
+ [key: string]: React.ReactNode;
26
+ }
27
+
28
+ export type PositionInterpolation = { input: number[]; output: number[] };
29
+ }
@@ -0,0 +1,99 @@
1
+ import * as React from 'react';
2
+ import { Pressable, StyleSheet, Text, useWindowDimensions } from 'react-native';
3
+ import Reanimated, {
4
+ type AnimatedStyle,
5
+ interpolate,
6
+ type SharedValue,
7
+ useAnimatedReaction,
8
+ } from 'react-native-reanimated';
9
+ import { useTabContext } from '../../hooks/TopTab.hooks';
10
+
11
+ export interface Route {
12
+ key: string;
13
+ name?: string;
14
+ tabBarLabel?: () => React.ReactNode;
15
+ title?: string;
16
+ }
17
+
18
+ export type NavigationState = { index: number; routes: Route[] };
19
+
20
+ export interface RenderTabsParams {
21
+ navigationState: NavigationState;
22
+ position: SharedValue<number>;
23
+ }
24
+
25
+ interface TabBarProps extends RenderTabsParams {
26
+ children?: React.ReactNode;
27
+ navigate: (route: Route) => void;
28
+ style?: AnimatedStyle;
29
+ }
30
+
31
+ export const TabBarBaseComponent = ({
32
+ children,
33
+ navigate,
34
+ navigationState,
35
+ position,
36
+ style,
37
+ }: TabBarProps) => {
38
+ const layout = useWindowDimensions();
39
+
40
+ const { topTabHeight, transformationX } = useTabContext();
41
+
42
+ //Assign interpolated offsetX to transformationX
43
+ useAnimatedReaction(
44
+ () =>
45
+ interpolate(
46
+ position.value,
47
+ navigationState.routes.map((_, i) => i * layout.width),
48
+ navigationState.routes.map((_, i) => i),
49
+ 'clamp'
50
+ ),
51
+ (value) => {
52
+ transformationX.value = value;
53
+ },
54
+ [navigationState.routes]
55
+ );
56
+
57
+ return (
58
+ <Reanimated.View
59
+ onLayout={({ nativeEvent }) => {
60
+ topTabHeight.value = nativeEvent.layout.height;
61
+ }}
62
+ >
63
+ <Reanimated.View style={[styles.wrapper, style]}>
64
+ {navigationState.routes.map((route) => (
65
+ <Pressable
66
+ style={styles.labelWrapper}
67
+ key={route.key}
68
+ onPress={() => {
69
+ navigate(route);
70
+ }}
71
+ >
72
+ {route.tabBarLabel ? (
73
+ route.tabBarLabel()
74
+ ) : (
75
+ <Text style={{ color: 'red' }}>{route.title}</Text>
76
+ )}
77
+ </Pressable>
78
+ ))}
79
+ </Reanimated.View>
80
+ {children}
81
+ </Reanimated.View>
82
+ );
83
+ };
84
+
85
+ // Static styles
86
+ const styles = StyleSheet.create({
87
+ labelWrapper: {
88
+ flex: 1,
89
+ alignItems: 'center',
90
+ justifyContent: 'center',
91
+ },
92
+ wrapper: {
93
+ width: '100%',
94
+ flexDirection: 'row',
95
+ backgroundColor: 'white',
96
+ paddingVertical: 8,
97
+ height: 50,
98
+ },
99
+ });
@@ -0,0 +1,63 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import Reanimated, {
3
+ interpolateColor,
4
+ useAnimatedStyle,
5
+ } from 'react-native-reanimated';
6
+ import { useTabContext } from '../../hooks/TopTab.hooks';
7
+
8
+ interface TabBarLabelTypes {
9
+ focused: boolean;
10
+ index: number;
11
+ title: string;
12
+ }
13
+
14
+ export const TabBarLabelBaseComponent = ({
15
+ index,
16
+ ...props
17
+ }: TabBarLabelTypes) => {
18
+ const { transformationX } = useTabContext();
19
+
20
+ const style = useAnimatedStyle(
21
+ () => ({
22
+ backgroundColor: interpolateColor(
23
+ transformationX.value,
24
+ [index - 1, index, index + 1],
25
+ ['transparent', 'blue', 'transparent']
26
+ ),
27
+ }),
28
+ []
29
+ );
30
+
31
+ const textStyle = useAnimatedStyle(
32
+ () => ({
33
+ color: interpolateColor(
34
+ transformationX.value,
35
+ [index - 1, index, index + 1],
36
+ ['black', 'white', 'black']
37
+ ),
38
+ }),
39
+ []
40
+ );
41
+
42
+ return (
43
+ <Reanimated.View style={[style, styles.wrapper]}>
44
+ <Reanimated.Text style={[textStyle, styles.text]}>
45
+ {props.title}
46
+ </Reanimated.Text>
47
+ </Reanimated.View>
48
+ );
49
+ };
50
+
51
+ const styles = StyleSheet.create({
52
+ text: {
53
+ fontWeight: 'bold',
54
+ textTransform: 'uppercase',
55
+ },
56
+ wrapper: {
57
+ borderRadius: 10,
58
+ height: 35,
59
+ justifyContent: 'center',
60
+ paddingHorizontal: 16,
61
+ paddingVertical: 8,
62
+ },
63
+ });
@@ -0,0 +1,135 @@
1
+ import { useMemo, useRef, useState } from 'react';
2
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
3
+ import Reanimated, {
4
+ Easing,
5
+ interpolate,
6
+ runOnJS,
7
+ useAnimatedReaction,
8
+ useAnimatedStyle,
9
+ useSharedValue,
10
+ withTiming,
11
+ } from 'react-native-reanimated';
12
+ import { useTabContext } from '../../hooks/TopTab.hooks';
13
+
14
+ //TODO: types
15
+ export const GestureWrapper = ({ children }: any) => {
16
+ const {
17
+ currentScreenIndex,
18
+ currentYPosition,
19
+ gestureEnabled,
20
+ headerHeight,
21
+ screenProperties,
22
+ transformationY,
23
+ screenRefs,
24
+ } = useTabContext();
25
+
26
+ const movedY = useSharedValue(0);
27
+
28
+ const gestureRef = useRef(Gesture.Pan());
29
+
30
+ const withTimingConfig = {
31
+ duration: 300,
32
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
33
+ } as const;
34
+
35
+ const [_gestureEnabled, setGestureEnabled] = useState(gestureEnabled.value);
36
+
37
+ useAnimatedReaction(
38
+ () => gestureEnabled.value,
39
+ (nextValue) => {
40
+ runOnJS(setGestureEnabled)(nextValue);
41
+ },
42
+ []
43
+ );
44
+
45
+ const _scrollTo = (index: number, value: number = 0) => {
46
+ const { scrollTo } = Object.values(screenRefs.refs.current)[index]
47
+ ?.current ?? { scrollTo: () => null };
48
+
49
+ scrollTo({
50
+ y: value,
51
+ animated: false,
52
+ });
53
+ };
54
+
55
+ const gesture = useMemo(
56
+ () =>
57
+ Gesture.Pan()
58
+ .activeOffsetY([-10, 10])
59
+ .onTouchesMove(({ state }, stateManager) => {
60
+ const current = Object.values(screenProperties).map(
61
+ ({ scrollY }) => scrollY
62
+ )[currentScreenIndex.value];
63
+
64
+ if ((current?.value ?? 0) > 0 && state !== 4 && state !== 3) {
65
+ stateManager.fail();
66
+ }
67
+ })
68
+ .onUpdate(({ translationY }) => {
69
+ if (translationY <= -headerHeight.value) {
70
+ runOnJS(_scrollTo)(
71
+ currentScreenIndex.value,
72
+ Math.abs(translationY + headerHeight.value)
73
+ );
74
+ }
75
+
76
+ transformationY.value =
77
+ currentYPosition.value + translationY - movedY.value;
78
+ })
79
+ .onEnd(({ translationY, velocityY }) => {
80
+ const gestureValue = translationY + velocityY * 0.2;
81
+ const changeOffset = 0.2 * headerHeight.value;
82
+
83
+ if (gestureValue < -changeOffset && transformationY.value < 0) {
84
+ transformationY.value = withTiming(
85
+ -headerHeight.value,
86
+ withTimingConfig
87
+ );
88
+ currentYPosition.value = -headerHeight.value;
89
+ } else if (gestureValue > changeOffset) {
90
+ const destination = 0;
91
+ transformationY.value = withTiming(destination, withTimingConfig);
92
+ currentYPosition.value = destination;
93
+ } else {
94
+ transformationY.value = withTiming(
95
+ currentYPosition.value,
96
+ withTimingConfig
97
+ );
98
+ }
99
+ })
100
+ .onFinalize(() => {
101
+ movedY.value = 0;
102
+ })
103
+ .simultaneousWithExternalGesture(
104
+ ...Object.values(screenProperties).map(
105
+ ({ nativeGesture }) => nativeGesture
106
+ )
107
+ )
108
+ .withRef(gestureRef)
109
+ .enabled(_gestureEnabled),
110
+ [_gestureEnabled]
111
+ );
112
+
113
+ const style = useAnimatedStyle(
114
+ () => ({
115
+ flex: 1,
116
+ transform: [
117
+ {
118
+ translateY: interpolate(
119
+ transformationY.value,
120
+ [0, -headerHeight.value],
121
+ [0, -headerHeight.value],
122
+ 'clamp'
123
+ ),
124
+ },
125
+ ],
126
+ }),
127
+ []
128
+ );
129
+
130
+ return (
131
+ <GestureDetector gesture={gesture}>
132
+ <Reanimated.View style={style}>{children}</Reanimated.View>
133
+ </GestureDetector>
134
+ );
135
+ };
@@ -0,0 +1,27 @@
1
+ import { useRoute } from '@react-navigation/native';
2
+ import { type LayoutChangeEvent, type ViewProps } from 'react-native';
3
+ import Reanimated, { type AnimatedProps } from 'react-native-reanimated';
4
+ import { useTabContext } from '../../hooks/TopTab.hooks';
5
+
6
+ export interface ScreenWrapperProps
7
+ extends AnimatedProps<Omit<ViewProps, 'onLayout'>> {}
8
+
9
+ export const ScreenWrapper = ({
10
+ children,
11
+ ...viewProps
12
+ }: ScreenWrapperProps) => {
13
+ const { key } = useRoute();
14
+ const { screenProperties } = useTabContext();
15
+
16
+ const onLayout = (event: LayoutChangeEvent) => {
17
+ const screen = screenProperties[key];
18
+ if (!screen) throw 'No screen property registered for this route';
19
+ screen.outerLayout.value = event.nativeEvent.layout;
20
+ };
21
+
22
+ return (
23
+ <Reanimated.View {...viewProps} onLayout={onLayout}>
24
+ {children}
25
+ </Reanimated.View>
26
+ );
27
+ };
@@ -0,0 +1,59 @@
1
+ import {
2
+ type LayoutChangeEvent,
3
+ type ScrollViewProps,
4
+ StyleSheet,
5
+ } from 'react-native';
6
+ import { GestureDetector } from 'react-native-gesture-handler';
7
+ import Reanimated, { type AnimatedProps } from 'react-native-reanimated';
8
+ import {
9
+ useScreenGesture,
10
+ useScreenProperties,
11
+ useScreenScrollable,
12
+ useTabContext,
13
+ } from '../../hooks/TopTab.hooks';
14
+ import { useRef } from 'react';
15
+ import { useRoute } from '@react-navigation/native';
16
+
17
+ export interface ScreenWrapperProps
18
+ extends AnimatedProps<Omit<ScrollViewProps, 'onScroll'>> {}
19
+
20
+ export const ScrollView = ({
21
+ children,
22
+ style,
23
+ ...props
24
+ }: ScreenWrapperProps) => {
25
+ const gesture = useScreenGesture();
26
+ const { style: animatedStyle, ...scrollProps } = useScreenScrollable();
27
+ const { innerLayout } = useScreenProperties();
28
+ const { screenRefs } = useTabContext();
29
+
30
+ const { key } = useRoute();
31
+
32
+ const _ref = useRef<Reanimated.ScrollView>(null);
33
+
34
+ const onLayout = (event: LayoutChangeEvent) => {
35
+ innerLayout.value = event.nativeEvent.layout;
36
+ screenRefs.setRef(key, _ref);
37
+ };
38
+
39
+ return (
40
+ <GestureDetector gesture={gesture}>
41
+ <Reanimated.ScrollView
42
+ {...scrollProps}
43
+ {...props}
44
+ ref={_ref}
45
+ contentContainerStyle={_style.contentContainerStyle}
46
+ onLayout={onLayout}
47
+ style={[style, animatedStyle]}
48
+ >
49
+ {children}
50
+ </Reanimated.ScrollView>
51
+ </GestureDetector>
52
+ );
53
+ };
54
+
55
+ const _style = StyleSheet.create({
56
+ contentContainerStyle: {
57
+ flexGrow: 1,
58
+ },
59
+ });
@@ -0,0 +1,55 @@
1
+ import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
2
+ import type { ReanimatedTopTabNavigation } from 'react-navigation-reanimated-top-tabs';
3
+
4
+ const useResetApproachingScreenScrollOffset = ({
5
+ screenRefs,
6
+ transformationX,
7
+ screenProperties,
8
+ currentYPosition,
9
+ }: Pick<
10
+ ReanimatedTopTabNavigation.ContextType,
11
+ 'screenRefs' | 'screenProperties' | 'currentYPosition' | 'transformationX'
12
+ >) => {
13
+ const resetApproachingScreenOffset = (
14
+ index: number,
15
+ shouldReset: boolean
16
+ ) => {
17
+ if (shouldReset) {
18
+ //NOTE: use reanimated scrollTo does not work
19
+ const { scrollTo } = Object.values(screenRefs.refs.current)[index]
20
+ ?.current ?? { scrollTo: () => null };
21
+
22
+ scrollTo({ y: 0, animated: false });
23
+ }
24
+ };
25
+
26
+ useAnimatedReaction(
27
+ () => transformationX.value,
28
+ (current, previous) => {
29
+ const approachingIndex = getTargetIndex(current, previous);
30
+
31
+ const approachingScreenOffset =
32
+ Object.values(screenProperties).map(({ scrollY }) => scrollY.value)[
33
+ approachingIndex
34
+ ] ?? -1;
35
+
36
+ runOnJS(resetApproachingScreenOffset)(
37
+ approachingIndex,
38
+ approachingScreenOffset > 0 && currentYPosition.value === 0
39
+ );
40
+ }
41
+ );
42
+ };
43
+
44
+ function getTargetIndex(currentValue: number, previousValue: number | null) {
45
+ 'worklet';
46
+ if (currentValue > (previousValue ?? 0)) {
47
+ return Math.ceil(currentValue);
48
+ } else {
49
+ return Math.floor(currentValue);
50
+ }
51
+ }
52
+
53
+ export const TopTabContextHooks = {
54
+ useResetApproachingScreenScrollOffset,
55
+ };