@react-navigation/bottom-tabs 7.0.0-alpha.1 → 7.0.0-alpha.3

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 (59) hide show
  1. package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js +47 -0
  2. package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
  3. package/lib/commonjs/TransitionConfigs/TransitionPresets.js +19 -0
  4. package/lib/commonjs/TransitionConfigs/TransitionPresets.js.map +1 -0
  5. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js +24 -0
  6. package/lib/commonjs/TransitionConfigs/TransitionSpecs.js.map +1 -0
  7. package/lib/commonjs/index.js +9 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/utils/useAnimatedHashMap.js +31 -0
  10. package/lib/commonjs/utils/useAnimatedHashMap.js.map +1 -0
  11. package/lib/commonjs/views/BottomTabView.js +74 -4
  12. package/lib/commonjs/views/BottomTabView.js.map +1 -1
  13. package/lib/commonjs/views/ScreenFallback.js +6 -8
  14. package/lib/commonjs/views/ScreenFallback.js.map +1 -1
  15. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js +40 -0
  16. package/lib/module/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
  17. package/lib/module/TransitionConfigs/TransitionPresets.js +11 -0
  18. package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -0
  19. package/lib/module/TransitionConfigs/TransitionSpecs.js +16 -0
  20. package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -0
  21. package/lib/module/index.js +9 -0
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/utils/useAnimatedHashMap.js +23 -0
  24. package/lib/module/utils/useAnimatedHashMap.js.map +1 -0
  25. package/lib/module/views/BottomTabView.js +75 -5
  26. package/lib/module/views/BottomTabView.js.map +1 -1
  27. package/lib/module/views/ScreenFallback.js +6 -8
  28. package/lib/module/views/ScreenFallback.js.map +1 -1
  29. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +10 -0
  30. package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -0
  31. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +4 -0
  32. package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -0
  33. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +4 -0
  34. package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -0
  35. package/lib/typescript/src/index.d.ts +7 -0
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +0 -1
  38. package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
  39. package/lib/typescript/src/types.d.ts +50 -0
  40. package/lib/typescript/src/types.d.ts.map +1 -1
  41. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +4 -0
  42. package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -0
  43. package/lib/typescript/src/views/Badge.d.ts +0 -1
  44. package/lib/typescript/src/views/Badge.d.ts.map +1 -1
  45. package/lib/typescript/src/views/BottomTabBar.d.ts +0 -1
  46. package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
  47. package/lib/typescript/src/views/BottomTabView.d.ts +0 -1
  48. package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
  49. package/lib/typescript/src/views/ScreenFallback.d.ts +4 -4
  50. package/lib/typescript/src/views/ScreenFallback.d.ts.map +1 -1
  51. package/package.json +10 -10
  52. package/src/TransitionConfigs/SceneStyleInterpolators.tsx +44 -0
  53. package/src/TransitionConfigs/TransitionPresets.tsx +13 -0
  54. package/src/TransitionConfigs/TransitionSpecs.tsx +19 -0
  55. package/src/index.tsx +9 -0
  56. package/src/types.tsx +70 -0
  57. package/src/utils/useAnimatedHashMap.tsx +25 -0
  58. package/src/views/BottomTabView.tsx +93 -6
  59. package/src/views/ScreenFallback.tsx +6 -13
package/src/types.tsx CHANGED
@@ -254,6 +254,22 @@ export type BottomTabNavigationOptions = HeaderOptions & {
254
254
  * Only supported on iOS and Android.
255
255
  */
256
256
  freezeOnBlur?: boolean;
257
+
258
+ /**
259
+ * Whether transition animations should be enabled when switching tabs.
260
+ * Defaults to `false`.
261
+ */
262
+ animationEnabled?: boolean;
263
+
264
+ /**
265
+ * Function which specifies interpolated styles for bottom-tab scenes.
266
+ */
267
+ sceneStyleInterpolator?: BottomTabSceneStyleInterpolator;
268
+
269
+ /**
270
+ * Object which specifies the animation type (timing or spring) and their options (such as duration for timing).
271
+ */
272
+ transitionSpec?: TransitionSpec;
257
273
  };
258
274
 
259
275
  export type BottomTabDescriptor = Descriptor<
@@ -264,6 +280,60 @@ export type BottomTabDescriptor = Descriptor<
264
280
 
265
281
  export type BottomTabDescriptorMap = Record<string, BottomTabDescriptor>;
266
282
 
283
+ export type BottomTabSceneInterpolationProps = {
284
+ /**
285
+ * Animated value for the current screen:
286
+ * - -1 if the index is lower than active tab,
287
+ * - 0 if they're active,
288
+ * - 1 if the index is higher than active tab
289
+ */
290
+ current: Animated.Value;
291
+ };
292
+
293
+ export type BottomTabSceneInterpolatedStyle = {
294
+ /**
295
+ * Interpolated style for the view representing the scene containing screen content.
296
+ */
297
+ sceneStyle: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
298
+ };
299
+
300
+ export type BottomTabSceneStyleInterpolator = (
301
+ props: BottomTabSceneInterpolationProps
302
+ ) => BottomTabSceneInterpolatedStyle;
303
+
304
+ export type TransitionSpec =
305
+ | {
306
+ animation: 'timing';
307
+ config: Omit<
308
+ Animated.TimingAnimationConfig,
309
+ 'toValue' | keyof Animated.AnimationConfig
310
+ >;
311
+ }
312
+ | {
313
+ animation: 'spring';
314
+ config: Omit<
315
+ Animated.SpringAnimationConfig,
316
+ 'toValue' | keyof Animated.AnimationConfig
317
+ >;
318
+ };
319
+
320
+ export type BottomTabTransitionPreset = {
321
+ /**
322
+ * Whether transition animations should be enabled when switching tabs.
323
+ */
324
+ animationEnabled?: boolean;
325
+
326
+ /**
327
+ * Function which specifies interpolated styles for bottom-tab scenes.
328
+ */
329
+ sceneStyleInterpolator?: BottomTabSceneStyleInterpolator;
330
+
331
+ /**
332
+ * Object which specifies the animation type (timing or spring) and their options (such as duration for timing).
333
+ */
334
+ transitionSpec?: TransitionSpec;
335
+ };
336
+
267
337
  export type BottomTabNavigationConfig = {
268
338
  /**
269
339
  * Function that returns a React element to display as the tab bar.
@@ -0,0 +1,25 @@
1
+ import type { NavigationState } from '@react-navigation/routers';
2
+ import * as React from 'react';
3
+ import { Animated } from 'react-native';
4
+
5
+ export function useAnimatedHashMap({ routes, index }: NavigationState) {
6
+ const refs = React.useRef<Record<string, Animated.Value>>({});
7
+ const previous = refs.current;
8
+ const routeKeys = Object.keys(previous);
9
+
10
+ if (
11
+ routes.length === routeKeys.length &&
12
+ routes.every((route) => routeKeys.includes(route.key))
13
+ ) {
14
+ return previous;
15
+ }
16
+ refs.current = {};
17
+
18
+ routes.forEach(({ key }, i) => {
19
+ refs.current[key] =
20
+ previous[key] ??
21
+ new Animated.Value(i === index ? 0 : i >= index ? 1 : -1);
22
+ });
23
+
24
+ return refs.current;
25
+ }
@@ -9,7 +9,7 @@ import type {
9
9
  TabNavigationState,
10
10
  } from '@react-navigation/native';
11
11
  import * as React from 'react';
12
- import { Platform, StyleSheet } from 'react-native';
12
+ import { Animated, Platform, StyleSheet } from 'react-native';
13
13
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
14
14
 
15
15
  import type {
@@ -18,12 +18,15 @@ import type {
18
18
  BottomTabHeaderProps,
19
19
  BottomTabNavigationConfig,
20
20
  BottomTabNavigationHelpers,
21
+ BottomTabNavigationOptions,
21
22
  BottomTabNavigationProp,
22
23
  } from '../types';
23
24
  import { BottomTabBarHeightCallbackContext } from '../utils/BottomTabBarHeightCallbackContext';
24
25
  import { BottomTabBarHeightContext } from '../utils/BottomTabBarHeightContext';
26
+ import { useAnimatedHashMap } from '../utils/useAnimatedHashMap';
25
27
  import { BottomTabBar, getTabBarHeight } from './BottomTabBar';
26
28
  import { MaybeScreen, MaybeScreenContainer } from './ScreenFallback';
29
+ import CompositeAnimation = Animated.CompositeAnimation;
27
30
 
28
31
  type Props = BottomTabNavigationConfig & {
29
32
  state: TabNavigationState<ParamListBase>;
@@ -31,6 +34,21 @@ type Props = BottomTabNavigationConfig & {
31
34
  descriptors: BottomTabDescriptorMap;
32
35
  };
33
36
 
37
+ const EPSILON = 1e-5;
38
+ const STATE_INACTIVE = 0;
39
+ const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
40
+ const STATE_ON_TOP = 2;
41
+
42
+ const hasAnimation = (options: BottomTabNavigationOptions) => {
43
+ const { animationEnabled, transitionSpec } = options;
44
+
45
+ if (animationEnabled === false || !transitionSpec) {
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ };
51
+
34
52
  export function BottomTabView(props: Props) {
35
53
  const {
36
54
  tabBar = (props: BottomTabBarProps) => <BottomTabBar {...props} />,
@@ -43,14 +61,54 @@ export function BottomTabView(props: Props) {
43
61
  Platform.OS === 'ios',
44
62
  sceneContainerStyle,
45
63
  } = props;
46
-
47
64
  const focusedRouteKey = state.routes[state.index].key;
65
+
66
+ /**
67
+ * List of loaded tabs, tabs will be loaded when navigated to.
68
+ */
48
69
  const [loaded, setLoaded] = React.useState([focusedRouteKey]);
49
70
 
50
71
  if (!loaded.includes(focusedRouteKey)) {
72
+ // Set the current tab to be loaded if it was not loaded before
51
73
  setLoaded([...loaded, focusedRouteKey]);
52
74
  }
53
75
 
76
+ const tabAnims = useAnimatedHashMap(state);
77
+
78
+ React.useEffect(() => {
79
+ const animateToIndex = () => {
80
+ Animated.parallel(
81
+ state.routes
82
+ .map((route, index) => {
83
+ const { options } = descriptors[route.key];
84
+ const { transitionSpec } = options;
85
+
86
+ const animationEnabled = hasAnimation(options);
87
+
88
+ const toValue =
89
+ index === state.index ? 0 : index >= state.index ? 1 : -1;
90
+
91
+ if (!animationEnabled || !transitionSpec) {
92
+ return Animated.timing(tabAnims[route.key], {
93
+ toValue,
94
+ duration: 0,
95
+ useNativeDriver: true,
96
+ });
97
+ }
98
+
99
+ return Animated[transitionSpec.animation](tabAnims[route.key], {
100
+ ...transitionSpec.config,
101
+ toValue,
102
+ useNativeDriver: true,
103
+ });
104
+ })
105
+ .filter(Boolean) as CompositeAnimation[]
106
+ ).start();
107
+ };
108
+
109
+ animateToIndex();
110
+ }, [descriptors, state.index, state.routes, tabAnims]);
111
+
54
112
  const dimensions = SafeAreaProviderCompat.initialMetrics.frame;
55
113
  const [tabBarHeight, setTabBarHeight] = React.useState(() =>
56
114
  getTabBarHeight({
@@ -88,16 +146,25 @@ export function BottomTabView(props: Props) {
88
146
 
89
147
  const { routes } = state;
90
148
 
149
+ // If there is no animation, we only have 2 states: visible and invisible
150
+ const hasTwoStates = !routes.some((route) =>
151
+ hasAnimation(descriptors[route.key].options)
152
+ );
153
+
91
154
  return (
92
155
  <SafeAreaProviderCompat>
93
156
  <MaybeScreenContainer
94
157
  enabled={detachInactiveScreens}
95
- hasTwoStates
158
+ hasTwoStates={hasTwoStates}
96
159
  style={styles.container}
97
160
  >
98
161
  {routes.map((route, index) => {
99
162
  const descriptor = descriptors[route.key];
100
- const { lazy = true, unmountOnBlur } = descriptor.options;
163
+ const {
164
+ lazy = true,
165
+ unmountOnBlur,
166
+ sceneStyleInterpolator,
167
+ } = descriptor.options;
101
168
  const isFocused = state.index === index;
102
169
 
103
170
  if (unmountOnBlur && !isFocused) {
@@ -123,11 +190,31 @@ export function BottomTabView(props: Props) {
123
190
  headerTransparent,
124
191
  } = descriptor.options;
125
192
 
193
+ const { sceneStyle } =
194
+ sceneStyleInterpolator?.({
195
+ current: tabAnims[route.key],
196
+ }) ?? {};
197
+
198
+ const animationEnabled = hasAnimation(descriptor.options);
199
+ const activityState = isFocused
200
+ ? STATE_ON_TOP // the screen is on top after the transition
201
+ : animationEnabled // is animation is not enabled, immediately move to inactive state
202
+ ? tabAnims[route.key].interpolate({
203
+ inputRange: [0, 1 - EPSILON, 1],
204
+ outputRange: [
205
+ STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition
206
+ STATE_TRANSITIONING_OR_BELOW_TOP,
207
+ STATE_INACTIVE, // the screen is detached after transition
208
+ ],
209
+ extrapolate: 'extend',
210
+ })
211
+ : STATE_INACTIVE;
212
+
126
213
  return (
127
214
  <MaybeScreen
128
215
  key={route.key}
129
216
  style={[StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }]}
130
- visible={isFocused}
217
+ active={activityState}
131
218
  enabled={detachInactiveScreens}
132
219
  freezeOnBlur={freezeOnBlur}
133
220
  >
@@ -146,7 +233,7 @@ export function BottomTabView(props: Props) {
146
233
  descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
147
234
  options: descriptor.options,
148
235
  })}
149
- style={sceneContainerStyle}
236
+ style={[sceneContainerStyle, animationEnabled && sceneStyle]}
150
237
  >
151
238
  {descriptor.render()}
152
239
  </Screen>
@@ -1,11 +1,10 @@
1
- import { ResourceSavingView } from '@react-navigation/elements';
2
1
  import * as React from 'react';
3
- import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
2
+ import { Animated, StyleProp, View, ViewProps, ViewStyle } from 'react-native';
4
3
 
5
4
  type Props = {
6
- visible: boolean;
7
- children: React.ReactNode;
8
5
  enabled: boolean;
6
+ active: 0 | 1 | 2 | Animated.AnimatedInterpolation<0 | 1>;
7
+ children: React.ReactNode;
9
8
  freezeOnBlur?: boolean;
10
9
  style?: StyleProp<ViewStyle>;
11
10
  };
@@ -33,18 +32,12 @@ export const MaybeScreenContainer = ({
33
32
  return <View {...rest} />;
34
33
  };
35
34
 
36
- export function MaybeScreen({ visible, children, ...rest }: Props) {
35
+ export function MaybeScreen({ enabled, active, ...rest }: ViewProps & Props) {
37
36
  if (Screens?.screensEnabled?.()) {
38
37
  return (
39
- <Screens.Screen activityState={visible ? 2 : 0} {...rest}>
40
- {children}
41
- </Screens.Screen>
38
+ <Screens.Screen enabled={enabled} activityState={active} {...rest} />
42
39
  );
43
40
  }
44
41
 
45
- return (
46
- <ResourceSavingView visible={visible} {...rest}>
47
- {children}
48
- </ResourceSavingView>
49
- );
42
+ return <View {...rest} />;
50
43
  }