This commit is contained in:
Boof2015
2026-07-13 14:19:36 -04:00
parent 8880d9e411
commit 5b39e5d0c0
3 changed files with 31 additions and 14 deletions
+6 -11
View File
@@ -1,14 +1,12 @@
import { useMemo, useRef } from 'react';
import { Tabs } from 'expo-router';
// RN's Easing (not reanimated): the bottom-tabs scene transition runs on legacy
// Animated.timing and may use the native driver, so the easing must be serializable.
import { Easing } from 'react-native';
import { TabBar, type TabItem } from '@/components/TabBar';
import {
TAB_TRANSITION_SETTLE_MS,
TAB_TRANSITION_SPEC,
} from '@/navigation/tabTransition';
import { useColors } from '@/theme/themed';
const TAB_TRANSITION_MS = 160;
const TAB_EASING = Easing.out(Easing.cubic);
export default function TabsLayout() {
const colors = useColors();
const lastSwitchAt = useRef(0);
@@ -22,10 +20,7 @@ export default function TabsLayout() {
sceneStyle: { backgroundColor: colors.bgPrimary },
// Directional slide + cross-fade between tabs, following tab order.
animation: 'shift' as const,
transitionSpec: {
animation: 'timing' as const,
config: { duration: TAB_TRANSITION_MS, easing: TAB_EASING },
},
transitionSpec: TAB_TRANSITION_SPEC,
}),
[colors.bgPrimary]
);
@@ -45,7 +40,7 @@ export default function TabsLayout() {
// completion frame and leave the incoming scene invisible; swallow
// taps until the current transition has finished.
const now = Date.now();
if (now - lastSwitchAt.current < TAB_TRANSITION_MS + 30) return;
if (now - lastSwitchAt.current < TAB_TRANSITION_SETTLE_MS + 30) return;
const event = navigation.emit({
type: 'tabPress',
target: item.key,
+5 -3
View File
@@ -22,6 +22,7 @@ import {
import { createThemedStyles, useColors } from '@/theme/themed';
import { useRipple } from '@/theme/ripple';
import { motion } from '@/theme/motion';
import { TAB_TRANSITION_SETTLE_MS } from '@/navigation/tabTransition';
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
import { usePlayerStore } from '@/stores/playerStore';
@@ -30,8 +31,6 @@ import { playHaptic } from '@/lib/haptics';
type IconName = keyof typeof Ionicons.glyphMap;
type MiniPlayerPhase = 'hidden' | 'reserved' | 'visible';
const TAB_TRANSITION_MS = 160;
export const TAB_META: Record<string, { label: string; icon: IconName }> = {
index: { label: 'Home', icon: 'home' },
library: { label: 'Library', icon: 'musical-notes' },
@@ -85,7 +84,10 @@ export function TabBar({ items, onPress }: TabBarProps) {
return;
}
const timer = setTimeout(() => setSettledHomeFocused(homeFocused), TAB_TRANSITION_MS);
const timer = setTimeout(
() => setSettledHomeFocused(homeFocused),
TAB_TRANSITION_SETTLE_MS
);
return () => clearTimeout(timer);
}, [homeFocused, settledHomeFocused]);
+20
View File
@@ -0,0 +1,20 @@
/**
* Bottom tabs use React Native's legacy native Animated driver. On Android,
* timing animations are pre-sampled at 60 fps, so their positions repeat on a
* 120 Hz display. A critically damped native spring is evaluated from each
* display frame instead while preserving the current ~160 ms ease-out feel.
*/
export const TAB_TRANSITION_SETTLE_MS = 160;
export const TAB_TRANSITION_SPEC = {
animation: 'spring',
config: {
stiffness: 2500,
damping: 100,
mass: 1,
overshootClamping: true,
restDisplacementThreshold: 0.004,
restSpeedThreshold: 0.15,
},
} as const;