diff --git a/navigation/tabs/index.js b/navigation/tabs/index.js index b26e9167..a886cf2c 100644 --- a/navigation/tabs/index.js +++ b/navigation/tabs/index.js @@ -2,7 +2,6 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { StyleSheet, TouchableOpacity, View } from 'react-native'; import Animated, { useAnimatedStyle, - useDerivedValue, useSharedValue, withTiming, } from 'react-native-reanimated'; @@ -20,7 +19,6 @@ const Tab = createBottomTabNavigator(); export const TAB_ITEM_HEIGHT = 60; const TAB_ITEM_WIDTH = 70; -const TAB_BORDER_WIDTH = 1; const OVERLAY_HEIGHT = 50; const ICON_SIZE = 26; @@ -43,24 +41,35 @@ function renderIcon(label, focused, color) { } } -// Each tab renders two stacked icon layers: a base layer in the inactive -// color that is ALWAYS visible, and an active layer (the focused color) whose -// opacity is driven by `progress` so it fades in exactly as the selection pill -// arrives under this tab. Because both the pill and this cross-fade read the -// same `progress` value, an icon can never end up the same color as whatever -// is behind it (which is what made icons "disappear" when the pill drifted off -// the focused tab). +// Each tab owns its OWN selection pill, rendered as a background layer inside +// this tab's subtree (declared before the icon). Because the icon is always +// drawn above its own pill within the same parent, the pill can never cover +// the icon — which is what made icons "disappear" on Android, where the old +// single global pill's sibling `zIndex` was unreliable and let it draw on top. +// +// Each tab fades its own pill/icon toward `focused ? 1 : 0` independently. Only +// the previously-focused and newly-focused tabs animate, so jumping between +// non-adjacent tabs no longer sweeps a value through the tabs in between (which +// used to flash their pill). Since the pill lives in the correct tab's subtree, +// a lagging update (after navigation.reset / resume / a blocked JS thread) can +// only mistime the fade — it can never hide the focused icon. function TabButton({ label, - index, - progress, + focused, activeColor, inactiveColor, + pillColor, showUnread, onPress, }) { - const activeIconStyle = useAnimatedStyle(() => ({ - opacity: Math.max(0, 1 - Math.abs(progress.value - index)), + const active = useSharedValue(focused ? 1 : 0); + + useEffect(() => { + active.value = withTiming(focused ? 1 : 0, { duration: 150 }); + }, [focused, active]); + + const selectionStyle = useAnimatedStyle(() => ({ + opacity: active.value, })); return ( @@ -69,12 +78,17 @@ function TabButton({ activeOpacity={0.7} style={styles.tabItemContainer} > + + {renderIcon(label, false, inactiveColor)} {renderIcon(label, true, activeColor)} @@ -96,27 +110,7 @@ function MyTabBar({ state, descriptors, navigation }) { const { bottomPadding } = useGlobalInsets(); const tabCount = state.routes.length || 1; - const selectedIndex = Math.min(state.index, tabCount - 1); const containerWidth = tabCount * TAB_ITEM_WIDTH; - const adjustedWidth = containerWidth - TAB_BORDER_WIDTH * 2; - const tabWidth = adjustedWidth / tabCount; - - // Single source of truth for the selection on the UI thread. `targetIndex` - // is kept in sync with the navigator's focused index, and `progress` - // continuously animates toward it. - const targetIndex = useSharedValue(selectedIndex); - - useEffect(() => { - targetIndex.value = selectedIndex; - }, [selectedIndex, targetIndex]); - - const progress = useDerivedValue(() => - withTiming(targetIndex.value, { duration: 150 }), - ); - - const overlayAnimatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateX: progress.value * tabWidth }], - })); const containerStyle = useMemo( () => ({ @@ -132,6 +126,9 @@ function MyTabBar({ state, descriptors, navigation }) { const inactiveColor = theme && darkModeType ? COLORS.darkModeText : COLORS.primary; + const pillColor = + theme && darkModeType ? COLORS.darkModeText : COLORS.primary; + return ( - {/* Animated selection overlay */} - - - - {state.routes.map((route, index) => { const { options } = descriptors[route.key]; @@ -199,10 +176,10 @@ function MyTabBar({ state, descriptors, navigation }) {