Skip to content

Commit f9e3834

Browse files
authored
chore: added tabBarVisible support for react-navigation 5 (#69)
* chore: added tabBarVisible support for react-navigation 5
1 parent 612ab50 commit f9e3834

File tree

3 files changed

+199
-25
lines changed

3 files changed

+199
-25
lines changed

src/AnimatedTabBar.tsx

Lines changed: 125 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import React, { useMemo, useCallback } from 'react';
1+
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
2+
import type { LayoutChangeEvent } from 'react-native';
23
import { useSafeArea } from 'react-native-safe-area-context';
34
import { AnimatedTabBarView } from './AnimatedTabBarView';
45
import { useStableCallback } from './utilities';
6+
import { useTabBarVisibility } from './hooks';
57
import type { PresetEnum } from './presets';
68
import type { AnimatedTabBarProps } from './types';
9+
import Animated, {
10+
interpolate,
11+
Extrapolate,
12+
useCode,
13+
cond,
14+
eq,
15+
call,
16+
onChange,
17+
} from 'react-native-reanimated';
18+
import { useValue } from 'react-native-redash';
719

820
interface Route {
921
name: string;
@@ -26,24 +38,11 @@ export function AnimatedTabBar<T extends PresetEnum>(
2638
...rest
2739
} = props;
2840

29-
//#region styles
30-
const { bottom: _safeBottomArea } = useSafeArea();
31-
const safeBottomArea = useMemo(
32-
() => overrideSafeAreaInsets?.bottom ?? _safeBottomArea,
33-
[overrideSafeAreaInsets, _safeBottomArea]
34-
);
35-
const style = useMemo(
36-
() => ({
37-
// @ts-ignore
38-
...overrideStyle,
39-
...{ paddingBottom: safeBottomArea },
40-
}),
41-
[overrideStyle, safeBottomArea]
42-
);
43-
//#endregion
44-
4541
//#region variables
42+
const tabBarContainerRef = useRef<Animated.View>(null);
4643
const isReactNavigation5 = useMemo(() => Boolean(state), [state]);
44+
const tabBarHeight = useValue<number>(0);
45+
4746
const CommonActions = useMemo(() => {
4847
if (isReactNavigation5) {
4948
const {
@@ -74,6 +73,21 @@ export function AnimatedTabBar<T extends PresetEnum>(
7473
}
7574
}, [state, navigation, isReactNavigation5]);
7675

76+
const shouldShowTabBar = useMemo(() => {
77+
/**
78+
* In React Navigation 4 the router view takes care of
79+
* hiding the tab bar.
80+
*/
81+
if (!isReactNavigation5) {
82+
return true;
83+
}
84+
const route = routes[navigationIndex];
85+
const { options } = descriptors[route.key];
86+
return options.tabBarVisible ?? true;
87+
}, [isReactNavigation5, routes, descriptors, navigationIndex]);
88+
89+
const shouldShowTabBarAnimated = useTabBarVisibility(shouldShowTabBar);
90+
7791
const getRouteTitle = useCallback(
7892
(route: Route) => {
7993
if (isReactNavigation5) {
@@ -113,6 +127,40 @@ export function AnimatedTabBar<T extends PresetEnum>(
113127
}, [routes, getRouteTitle, getRouteTabConfigs]) as any;
114128
//#endregion
115129

130+
//#region styles
131+
const { bottom: _safeBottomArea } = useSafeArea();
132+
const safeBottomArea = useMemo(
133+
() => overrideSafeAreaInsets?.bottom ?? _safeBottomArea,
134+
[overrideSafeAreaInsets, _safeBottomArea]
135+
);
136+
const style = useMemo(
137+
() => ({
138+
// @ts-ignore
139+
...overrideStyle,
140+
paddingBottom: safeBottomArea,
141+
}),
142+
[overrideStyle, safeBottomArea]
143+
);
144+
const containerStyle = useMemo(
145+
() => ({
146+
bottom: 0,
147+
left: 0,
148+
right: 0,
149+
transform: [
150+
{
151+
translateY: interpolate(shouldShowTabBarAnimated, {
152+
inputRange: [0, 1],
153+
outputRange: [tabBarHeight, 0],
154+
extrapolate: Extrapolate.CLAMP,
155+
}),
156+
},
157+
],
158+
}),
159+
// eslint-disable-next-line react-hooks/exhaustive-deps
160+
[]
161+
);
162+
//#endregion
163+
116164
//#region callbacks
117165
const handleIndexChange = useStableCallback((index: number) => {
118166
if (isReactNavigation5) {
@@ -146,17 +194,69 @@ export function AnimatedTabBar<T extends PresetEnum>(
146194
onTabLongPress({ route: routes[index] });
147195
}
148196
});
197+
const handleLayout = useCallback(
198+
({
199+
nativeEvent: {
200+
layout: { height },
201+
},
202+
}: LayoutChangeEvent) => {
203+
tabBarHeight.setValue(height);
204+
},
205+
// eslint-disable-next-line react-hooks/exhaustive-deps
206+
[]
207+
);
208+
//#endregion
209+
210+
//#region effects
211+
useCode(
212+
() =>
213+
onChange(
214+
shouldShowTabBarAnimated,
215+
cond(
216+
eq(shouldShowTabBarAnimated, 1),
217+
call([], () => {
218+
if (tabBarContainerRef.current) {
219+
// @ts-ignore
220+
tabBarContainerRef.current.setNativeProps({
221+
style: {
222+
position: 'relative',
223+
},
224+
});
225+
}
226+
})
227+
)
228+
),
229+
[]
230+
);
231+
useEffect(() => {
232+
if (!shouldShowTabBar) {
233+
if (tabBarContainerRef.current) {
234+
// @ts-ignore
235+
tabBarContainerRef.current.setNativeProps({
236+
style: {
237+
position: 'absolute',
238+
},
239+
});
240+
}
241+
}
242+
}, [shouldShowTabBar]);
149243
//#endregion
150244

151245
// render
152246
return (
153-
<AnimatedTabBarView
154-
index={navigationIndex}
155-
onIndexChange={handleIndexChange}
156-
onLongPress={handleLongPress}
157-
tabs={routesWithTabConfig}
158-
style={style}
159-
{...rest}
160-
/>
247+
<Animated.View
248+
ref={tabBarContainerRef}
249+
style={containerStyle}
250+
onLayout={handleLayout}
251+
>
252+
<AnimatedTabBarView
253+
index={navigationIndex}
254+
onIndexChange={handleIndexChange}
255+
onLongPress={handleLongPress}
256+
tabs={routesWithTabConfig}
257+
style={style}
258+
{...rest}
259+
/>
260+
</Animated.View>
161261
);
162262
}

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useTabBarVisibility } from './useTabBarVisibility';

src/hooks/useTabBarVisibility.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { useMemo, useEffect } from 'react';
2+
import Animated, {
3+
block,
4+
onChange,
5+
stopClock,
6+
set,
7+
cond,
8+
and,
9+
not,
10+
clockRunning,
11+
startClock,
12+
timing,
13+
Easing,
14+
} from 'react-native-reanimated';
15+
import { useClock, useValue } from 'react-native-redash';
16+
17+
export const useTabBarVisibility = (shouldShowTabBar: boolean) => {
18+
const _shouldShowTabBar = useValue(shouldShowTabBar ? 1 : 0);
19+
const clock = useClock();
20+
const shouldAnimate = useValue(0);
21+
const state = useMemo(
22+
() => ({
23+
finished: new Animated.Value(0),
24+
frameTime: new Animated.Value(0),
25+
position: new Animated.Value(shouldShowTabBar ? 1 : 0),
26+
time: new Animated.Value(0),
27+
}),
28+
// eslint-disable-next-line react-hooks/exhaustive-deps
29+
[]
30+
);
31+
const config = useMemo(
32+
() => ({
33+
toValue: new Animated.Value(0),
34+
easing: Easing.linear,
35+
duration: 250,
36+
}),
37+
[]
38+
);
39+
40+
const finishTiming = useMemo(
41+
() => [
42+
stopClock(clock),
43+
set(state.finished, 0),
44+
set(state.frameTime, 0),
45+
set(state.time, 0),
46+
],
47+
[clock, state]
48+
);
49+
50+
// effects
51+
useEffect(() => {
52+
_shouldShowTabBar.setValue(shouldShowTabBar ? 1 : 0);
53+
// eslint-disable-next-line react-hooks/exhaustive-deps
54+
}, [shouldShowTabBar]);
55+
56+
return block([
57+
onChange(_shouldShowTabBar, [finishTiming, set(shouldAnimate, 1)]),
58+
59+
cond(shouldAnimate, [
60+
cond(and(not(clockRunning(clock)), not(state.finished)), [
61+
set(state.frameTime, 0),
62+
set(state.time, 0),
63+
set(state.finished, 0),
64+
set(config.toValue, _shouldShowTabBar),
65+
startClock(clock),
66+
]),
67+
timing(clock, state, config),
68+
cond(state.finished, [finishTiming, set(shouldAnimate, 0)]),
69+
]),
70+
71+
state.position,
72+
]);
73+
};

0 commit comments

Comments
 (0)