@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.
- package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js +47 -0
- package/lib/commonjs/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
- package/lib/commonjs/TransitionConfigs/TransitionPresets.js +19 -0
- package/lib/commonjs/TransitionConfigs/TransitionPresets.js.map +1 -0
- package/lib/commonjs/TransitionConfigs/TransitionSpecs.js +24 -0
- package/lib/commonjs/TransitionConfigs/TransitionSpecs.js.map +1 -0
- package/lib/commonjs/index.js +9 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/useAnimatedHashMap.js +31 -0
- package/lib/commonjs/utils/useAnimatedHashMap.js.map +1 -0
- package/lib/commonjs/views/BottomTabView.js +74 -4
- package/lib/commonjs/views/BottomTabView.js.map +1 -1
- package/lib/commonjs/views/ScreenFallback.js +6 -8
- package/lib/commonjs/views/ScreenFallback.js.map +1 -1
- package/lib/module/TransitionConfigs/SceneStyleInterpolators.js +40 -0
- package/lib/module/TransitionConfigs/SceneStyleInterpolators.js.map +1 -0
- package/lib/module/TransitionConfigs/TransitionPresets.js +11 -0
- package/lib/module/TransitionConfigs/TransitionPresets.js.map +1 -0
- package/lib/module/TransitionConfigs/TransitionSpecs.js +16 -0
- package/lib/module/TransitionConfigs/TransitionSpecs.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/useAnimatedHashMap.js +23 -0
- package/lib/module/utils/useAnimatedHashMap.js.map +1 -0
- package/lib/module/views/BottomTabView.js +75 -5
- package/lib/module/views/BottomTabView.js.map +1 -1
- package/lib/module/views/ScreenFallback.js +6 -8
- package/lib/module/views/ScreenFallback.js.map +1 -1
- package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts +10 -0
- package/lib/typescript/src/TransitionConfigs/SceneStyleInterpolators.d.ts.map +1 -0
- package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts +4 -0
- package/lib/typescript/src/TransitionConfigs/TransitionPresets.d.ts.map +1 -0
- package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts +4 -0
- package/lib/typescript/src/TransitionConfigs/TransitionSpecs.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts +0 -1
- package/lib/typescript/src/navigators/createBottomTabNavigator.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +50 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/useAnimatedHashMap.d.ts +4 -0
- package/lib/typescript/src/utils/useAnimatedHashMap.d.ts.map +1 -0
- package/lib/typescript/src/views/Badge.d.ts +0 -1
- package/lib/typescript/src/views/Badge.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabBar.d.ts +0 -1
- package/lib/typescript/src/views/BottomTabBar.d.ts.map +1 -1
- package/lib/typescript/src/views/BottomTabView.d.ts +0 -1
- package/lib/typescript/src/views/BottomTabView.d.ts.map +1 -1
- package/lib/typescript/src/views/ScreenFallback.d.ts +4 -4
- package/lib/typescript/src/views/ScreenFallback.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/TransitionConfigs/SceneStyleInterpolators.tsx +44 -0
- package/src/TransitionConfigs/TransitionPresets.tsx +13 -0
- package/src/TransitionConfigs/TransitionSpecs.tsx +19 -0
- package/src/index.tsx +9 -0
- package/src/types.tsx +70 -0
- package/src/utils/useAnimatedHashMap.tsx +25 -0
- package/src/views/BottomTabView.tsx +93 -6
- 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 {
|
|
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
|
-
|
|
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({
|
|
35
|
+
export function MaybeScreen({ enabled, active, ...rest }: ViewProps & Props) {
|
|
37
36
|
if (Screens?.screensEnabled?.()) {
|
|
38
37
|
return (
|
|
39
|
-
<Screens.Screen activityState={
|
|
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
|
}
|