Fix stuck bottom bar on Home and use the Reanimated 4 spring curve
The header and bottom bar were being handed the same `withSpring()` animation object. Animation objects carry their own state, so the second shared value to receive it could stop partway, leaving the bar stuck half hidden after a slow scroll on Home. Each shared value now gets its own animation. Shell show/hide springs now share one config based on Reanimated 4's default critically damped curve, with the perceptual duration shortened from 550ms to 400ms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWQciPDiipBhqiW922Nivs
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
import {type WithSpringConfig} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring used for showing and hiding shell UI (header, bottom bar, and things
|
||||||
|
* attached to them). This is Reanimated 4's default critically damped curve,
|
||||||
|
* whose perceptual duration is 550ms, sped up a little.
|
||||||
|
*/
|
||||||
|
export const SHELL_SPRING_CONFIG = {
|
||||||
|
duration: 400,
|
||||||
|
dampingRatio: 1,
|
||||||
|
} as const satisfies WithSpringConfig
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import {useCallback, useState} from 'react'
|
import {useCallback, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
clamp,
|
clamp,
|
||||||
Reanimated3DefaultSpringConfig,
|
|
||||||
type SharedValue,
|
type SharedValue,
|
||||||
useDerivedValue,
|
useDerivedValue,
|
||||||
withSpring,
|
withSpring,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {scheduleOnRN} from 'react-native-worklets'
|
import {scheduleOnRN} from 'react-native-worklets'
|
||||||
|
|
||||||
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
|
|
||||||
type Contribution = {
|
type Contribution = {
|
||||||
id: number
|
id: number
|
||||||
value: SharedValue<number>
|
value: SharedValue<number>
|
||||||
@@ -52,15 +53,11 @@ export function useContributionRegistry(base?: SharedValue<number>) {
|
|||||||
setContributions(prev => [...prev, {id, value}])
|
setContributions(prev => [...prev, {id, value}])
|
||||||
return () => {
|
return () => {
|
||||||
value.set(
|
value.set(
|
||||||
withSpring(
|
withSpring(0, SHELL_SPRING_CONFIG, finished => {
|
||||||
0,
|
if (finished) {
|
||||||
{...Reanimated3DefaultSpringConfig, overshootClamping: true},
|
scheduleOnRN(remove, id)
|
||||||
finished => {
|
}
|
||||||
if (finished) {
|
}),
|
||||||
scheduleOnRN(remove, id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useContext, useEffect} from 'react'
|
import {useContext, useEffect} from 'react'
|
||||||
import {
|
import {
|
||||||
Reanimated3DefaultSpringConfig,
|
|
||||||
type SharedValue,
|
type SharedValue,
|
||||||
useDerivedValue,
|
useDerivedValue,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
type ParamListBase,
|
type ParamListBase,
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
import {ScreenPresenceContext} from './context'
|
import {ScreenPresenceContext} from './context'
|
||||||
|
|
||||||
export {type ScreenPresence, useScreenPresence} from './context'
|
export {type ScreenPresence, useScreenPresence} from './context'
|
||||||
@@ -117,10 +117,10 @@ function useAncestorsFocused(navigation: NavigationProp<ParamListBase>) {
|
|||||||
if (ancestors.length === 0) return
|
if (ancestors.length === 0) return
|
||||||
const update = () => {
|
const update = () => {
|
||||||
focused.set(
|
focused.set(
|
||||||
withSpring(areAncestorsFocused(navigation) ? 1 : 0, {
|
withSpring(
|
||||||
...Reanimated3DefaultSpringConfig,
|
areAncestorsFocused(navigation) ? 1 : 0,
|
||||||
overshootClamping: true,
|
SHELL_SPRING_CONFIG,
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {
|
import {useSharedValue, withSpring} from 'react-native-reanimated'
|
||||||
Reanimated3DefaultSpringConfig,
|
|
||||||
useSharedValue,
|
|
||||||
withSpring,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
import {ScreenPresenceContext} from './context'
|
import {ScreenPresenceContext} from './context'
|
||||||
|
|
||||||
export {type ScreenPresence, useScreenPresence} from './context'
|
export {type ScreenPresence, useScreenPresence} from './context'
|
||||||
@@ -35,13 +32,8 @@ export function ScreenPresenceProvider({
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const spring = (to: number) =>
|
presence.set(withSpring(1, SHELL_SPRING_CONFIG))
|
||||||
withSpring(to, {
|
return () => presence.set(withSpring(0, SHELL_SPRING_CONFIG))
|
||||||
...Reanimated3DefaultSpringConfig,
|
|
||||||
overshootClamping: true,
|
|
||||||
})
|
|
||||||
presence.set(spring(1))
|
|
||||||
return () => presence.set(spring(0))
|
|
||||||
}, [presence]),
|
}, [presence]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {createContext, useContext, useEffect, useMemo} from 'react'
|
import {createContext, useContext, useEffect, useMemo} from 'react'
|
||||||
import {
|
import {
|
||||||
type DerivedValue,
|
type DerivedValue,
|
||||||
Reanimated3DefaultSpringConfig,
|
|
||||||
type SharedValue,
|
type SharedValue,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withSpring,
|
withSpring,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
import {useContributionRegistry} from '#/lib/hooks/useContributionRegistry'
|
import {useContributionRegistry} from '#/lib/hooks/useContributionRegistry'
|
||||||
import {useScreenPresence} from '#/lib/hooks/useScreenPresence'
|
import {useScreenPresence} from '#/lib/hooks/useScreenPresence'
|
||||||
|
|
||||||
@@ -84,12 +84,7 @@ export function useEnableMinimalShellMode({enabled} = {enabled: true}) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
const unregister = register(contribution)
|
const unregister = register(contribution)
|
||||||
contribution.set(
|
contribution.set(withSpring(1, SHELL_SPRING_CONFIG))
|
||||||
withSpring(1, {
|
|
||||||
...Reanimated3DefaultSpringConfig,
|
|
||||||
overshootClamping: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return unregister
|
return unregister
|
||||||
}, [enabled, register, contribution])
|
}, [enabled, register, contribution])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {type NativeScrollEvent} from 'react-native'
|
|||||||
import {
|
import {
|
||||||
clamp,
|
clamp,
|
||||||
interpolate,
|
interpolate,
|
||||||
Reanimated3DefaultSpringConfig,
|
|
||||||
type SharedValue,
|
type SharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {EventEmitter} from 'eventemitter3'
|
import {EventEmitter} from 'eventemitter3'
|
||||||
|
|
||||||
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {useMinimalShellScrollMode} from '#/state/shell/minimal-mode'
|
import {useMinimalShellScrollMode} from '#/state/shell/minimal-mode'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
@@ -106,12 +106,9 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
|||||||
const setMode = useCallback(
|
const setMode = useCallback(
|
||||||
(v: boolean) => {
|
(v: boolean) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const target = withSpring(v ? 1 : 0, {
|
// animation objects are stateful, so each shared value needs its own
|
||||||
...Reanimated3DefaultSpringConfig,
|
headerMode.set(withSpring(v ? 1 : 0, SHELL_SPRING_CONFIG))
|
||||||
overshootClamping: true,
|
footerScrollMode.set(withSpring(v ? 1 : 0, SHELL_SPRING_CONFIG))
|
||||||
})
|
|
||||||
headerMode.set(target)
|
|
||||||
footerScrollMode.set(target)
|
|
||||||
},
|
},
|
||||||
[headerMode, footerScrollMode],
|
[headerMode, footerScrollMode],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'
|
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'
|
||||||
import {ActivityIndicator, AppState, StyleSheet} from 'react-native'
|
import {ActivityIndicator, AppState, StyleSheet} from 'react-native'
|
||||||
import {
|
import {withSpring} from 'react-native-reanimated'
|
||||||
Reanimated3DefaultSpringConfig,
|
|
||||||
withSpring,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
@@ -12,6 +9,7 @@ import {
|
|||||||
PROD_DEFAULT_FEED,
|
PROD_DEFAULT_FEED,
|
||||||
TIMELINE_SAVED_FEED,
|
TIMELINE_SAVED_FEED,
|
||||||
} from '#/lib/constants'
|
} from '#/lib/constants'
|
||||||
|
import {SHELL_SPRING_CONFIG} from '#/lib/custom-animations/springs'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates'
|
import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates'
|
||||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||||
@@ -160,12 +158,9 @@ function HomeScreenReady({
|
|||||||
const footerScrollMode = useMinimalShellScrollMode()
|
const footerScrollMode = useMinimalShellScrollMode()
|
||||||
const showShell = useCallback(() => {
|
const showShell = useCallback(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const shown = withSpring(0, {
|
// animation objects are stateful, so each shared value needs its own
|
||||||
...Reanimated3DefaultSpringConfig,
|
headerMode.set(withSpring(0, SHELL_SPRING_CONFIG))
|
||||||
overshootClamping: true,
|
footerScrollMode.set(withSpring(0, SHELL_SPRING_CONFIG))
|
||||||
})
|
|
||||||
headerMode.set(shown)
|
|
||||||
footerScrollMode.set(shown)
|
|
||||||
}, [headerMode, footerScrollMode])
|
}, [headerMode, footerScrollMode])
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
|||||||
Reference in New Issue
Block a user