Skip to content

Commit 276f258

Browse files
committed
force bottom navigation animation completion
1 parent ff0df54 commit 276f258

File tree

1 file changed

+60
-13
lines changed

1 file changed

+60
-13
lines changed

src/components/BottomNavigation/BottomNavigationBar.tsx

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ const BottomNavigationBar = <Route extends BaseRoute>({
380380
* Animation for the background color ripple, used to determine it's scale and opacity.
381381
*/
382382
const rippleAnim = useAnimatedValue(MIN_RIPPLE_SCALE);
383+
const animationInProgressRef = React.useRef(false);
384+
const targetIndexRef = React.useRef(navigationState.index);
385+
const isInitialMount = React.useRef(true);
383386

384387
/**
385388
* Layout of the navigation bar. The width is used to determine the size and position of the ripple.
@@ -412,6 +415,21 @@ const BottomNavigationBar = <Route extends BaseRoute>({
412415

413416
const animateToIndex = React.useCallback(
414417
(index: number) => {
418+
// If already animating to this index, do nothing
419+
if (animationInProgressRef.current && targetIndexRef.current === index) {
420+
return;
421+
}
422+
423+
// If animating to a different index, cancel current animation and restart
424+
// This prevents half-states when rapidly switching tabs
425+
if (animationInProgressRef.current) {
426+
rippleAnim.stopAnimation();
427+
indexAnim.stopAnimation();
428+
tabsAnims.forEach(tab => tab.stopAnimation());
429+
}
430+
431+
targetIndexRef.current = index;
432+
animationInProgressRef.current = true;
415433
// Reset the ripple to avoid glitch if it's currently animating
416434
rippleAnim.setValue(MIN_RIPPLE_SCALE);
417435

@@ -429,13 +447,40 @@ const BottomNavigationBar = <Route extends BaseRoute>({
429447
easing: animationEasing,
430448
})
431449
),
432-
]).start(() => {
433-
// Workaround a bug in native animations where this is reset after first animation
434-
tabsAnims.map((tab, i) => tab.setValue(i === index ? 1 : 0));
450+
]).start((result) => {
451+
animationInProgressRef.current = false;
435452

436-
// Update the index to change bar's background color and then hide the ripple
453+
if (targetIndexRef.current !== index) {
454+
return;
455+
}
456+
457+
// Explicitly finish the animation values to avoid RN 0.80 dropping the completion frame
458+
tabsAnims.forEach((tab, i) => {
459+
tab.stopAnimation();
460+
tab.setValue(i === index ? 1 : 0);
461+
});
462+
463+
indexAnim.stopAnimation();
437464
indexAnim.setValue(index);
465+
466+
rippleAnim.stopAnimation();
438467
rippleAnim.setValue(MIN_RIPPLE_SCALE);
468+
469+
// Safety fallback: if animation was interrupted, ensure completion on next frame
470+
if (result?.finished === false) {
471+
requestAnimationFrame(() => {
472+
if (targetIndexRef.current !== index) {
473+
return;
474+
}
475+
476+
tabsAnims.forEach((tab, i) => {
477+
tab.stopAnimation();
478+
tab.setValue(i === index ? 1 : 0);
479+
});
480+
indexAnim.setValue(index);
481+
rippleAnim.setValue(MIN_RIPPLE_SCALE);
482+
});
483+
}
439484
});
440485
},
441486
[
@@ -450,19 +495,18 @@ const BottomNavigationBar = <Route extends BaseRoute>({
450495
]
451496
);
452497

453-
React.useEffect(() => {
454-
// Workaround for native animated bug in react-native@^0.57
455-
// Context: https://github.com/callstack/react-native-paper/pull/637
456-
animateToIndex(navigationState.index);
457-
// eslint-disable-next-line react-hooks/exhaustive-deps
458-
}, []);
459-
460498
useIsKeyboardShown({
461499
onShow: handleKeyboardShow,
462500
onHide: handleKeyboardHide,
463501
});
464502

465503
React.useEffect(() => {
504+
if (isInitialMount.current) {
505+
// Skip animation on initial mount - values are already correctly initialized
506+
// Animating on mount causes RN 0.80+ native driver to render incomplete frames
507+
isInitialMount.current = false;
508+
return;
509+
}
466510
animateToIndex(navigationState.index);
467511
}, [navigationState.index, animateToIndex]);
468512

@@ -676,7 +720,10 @@ const BottomNavigationBar = <Route extends BaseRoute>({
676720
inputRange: [0, 1],
677721
outputRange: [0.5, 1],
678722
})
679-
: 0;
723+
: active.interpolate({
724+
inputRange: [0, 1],
725+
outputRange: [0, 1],
726+
});
680727

681728
const badge = getBadge({ route });
682729

@@ -740,7 +787,7 @@ const BottomNavigationBar = <Route extends BaseRoute>({
740787
},
741788
]}
742789
>
743-
{isV3 && focused && (
790+
{isV3 && (
744791
<Animated.View
745792
style={[
746793
styles.outline,

0 commit comments

Comments
 (0)