Use compiler-safe Reanimated get/set APIs (#6391)

* Convert lightbox to get/set

* Work around software-mansion/react-native-reanimated#6613

* Use get/set in more places

* Port MainScrollProvider to get/set

* Port more to get/set

* Port composer to get/set

* Remove unnecessary thread hops in composer

* Port more things to get/set

* Convert more to get/set, remove redundant runOnJS

* Convert remaining cases to get/set
This commit is contained in:
dan
2024-11-17 15:06:28 +00:00
committed by GitHub
parent d575a2fdaa
commit 474c4eff29
26 changed files with 352 additions and 305 deletions
+40 -32
View File
@@ -81,40 +81,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return { return {
transform: [ transform: [
{ {
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
}, },
{ {
scale: interpolate( scale: interpolate(
outroLogo.value, outroLogo.get(),
[0, 0.08, 1], [0, 0.08, 1],
[1, 0.8, 500], [1, 0.8, 500],
'clamp', 'clamp',
), ),
}, },
], ],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
} }
}) })
const bottomLogoAnimation = useAnimatedStyle(() => { const bottomLogoAnimation = useAnimatedStyle(() => {
return { return {
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
} }
}) })
const reducedLogoAnimation = useAnimatedStyle(() => { const reducedLogoAnimation = useAnimatedStyle(() => {
return { return {
transform: [ transform: [
{ {
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
}, },
], ],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
} }
}) })
const logoWrapperAnimation = useAnimatedStyle(() => { const logoWrapperAnimation = useAnimatedStyle(() => {
return { return {
opacity: interpolate( opacity: interpolate(
outroAppOpacity.value, outroAppOpacity.get(),
[0, 0.1, 0.2, 1], [0, 0.1, 0.2, 1],
[1, 1, 0, 0], [1, 1, 0, 0],
'clamp', 'clamp',
@@ -126,11 +126,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return { return {
transform: [ transform: [
{ {
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'), scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
}, },
], ],
opacity: interpolate( opacity: interpolate(
outroAppOpacity.value, outroAppOpacity.get(),
[0, 0.1, 0.2, 1], [0, 0.1, 0.2, 1],
[0, 0, 1, 1], [0, 0, 1, 1],
'clamp', 'clamp',
@@ -146,29 +146,37 @@ export function Splash(props: React.PropsWithChildren<Props>) {
if (isReady) { if (isReady) {
SplashScreen.hideAsync() SplashScreen.hideAsync()
.then(() => { .then(() => {
intro.value = withTiming( intro.set(() =>
1, withTiming(
{duration: 400, easing: Easing.out(Easing.cubic)}, 1,
async () => { {duration: 400, easing: Easing.out(Easing.cubic)},
// set these values to check animation at specific point async () => {
// outroLogo.value = 0.1 // set these values to check animation at specific point
// outroApp.value = 0.1 // outroLogo.set(0.1)
outroLogo.value = withTiming( // outroApp.set(0.1)
1, outroLogo.set(() =>
{duration: 1200, easing: Easing.in(Easing.cubic)}, withTiming(
() => { 1,
runOnJS(onFinish)() {duration: 1200, easing: Easing.in(Easing.cubic)},
}, () => {
) runOnJS(onFinish)()
outroApp.value = withTiming(1, { },
duration: 1200, ),
easing: Easing.inOut(Easing.cubic), )
}) outroApp.set(() =>
outroAppOpacity.value = withTiming(1, { withTiming(1, {
duration: 1200, duration: 1200,
easing: Easing.in(Easing.cubic), easing: Easing.inOut(Easing.cubic),
}) }),
}, )
outroAppOpacity.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
}),
)
},
),
) )
}) })
.catch(() => {}) .catch(() => {})
+3 -4
View File
@@ -17,13 +17,12 @@ export function Loader(props: Props) {
const rotation = useSharedValue(0) const rotation = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => ({ const animatedStyles = useAnimatedStyle(() => ({
transform: [{rotate: rotation.value + 'deg'}], transform: [{rotate: rotation.get() + 'deg'}],
})) }))
React.useEffect(() => { React.useEffect(() => {
rotation.value = withRepeat( rotation.set(() =>
withTiming(360, {duration: 500, easing: Easing.linear}), withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
-1,
) )
}, [rotation]) }, [rotation])
+28 -22
View File
@@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef<
// animate the opacity then set isOpen to false when done // animate the opacity then set isOpen to false when done
const setIsntOpen = () => setIsOpen(false) const setIsntOpen = () => setIsOpen(false)
opacity.value = withTiming( opacity.set(() =>
0, withTiming(
{ 0,
duration: 400, {
easing: Easing.out(Easing.cubic), duration: 400,
}, easing: Easing.out(Easing.cubic),
() => runOnJS(setIsntOpen)(), },
() => runOnJS(setIsntOpen)(),
),
) )
}, [setIsOpen, opacity]) }, [setIsOpen, opacity])
@@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef<
// animate the vertical translation, the opacity, and the checkmark // animate the vertical translation, the opacity, and the checkmark
const playCheckmark = () => animatedCheckRef.current?.play() const playCheckmark = () => animatedCheckRef.current?.play()
opacity.value = 0 opacity.set(0)
opacity.value = withTiming( opacity.set(() =>
1, withTiming(
{ 1,
duration: 100, {
easing: Easing.out(Easing.cubic), duration: 100,
}, easing: Easing.out(Easing.cubic),
() => runOnJS(playCheckmark)(), },
() => runOnJS(playCheckmark)(),
),
)
translateY.set(0)
translateY.set(() =>
withTiming(insets.top + 10, {
duration: 500,
easing: Easing.out(Easing.cubic),
}),
) )
translateY.value = 0
translateY.value = withTiming(insets.top + 10, {
duration: 500,
easing: Easing.out(Easing.cubic),
})
// start the countdown timer to autoclose // start the countdown timer to autoclose
timeoutRef.current = setTimeout(close, visibleDuration || 5e3) timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
@@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef<
}, [winDim.width]) }, [winDim.width])
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [{translateY: translateY.value}], transform: [{translateY: translateY.get()}],
opacity: opacity.value, opacity: opacity.get(),
})) }))
return ( return (
+12 -8
View File
@@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef<
const checkAnim = useSharedValue(0) const checkAnim = useSharedValue(0)
const circleAnimatedProps = useAnimatedProps(() => ({ const circleAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 166 - circleAnim.value * 166, strokeDashoffset: 166 - circleAnim.get() * 166,
})) }))
const checkAnimatedProps = useAnimatedProps(() => ({ const checkAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 48 - 48 * checkAnim.value, strokeDashoffset: 48 - 48 * checkAnim.get(),
})) }))
const play = React.useCallback( const play = React.useCallback(
(cb?: () => void) => { (cb?: () => void) => {
circleAnim.value = 0 circleAnim.set(0)
checkAnim.value = 0 checkAnim.set(0)
circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear}) circleAnim.set(() =>
checkAnim.value = withDelay( withTiming(1, {duration: 500, easing: Easing.linear}),
500, )
withTiming(1, {duration: 300, easing: Easing.linear}, cb), checkAnim.set(() =>
withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
),
) )
}, },
[circleAnim, checkAnim], [circleAnim, checkAnim],
+9 -7
View File
@@ -34,7 +34,7 @@ export function ActionsWrapper({
const scale = useSharedValue(1) const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}], transform: [{scale: scale.get()}],
})) }))
const open = React.useCallback(() => { const open = React.useCallback(() => {
@@ -46,7 +46,7 @@ export function ActionsWrapper({
const shrink = React.useCallback(() => { const shrink = React.useCallback(() => {
'worklet' 'worklet'
cancelAnimation(scale) cancelAnimation(scale)
scale.value = withTiming(1, {duration: 200}) scale.set(() => withTiming(1, {duration: 200}))
}, [scale]) }, [scale])
const doubleTapGesture = Gesture.Tap() const doubleTapGesture = Gesture.Tap()
@@ -58,11 +58,13 @@ export function ActionsWrapper({
const pressAndHoldGesture = Gesture.LongPress() const pressAndHoldGesture = Gesture.LongPress()
.onStart(() => { .onStart(() => {
'worklet' 'worklet'
scale.value = withTiming(1.05, {duration: 200}, finished => { scale.set(() =>
if (!finished) return withTiming(1.05, {duration: 200}, finished => {
runOnJS(open)() if (!finished) return
shrink() runOnJS(open)()
}) shrink()
}),
)
}) })
.onTouchesUp(shrink) .onTouchesUp(shrink)
.onTouchesMove(shrink) .onTouchesMove(shrink)
+3 -3
View File
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
const onPressIn = React.useCallback(() => { const onPressIn = React.useCallback(() => {
if (isWeb) return if (isWeb) return
scale.value = withTiming(1.075, {duration: 100}) scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale]) }, [scale])
const onPressOut = React.useCallback(() => { const onPressOut = React.useCallback(() => {
if (isWeb) return if (isWeb) return
scale.value = withTiming(1, {duration: 100}) scale.set(() => withTiming(1, {duration: 100}))
}, [scale]) }, [scale])
const onPress = React.useCallback(() => { const onPress = React.useCallback(() => {
@@ -61,7 +61,7 @@ export function ChatEmptyPill() {
}, [playHaptic, prompts.length]) }, [playHaptic, prompts.length])
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}], transform: [{scale: scale.get()}],
})) }))
return ( return (
+3 -3
View File
@@ -35,12 +35,12 @@ export function NewMessagesPill({
const onPressIn = React.useCallback(() => { const onPressIn = React.useCallback(() => {
if (isWeb) return if (isWeb) return
scale.value = withTiming(1.075, {duration: 100}) scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale]) }, [scale])
const onPressOut = React.useCallback(() => { const onPressOut = React.useCallback(() => {
if (isWeb) return if (isWeb) return
scale.value = withTiming(1, {duration: 100}) scale.set(() => withTiming(1, {duration: 100}))
}, [scale]) }, [scale])
const onPress = React.useCallback(() => { const onPress = React.useCallback(() => {
@@ -49,7 +49,7 @@ export function NewMessagesPill({
}, [onPressInner, playHaptic]) }, [onPressInner, playHaptic])
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}], transform: [{scale: scale.get()}],
})) }))
return ( return (
+43 -41
View File
@@ -61,7 +61,7 @@ export function GestureActionView({
const clampedTransX = useDerivedValue(() => { const clampedTransX = useDerivedValue(() => {
const min = actions.leftFirst ? -MAX_WIDTH : 0 const min = actions.leftFirst ? -MAX_WIDTH : 0
const max = actions.rightFirst ? MAX_WIDTH : 0 const max = actions.rightFirst ? MAX_WIDTH : 0
return clamp(transX.value, min, max) return clamp(transX.get(), min, max)
}) })
const iconScale = useSharedValue(1) const iconScale = useSharedValue(1)
@@ -75,21 +75,23 @@ export function GestureActionView({
return return
} }
iconScale.value = withSequence( iconScale.set(() =>
withTiming(1.2, {duration: 175}), withSequence(
withTiming(1, {duration: 100}), withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
),
) )
} }
useAnimatedReaction( useAnimatedReaction(
() => transX, () => transX,
() => { () => {
if (transX.value === 0) { if (transX.get() === 0) {
runOnJS(setActiveAction)(null) runOnJS(setActiveAction)(null)
} else if (transX.value < 0) { } else if (transX.get() < 0) {
if ( if (
actions.leftSecond && actions.leftSecond &&
transX.value <= -actions.leftSecond.threshold transX.get() <= -actions.leftSecond.threshold
) { ) {
if (activeAction !== 'leftSecond') { if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond') runOnJS(setActiveAction)('leftSecond')
@@ -97,10 +99,10 @@ export function GestureActionView({
} else if (activeAction !== 'leftFirst') { } else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst') runOnJS(setActiveAction)('leftFirst')
} }
} else if (transX.value > 0) { } else if (transX.get() > 0) {
if ( if (
actions.rightSecond && actions.rightSecond &&
transX.value > actions.rightSecond.threshold transX.get() > actions.rightSecond.threshold
) { ) {
if (activeAction !== 'rightSecond') { if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond') runOnJS(setActiveAction)('rightSecond')
@@ -119,44 +121,44 @@ export function GestureActionView({
.activeOffsetY([-200, 200]) .activeOffsetY([-200, 200])
.onStart(() => { .onStart(() => {
'worklet' 'worklet'
isActive.value = true isActive.set(true)
}) })
.onChange(e => { .onChange(e => {
'worklet' 'worklet'
transX.value = e.translationX transX.set(e.translationX)
if (e.translationX < 0) { if (e.translationX < 0) {
// Left side // Left side
if (actions.leftSecond) { if (actions.leftSecond) {
if ( if (
e.translationX <= -actions.leftSecond.threshold && e.translationX <= -actions.leftSecond.threshold &&
!hitSecond.value !hitSecond.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() runOnJS(haptic)()
hitSecond.value = true hitSecond.set(true)
} else if ( } else if (
hitSecond.value && hitSecond.get() &&
e.translationX > -actions.leftSecond.threshold e.translationX > -actions.leftSecond.threshold
) { ) {
runPopAnimation() runPopAnimation()
hitSecond.value = false hitSecond.set(false)
} }
} }
if (!hitSecond.value && actions.leftFirst) { if (!hitSecond.get() && actions.leftFirst) {
if ( if (
e.translationX <= -actions.leftFirst.threshold && e.translationX <= -actions.leftFirst.threshold &&
!hitFirst.value !hitFirst.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() runOnJS(haptic)()
hitFirst.value = true hitFirst.set(true)
} else if ( } else if (
hitFirst.value && hitFirst.get() &&
e.translationX > -actions.leftFirst.threshold e.translationX > -actions.leftFirst.threshold
) { ) {
hitFirst.value = false hitFirst.set(false)
} }
} }
} else if (e.translationX > 0) { } else if (e.translationX > 0) {
@@ -164,33 +166,33 @@ export function GestureActionView({
if (actions.rightSecond) { if (actions.rightSecond) {
if ( if (
e.translationX >= actions.rightSecond.threshold && e.translationX >= actions.rightSecond.threshold &&
!hitSecond.value !hitSecond.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() runOnJS(haptic)()
hitSecond.value = true hitSecond.set(true)
} else if ( } else if (
hitSecond.value && hitSecond.get() &&
e.translationX < actions.rightSecond.threshold e.translationX < actions.rightSecond.threshold
) { ) {
runPopAnimation() runPopAnimation()
hitSecond.value = false hitSecond.set(false)
} }
} }
if (!hitSecond.value && actions.rightFirst) { if (!hitSecond.get() && actions.rightFirst) {
if ( if (
e.translationX >= actions.rightFirst.threshold && e.translationX >= actions.rightFirst.threshold &&
!hitFirst.value !hitFirst.get()
) { ) {
runPopAnimation() runPopAnimation()
runOnJS(haptic)() runOnJS(haptic)()
hitFirst.value = true hitFirst.set(true)
} else if ( } else if (
hitFirst.value && hitFirst.get() &&
e.translationX < actions.rightFirst.threshold e.translationX < actions.rightFirst.threshold
) { ) {
hitFirst.value = false hitFirst.set(false)
} }
} }
} }
@@ -198,29 +200,29 @@ export function GestureActionView({
.onEnd(e => { .onEnd(e => {
'worklet' 'worklet'
if (e.translationX < 0) { if (e.translationX < 0) {
if (hitSecond.value && actions.leftSecond) { if (hitSecond.get() && actions.leftSecond) {
runOnJS(actions.leftSecond.action)() runOnJS(actions.leftSecond.action)()
} else if (hitFirst.value && actions.leftFirst) { } else if (hitFirst.get() && actions.leftFirst) {
runOnJS(actions.leftFirst.action)() runOnJS(actions.leftFirst.action)()
} }
} else if (e.translationX > 0) { } else if (e.translationX > 0) {
if (hitSecond.value && actions.rightSecond) { if (hitSecond.get() && actions.rightSecond) {
runOnJS(actions.rightSecond.action)() runOnJS(actions.rightSecond.action)()
} else if (hitSecond.value && actions.rightFirst) { } else if (hitSecond.get() && actions.rightFirst) {
runOnJS(actions.rightFirst.action)() runOnJS(actions.rightFirst.action)()
} }
} }
transX.value = withTiming(0, {duration: 200}) transX.set(() => withTiming(0, {duration: 200}))
hitFirst.value = false hitFirst.set(false)
hitSecond.value = false hitSecond.set(false)
isActive.value = false isActive.set(false)
}) })
const composedGesture = Gesture.Simultaneous(panGesture) const composedGesture = Gesture.Simultaneous(panGesture)
const animatedSliderStyle = useAnimatedStyle(() => { const animatedSliderStyle = useAnimatedStyle(() => {
return { return {
transform: [{translateX: clampedTransX.value}], transform: [{translateX: clampedTransX.get()}],
} }
}) })
@@ -274,7 +276,7 @@ export function GestureActionView({
const animatedBackgroundStyle = useAnimatedStyle(() => { const animatedBackgroundStyle = useAnimatedStyle(() => {
return { return {
backgroundColor: interpolateColor( backgroundColor: interpolateColor(
clampedTransX.value, clampedTransX.get(),
interpolation.inputRange, interpolation.inputRange,
// @ts-expect-error - Weird type expected by reanimated, but this is okay // @ts-expect-error - Weird type expected by reanimated, but this is okay
interpolation.outputRange, interpolation.outputRange,
@@ -283,10 +285,10 @@ export function GestureActionView({
}) })
const animatedIconStyle = useAnimatedStyle(() => { const animatedIconStyle = useAnimatedStyle(() => {
const absTransX = Math.abs(clampedTransX.value) const absTransX = Math.abs(clampedTransX.get())
return { return {
opacity: interpolate(absTransX, [0, 75], [0.15, 1]), opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
transform: [{scale: iconScale.value}], transform: [{scale: iconScale.get()}],
} }
}) })
+5 -8
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native' import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
import Animated, { import Animated, {
cancelAnimation, cancelAnimation,
runOnJS,
useAnimatedStyle, useAnimatedStyle,
useReducedMotion, useReducedMotion,
useSharedValue, useSharedValue,
@@ -32,27 +31,25 @@ export function PressableScale({
const scale = useSharedValue(1) const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}], transform: [{scale: scale.get()}],
})) }))
return ( return (
<AnimatedPressable <AnimatedPressable
accessibilityRole="button" accessibilityRole="button"
onPressIn={e => { onPressIn={e => {
'worklet'
if (onPressIn) { if (onPressIn) {
runOnJS(onPressIn)(e) onPressIn(e)
} }
cancelAnimation(scale) cancelAnimation(scale)
scale.value = withTiming(targetScale, {duration: 100}) scale.set(() => withTiming(targetScale, {duration: 100}))
}} }}
onPressOut={e => { onPressOut={e => {
'worklet'
if (onPressOut) { if (onPressOut) {
runOnJS(onPressOut)(e) onPressOut(e)
} }
cancelAnimation(scale) cancelAnimation(scale)
scale.value = withTiming(1, {duration: 100}) scale.set(() => withTiming(1, {duration: 100}))
}} }}
style={[!reducedMotion && animatedStyle, style]} style={[!reducedMotion && animatedStyle, style]}
{...rest}> {...rest}>
+11 -9
View File
@@ -10,15 +10,16 @@ export function useMinimalShellHeaderTransform() {
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const headerTransform = useAnimatedStyle(() => { const headerTransform = useAnimatedStyle(() => {
const headerModeValue = headerMode.get()
return { return {
pointerEvents: headerMode.value === 0 ? 'auto' : 'none', pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerMode.value, 2), opacity: Math.pow(1 - headerModeValue, 2),
transform: [ transform: [
{ {
translateY: interpolate( translateY: interpolate(
headerMode.value, headerModeValue,
[0, 1], [0, 1],
[0, -headerHeight.value], [0, -headerHeight.get()],
), ),
}, },
], ],
@@ -33,15 +34,16 @@ export function useMinimalShellFooterTransform() {
const {footerHeight} = useShellLayout() const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => { const footerTransform = useAnimatedStyle(() => {
const footerModeValue = footerMode.get()
return { return {
pointerEvents: footerMode.value === 0 ? 'auto' : 'none', pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerMode.value, 2), opacity: Math.pow(1 - footerModeValue, 2),
transform: [ transform: [
{ {
translateY: interpolate( translateY: interpolate(
footerMode.value, footerModeValue,
[0, 1], [0, 1],
[0, footerHeight.value], [0, footerHeight.get()],
), ),
}, },
], ],
@@ -58,7 +60,7 @@ export function useMinimalShellFabTransform() {
return { return {
transform: [ transform: [
{ {
translateY: interpolate(footerMode.value, [0, 1], [-44, 0]), translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
}, },
], ],
} }
@@ -108,22 +108,22 @@ export function MessageInput({
const measurement = measure(inputRef) const measurement = measure(inputRef)
if (!measurement) return if (!measurement) return
const max = windowHeight - -keyboardHeight.value - topInset - 150 const max = windowHeight - -keyboardHeight.get() - topInset - 150
const availableSpace = max - measurement.height const availableSpace = max - measurement.height
maxHeight.value = max maxHeight.set(max)
isInputScrollable.value = availableSpace < 30 isInputScrollable.set(availableSpace < 30)
}, },
}, },
[windowHeight, topInset], [windowHeight, topInset],
) )
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
maxHeight: maxHeight.value, maxHeight: maxHeight.get(),
})) }))
const animatedProps = useAnimatedProps(() => ({ const animatedProps = useAnimatedProps(() => ({
scrollEnabled: isInputScrollable.value, scrollEnabled: isInputScrollable.get(),
})) }))
return ( return (
@@ -145,7 +145,7 @@ export function MessagesList({
(_: number, height: number) => { (_: number, height: number) => {
// Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
// previous off whenever we add new content to the previous offset whenever we add new content to the list. // previous off whenever we add new content to the previous offset whenever we add new content to the list.
if (isWeb && isAtTop.value && hasScrolled) { if (isWeb && isAtTop.get() && hasScrolled) {
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
offset: height - prevContentHeight.current, offset: height - prevContentHeight.current,
animated: false, animated: false,
@@ -153,7 +153,7 @@ export function MessagesList({
} }
// This number _must_ be the height of the MaybeLoader component // This number _must_ be the height of the MaybeLoader component
if (height > 50 && isAtBottom.value) { if (height > 50 && isAtBottom.get()) {
// If the size of the content is changing by more than the height of the screen, then we don't // If the size of the content is changing by more than the height of the screen, then we don't
// want to scroll further than the start of all the new content. Since we are storing the previous offset, // want to scroll further than the start of all the new content. Since we are storing the previous offset,
// we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill // we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill
@@ -161,7 +161,7 @@ export function MessagesList({
if ( if (
didBackground.current && didBackground.current &&
hasScrolled && hasScrolled &&
height - prevContentHeight.current > layoutHeight.value - 50 && height - prevContentHeight.current > layoutHeight.get() - 50 &&
convoState.items.length - prevItemCount.current > 1 convoState.items.length - prevItemCount.current > 1
) { ) {
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
@@ -209,7 +209,7 @@ export function MessagesList({
) )
const onStartReached = useCallback(() => { const onStartReached = useCallback(() => {
if (hasScrolled && prevContentHeight.current > layoutHeight.value) { if (hasScrolled && prevContentHeight.current > layoutHeight.get()) {
convoState.fetchMessageHistory() convoState.fetchMessageHistory()
} }
}, [convoState, hasScrolled, layoutHeight]) }, [convoState, hasScrolled, layoutHeight])
@@ -217,18 +217,18 @@ export function MessagesList({
const onScroll = React.useCallback( const onScroll = React.useCallback(
(e: ReanimatedScrollEvent) => { (e: ReanimatedScrollEvent) => {
'worklet' 'worklet'
layoutHeight.value = e.layoutMeasurement.height layoutHeight.set(e.layoutMeasurement.height)
const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height
// Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom
// when a new message is added, hence the 100 pixel offset // when a new message is added, hence the 100 pixel offset
isAtBottom.value = e.contentSize.height - 100 < bottomOffset isAtBottom.set(e.contentSize.height - 100 < bottomOffset)
isAtTop.value = e.contentOffset.y <= 1 isAtTop.set(e.contentOffset.y <= 1)
if ( if (
newMessagesPill.show && newMessagesPill.show &&
(e.contentOffset.y > newMessagesPill.startContentOffset + 200 || (e.contentOffset.y > newMessagesPill.startContentOffset + 200 ||
isAtBottom.value) isAtBottom.get())
) { ) {
runOnJS(setNewMessagesPill)({ runOnJS(setNewMessagesPill)({
show: false, show: false,
@@ -256,28 +256,28 @@ export function MessagesList({
// Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should
// just update the height here instead of having the `onMove` event do it (that event will not fire!) // just update the height here instead of having the `onMove` event do it (that event will not fire!)
if (e.duration === 0) { if (e.duration === 0) {
layoutScrollWithoutAnimation.value = true layoutScrollWithoutAnimation.set(true)
keyboardHeight.value = e.height keyboardHeight.set(e.height)
} else { } else {
keyboardIsOpening.value = true keyboardIsOpening.set(true)
} }
}, },
onMove: e => { onMove: e => {
'worklet' 'worklet'
keyboardHeight.value = e.height keyboardHeight.set(e.height)
if (e.height > bottomOffset) { if (e.height > bottomOffset) {
scrollTo(flatListRef, 0, 1e7, false) scrollTo(flatListRef, 0, 1e7, false)
} }
}, },
onEnd: () => { onEnd: () => {
'worklet' 'worklet'
keyboardIsOpening.value = false keyboardIsOpening.set(false)
}, },
}) })
const animatedListStyle = useAnimatedStyle(() => ({ const animatedListStyle = useAnimatedStyle(() => ({
marginBottom: marginBottom:
keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset, keyboardHeight.get() > bottomOffset ? keyboardHeight.get() : bottomOffset,
})) }))
// -- Message sending // -- Message sending
@@ -363,13 +363,13 @@ export function MessagesList({
// -- List layout changes (opening emoji keyboard, etc.) // -- List layout changes (opening emoji keyboard, etc.)
const onListLayout = React.useCallback( const onListLayout = React.useCallback(
(e: LayoutChangeEvent) => { (e: LayoutChangeEvent) => {
layoutHeight.value = e.nativeEvent.layout.height layoutHeight.set(e.nativeEvent.layout.height)
if (isWeb || !keyboardIsOpening.value) { if (isWeb || !keyboardIsOpening.get()) {
flatListRef.current?.scrollToEnd({ flatListRef.current?.scrollToEnd({
animated: !layoutScrollWithoutAnimation.value, animated: !layoutScrollWithoutAnimation.get(),
}) })
layoutScrollWithoutAnimation.value = false layoutScrollWithoutAnimation.set(false)
} }
}, },
[ [
@@ -45,7 +45,7 @@ function GrowableAvatarInner({
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [ transform: [
{ {
scale: interpolate(scrollY.value, [-150, 0], [1.2, 1], { scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], {
extrapolateRight: Extrapolation.CLAMP, extrapolateRight: Extrapolation.CLAMP,
}), }),
}, },
@@ -66,7 +66,7 @@ function GrowableBannerInner({
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
transform: [ transform: [
{ {
scale: interpolate(scrollY.value, [-150, 0], [2, 1], { scale: interpolate(scrollY.get(), [-150, 0], [2, 1], {
extrapolateRight: Extrapolation.CLAMP, extrapolateRight: Extrapolation.CLAMP,
}), }),
}, },
@@ -76,7 +76,7 @@ function GrowableBannerInner({
const animatedBlurViewProps = useAnimatedProps(() => { const animatedBlurViewProps = useAnimatedProps(() => {
return { return {
intensity: interpolate( intensity: interpolate(
scrollY.value, scrollY.get(),
[-300, -65, -15], [-300, -65, -15],
[50, 40, 0], [50, 40, 0],
Extrapolation.CLAMP, Extrapolation.CLAMP,
@@ -85,16 +85,17 @@ function GrowableBannerInner({
}) })
const animatedSpinnerStyle = useAnimatedStyle(() => { const animatedSpinnerStyle = useAnimatedStyle(() => {
const scrollYValue = scrollY.get()
return { return {
display: scrollY.value < 0 ? 'flex' : 'none', display: scrollYValue < 0 ? 'flex' : 'none',
opacity: interpolate( opacity: interpolate(
scrollY.value, scrollYValue,
[-60, -15], [-60, -15],
[1, 0], [1, 0],
Extrapolation.CLAMP, Extrapolation.CLAMP,
), ),
transform: [ transform: [
{translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])}, {translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])},
{rotate: '90deg'}, {rotate: '90deg'},
], ],
} }
@@ -103,7 +104,7 @@ function GrowableBannerInner({
const animatedBackButtonStyle = useAnimatedStyle(() => ({ const animatedBackButtonStyle = useAnimatedStyle(() => ({
transform: [ transform: [
{ {
translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], { translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], {
extrapolateRight: Extrapolation.CLAMP, extrapolateRight: Extrapolation.CLAMP,
}), }),
}, },
@@ -168,7 +169,7 @@ function useShouldAnimateSpinner({
const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10) const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
useAnimatedReaction( useAnimatedReaction(
() => scrollY.value < -5, () => scrollY.get() < -5,
(value, prevValue) => { (value, prevValue) => {
if (value !== prevValue) { if (value !== prevValue) {
runOnJS(setIsOverscrolled)(value) runOnJS(setIsOverscrolled)(value)
+10 -6
View File
@@ -44,13 +44,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
'worklet' 'worklet'
// Cancel any existing animation // Cancel any existing animation
cancelAnimation(headerMode) cancelAnimation(headerMode)
headerMode.value = withSpring(v ? 1 : 0, { headerMode.set(() =>
overshootClamping: true, withSpring(v ? 1 : 0, {
}) overshootClamping: true,
}),
)
cancelAnimation(footerMode) cancelAnimation(footerMode)
footerMode.value = withSpring(v ? 1 : 0, { footerMode.set(() =>
overshootClamping: true, withSpring(v ? 1 : 0, {
}) overshootClamping: true,
}),
)
}, },
[headerMode, footerMode], [headerMode, footerMode],
) )
+27 -19
View File
@@ -1267,12 +1267,12 @@ function useScrollTracker({
const contentHeight = useSharedValue(0) const contentHeight = useSharedValue(0)
const hasScrolledToTop = useDerivedValue(() => const hasScrolledToTop = useDerivedValue(() =>
withTiming(contentOffset.value === 0 ? 1 : 0), withTiming(contentOffset.get() === 0 ? 1 : 0),
) )
const hasScrolledToBottom = useDerivedValue(() => const hasScrolledToBottom = useDerivedValue(() =>
withTiming( withTiming(
contentHeight.value - contentOffset.value - 5 <= scrollViewHeight.value contentHeight.get() - contentOffset.get() - 5 <= scrollViewHeight.get()
? 1 ? 1
: 0, : 0,
), ),
@@ -1290,11 +1290,11 @@ function useScrollTracker({
}) => { }) => {
'worklet' 'worklet'
if (typeof newContentHeight === 'number') if (typeof newContentHeight === 'number')
contentHeight.value = Math.floor(newContentHeight) contentHeight.set(Math.floor(newContentHeight))
if (typeof newContentOffset === 'number') if (typeof newContentOffset === 'number')
contentOffset.value = Math.floor(newContentOffset) contentOffset.set(Math.floor(newContentOffset))
if (typeof newScrollViewHeight === 'number') if (typeof newScrollViewHeight === 'number')
scrollViewHeight.value = Math.floor(newScrollViewHeight) scrollViewHeight.set(Math.floor(newScrollViewHeight))
}, },
[contentHeight, contentOffset, scrollViewHeight], [contentHeight, contentOffset, scrollViewHeight],
) )
@@ -1310,21 +1310,22 @@ function useScrollTracker({
}, },
}) })
const onScrollViewContentSizeChange = useCallback( const onScrollViewContentSizeChangeUIThread = useCallback(
(_width: number, height: number) => { (newContentHeight: number) => {
if (stickyBottom && height > contentHeight.value) { 'worklet'
const oldContentHeight = contentHeight.get()
let shouldScrollToBottom = false
if (stickyBottom && newContentHeight > oldContentHeight) {
const isFairlyCloseToBottom = const isFairlyCloseToBottom =
contentHeight.value - contentOffset.value - 100 <= oldContentHeight - contentOffset.get() - 100 <= scrollViewHeight.get()
scrollViewHeight.value
if (isFairlyCloseToBottom) { if (isFairlyCloseToBottom) {
runOnUI(() => { shouldScrollToBottom = true
scrollTo(scrollViewRef, 0, contentHeight.value, true)
})()
} }
} }
showHideBottomBorder({ showHideBottomBorder({newContentHeight})
newContentHeight: height, if (shouldScrollToBottom) {
}) scrollTo(scrollViewRef, 0, newContentHeight, true)
}
}, },
[ [
showHideBottomBorder, showHideBottomBorder,
@@ -1336,6 +1337,13 @@ function useScrollTracker({
], ],
) )
const onScrollViewContentSizeChange = useCallback(
(_width: number, height: number) => {
runOnUI(onScrollViewContentSizeChangeUIThread)(height)
},
[onScrollViewContentSizeChangeUIThread],
)
const onScrollViewLayout = useCallback( const onScrollViewLayout = useCallback(
(evt: LayoutChangeEvent) => { (evt: LayoutChangeEvent) => {
showHideBottomBorder({ showHideBottomBorder({
@@ -1349,7 +1357,7 @@ function useScrollTracker({
return { return {
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolledToTop.value, hasScrolledToTop.get(),
[0, 1], [0, 1],
[t.atoms.border_contrast_medium.borderColor, 'transparent'], [t.atoms.border_contrast_medium.borderColor, 'transparent'],
), ),
@@ -1359,7 +1367,7 @@ function useScrollTracker({
return { return {
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolledToBottom.value, hasScrolledToBottom.get(),
[0, 1], [0, 1],
[t.atoms.border_contrast_medium.borderColor, 'transparent'], [t.atoms.border_contrast_medium.borderColor, 'transparent'],
), ),
@@ -1604,7 +1612,7 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => {
return { return {
transform: [{rotateZ: `${rotate.value}deg`}], transform: [{rotateZ: `${rotate.get()}deg`}],
} }
}) })
+1 -1
View File
@@ -93,7 +93,7 @@ function HomeHeaderLayoutDesktopAndTablet({
{tabBarAnchor} {tabBarAnchor}
<Animated.View <Animated.View
onLayout={e => { onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height headerHeight.set(e.nativeEvent.layout.height)
}} }}
style={[ style={[
t.atoms.bg, t.atoms.bg,
+1 -1
View File
@@ -43,7 +43,7 @@ export function HomeHeaderLayoutMobile({
<Animated.View <Animated.View
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
onLayout={e => { onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height headerHeight.set(e.nativeEvent.layout.height)
}}> }}>
<View style={[pal.view, styles.topBar]}> <View style={[pal.view, styles.topBar]}>
<View style={[pal.view, {width: 100}]}> <View style={[pal.view, {width: 100}]}>
@@ -87,11 +87,11 @@ const ImageItem = ({
// Note: DO NOT move any logic reading animated values outside this function. // Note: DO NOT move any logic reading animated values outside this function.
useAnimatedReaction( useAnimatedReaction(
() => { () => {
if (pinchScale.value !== 1) { if (pinchScale.get() !== 1) {
// We're currently pinching. // We're currently pinching.
return true return true
} }
const [, , committedScale] = readTransform(committedTransform.value) const [, , committedScale] = readTransform(committedTransform.get())
if (committedScale !== 1) { if (committedScale !== 1) {
// We started from a pinched in state. // We started from a pinched in state.
return true return true
@@ -147,10 +147,10 @@ const ImageItem = ({
.onStart(e => { .onStart(e => {
'worklet' 'worklet'
const screenSize = measureSafeArea() const screenSize = measureSafeArea()
pinchOrigin.value = { pinchOrigin.set({
x: e.focalX - screenSize.width / 2, x: e.focalX - screenSize.width / 2,
y: e.focalY - screenSize.height / 2, y: e.focalY - screenSize.height / 2,
} })
}) })
.onChange(e => { .onChange(e => {
'worklet' 'worklet'
@@ -160,7 +160,7 @@ const ImageItem = ({
} }
// Don't let the picture zoom in so close that it gets blurry. // Don't let the picture zoom in so close that it gets blurry.
// Also, like in stock Android apps, don't let the user zoom out further than 1:1. // Also, like in stock Android apps, don't let the user zoom out further than 1:1.
const [, , committedScale] = readTransform(committedTransform.value) const [, , committedScale] = readTransform(committedTransform.get())
const maxCommittedScale = Math.max( const maxCommittedScale = Math.max(
MIN_SCREEN_ZOOM, MIN_SCREEN_ZOOM,
(imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM, (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM,
@@ -171,20 +171,21 @@ const ImageItem = ({
Math.max(minPinchScale, e.scale), Math.max(minPinchScale, e.scale),
maxPinchScale, maxPinchScale,
) )
pinchScale.value = nextPinchScale pinchScale.set(nextPinchScale)
// Zooming out close to the corner could push us out of bounds, which we don't want on Android. // Zooming out close to the corner could push us out of bounds, which we don't want on Android.
// Calculate where we'll end up so we know how much to translate back to stay in bounds. // Calculate where we'll end up so we know how much to translate back to stay in bounds.
const t = createTransform() const t = createTransform()
prependPan(t, panTranslation.value) prependPan(t, panTranslation.get())
prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value) prependPinch(t, nextPinchScale, pinchOrigin.get(), pinchTranslation.get())
prependTransform(t, committedTransform.value) prependTransform(t, committedTransform.get())
const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
if (dx !== 0 || dy !== 0) { if (dx !== 0 || dy !== 0) {
pinchTranslation.value = { const pt = pinchTranslation.get()
x: pinchTranslation.value.x + dx, pinchTranslation.set({
y: pinchTranslation.value.y + dy, x: pt.x + dx,
} y: pt.y + dy,
})
} }
}) })
.onEnd(() => { .onEnd(() => {
@@ -193,18 +194,18 @@ const ImageItem = ({
let t = createTransform() let t = createTransform()
prependPinch( prependPinch(
t, t,
pinchScale.value, pinchScale.get(),
pinchOrigin.value, pinchOrigin.get(),
pinchTranslation.value, pinchTranslation.get(),
) )
prependTransform(t, committedTransform.value) prependTransform(t, committedTransform.get())
applyRounding(t) applyRounding(t)
committedTransform.value = t committedTransform.set(t)
// Reset just the pinch. // Reset just the pinch.
pinchScale.value = 1 pinchScale.set(1)
pinchOrigin.value = {x: 0, y: 0} pinchOrigin.set({x: 0, y: 0})
pinchTranslation.value = {x: 0, y: 0} pinchTranslation.set({x: 0, y: 0})
}) })
const pan = Gesture.Pan() const pan = Gesture.Pan()
@@ -223,29 +224,29 @@ const ImageItem = ({
prependPan(t, nextPanTranslation) prependPan(t, nextPanTranslation)
prependPinch( prependPinch(
t, t,
pinchScale.value, pinchScale.get(),
pinchOrigin.value, pinchOrigin.get(),
pinchTranslation.value, pinchTranslation.get(),
) )
prependTransform(t, committedTransform.value) prependTransform(t, committedTransform.get())
// Prevent panning from going out of bounds. // Prevent panning from going out of bounds.
const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize)
nextPanTranslation.x += dx nextPanTranslation.x += dx
nextPanTranslation.y += dy nextPanTranslation.y += dy
panTranslation.value = nextPanTranslation panTranslation.set(nextPanTranslation)
}) })
.onEnd(() => { .onEnd(() => {
'worklet' 'worklet'
// Commit just the pan. // Commit just the pan.
let t = createTransform() let t = createTransform()
prependPan(t, panTranslation.value) prependPan(t, panTranslation.get())
prependTransform(t, committedTransform.value) prependTransform(t, committedTransform.get())
applyRounding(t) applyRounding(t)
committedTransform.value = t committedTransform.set(t)
// Reset just the pan. // Reset just the pan.
panTranslation.value = {x: 0, y: 0} panTranslation.set({x: 0, y: 0})
}) })
const singleTap = Gesture.Tap().onEnd(() => { const singleTap = Gesture.Tap().onEnd(() => {
@@ -261,11 +262,11 @@ const ImageItem = ({
if (!imageDimensions || !imageAspect) { if (!imageDimensions || !imageAspect) {
return return
} }
const [, , committedScale] = readTransform(committedTransform.value) const [, , committedScale] = readTransform(committedTransform.get())
if (committedScale !== 1) { if (committedScale !== 1) {
// Go back to 1:1 using the identity vector. // Go back to 1:1 using the identity vector.
let t = createTransform() let t = createTransform()
committedTransform.value = withClampedSpring(t) committedTransform.set(withClampedSpring(t))
return return
} }
@@ -299,7 +300,7 @@ const ImageItem = ({
) )
const finalTransform = createTransform() const finalTransform = createTransform()
prependPinch(finalTransform, scale, origin, {x: dx, y: dy}) prependPinch(finalTransform, scale, origin, {x: dx, y: dy})
committedTransform.value = withClampedSpring(finalTransform) committedTransform.set(withClampedSpring(finalTransform))
}) })
const composedGesture = isScrollViewBeingDragged const composedGesture = isScrollViewBeingDragged
@@ -313,13 +314,13 @@ const ImageItem = ({
) )
const containerStyle = useAnimatedStyle(() => { const containerStyle = useAnimatedStyle(() => {
const {scaleAndMoveTransform, isHidden} = transforms.value const {scaleAndMoveTransform, isHidden} = transforms.get()
// Apply the active adjustments on top of the committed transform before the gestures. // Apply the active adjustments on top of the committed transform before the gestures.
// This is matrix multiplication, so operations are applied in the reverse order. // This is matrix multiplication, so operations are applied in the reverse order.
let t = createTransform() let t = createTransform()
prependPan(t, panTranslation.value) prependPan(t, panTranslation.get())
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value) prependPinch(t, pinchScale.get(), pinchOrigin.get(), pinchTranslation.get())
prependTransform(t, committedTransform.value) prependTransform(t, committedTransform.get())
const [translateX, translateY, scale] = readTransform(t) const [translateX, translateY, scale] = readTransform(t)
const manipulationTransform = [ const manipulationTransform = [
{translateX}, {translateX},
@@ -338,7 +339,7 @@ const ImageItem = ({
}) })
const imageCropStyle = useAnimatedStyle(() => { const imageCropStyle = useAnimatedStyle(() => {
const {cropFrameTransform} = transforms.value const {cropFrameTransform} = transforms.get()
return { return {
flex: 1, flex: 1,
overflow: 'hidden', overflow: 'hidden',
@@ -347,7 +348,7 @@ const ImageItem = ({
}) })
const imageStyle = useAnimatedStyle(() => { const imageStyle = useAnimatedStyle(() => {
const {cropContentTransform} = transforms.value const {cropContentTransform} = transforms.get()
return { return {
flex: 1, flex: 1,
transform: cropContentTransform, transform: cropContentTransform,
@@ -359,7 +360,7 @@ const ImageItem = ({
const [hasLoaded, setHasLoaded] = useState(false) const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction( useAnimatedReaction(
() => { () => {
return transforms.value.isResting && !hasLoaded return transforms.get().isResting && !hasLoaded
}, },
(show, prevShow) => { (show, prevShow) => {
if (show && !prevShow) { if (show && !prevShow) {
@@ -148,7 +148,7 @@ const ImageItem = ({
) )
const containerStyle = useAnimatedStyle(() => { const containerStyle = useAnimatedStyle(() => {
const {scaleAndMoveTransform, isHidden} = transforms.value const {scaleAndMoveTransform, isHidden} = transforms.get()
return { return {
flex: 1, flex: 1,
transform: scaleAndMoveTransform, transform: scaleAndMoveTransform,
@@ -158,7 +158,7 @@ const ImageItem = ({
const imageCropStyle = useAnimatedStyle(() => { const imageCropStyle = useAnimatedStyle(() => {
const screenSize = measureSafeArea() const screenSize = measureSafeArea()
const {cropFrameTransform} = transforms.value const {cropFrameTransform} = transforms.get()
return { return {
overflow: 'hidden', overflow: 'hidden',
transform: cropFrameTransform, transform: cropFrameTransform,
@@ -171,7 +171,7 @@ const ImageItem = ({
}) })
const imageStyle = useAnimatedStyle(() => { const imageStyle = useAnimatedStyle(() => {
const {cropContentTransform} = transforms.value const {cropContentTransform} = transforms.get()
return { return {
transform: cropContentTransform, transform: cropContentTransform,
width: '100%', width: '100%',
@@ -184,7 +184,7 @@ const ImageItem = ({
const [hasLoaded, setHasLoaded] = useState(false) const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction( useAnimatedReaction(
() => { () => {
return transforms.value.isResting && !hasLoaded return transforms.get().isResting && !hasLoaded
}, },
(show, prevShow) => { (show, prevShow) => {
if (show && !prevShow) { if (show && !prevShow) {
+42 -32
View File
@@ -109,18 +109,22 @@ export default function ImageViewRoot({
// https://github.com/software-mansion/react-native-reanimated/issues/6677 // https://github.com/software-mansion/react-native-reanimated/issues/6677
requestAnimationFrame(() => { requestAnimationFrame(() => {
openProgress.value = canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1 openProgress.set(() =>
canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1,
)
}) })
return () => { return () => {
// https://github.com/software-mansion/react-native-reanimated/issues/6677 // https://github.com/software-mansion/react-native-reanimated/issues/6677
requestAnimationFrame(() => { requestAnimationFrame(() => {
openProgress.value = canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0 openProgress.set(() =>
canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0,
)
}) })
} }
}, [nextLightbox, openProgress]) }, [nextLightbox, openProgress])
useAnimatedReaction( useAnimatedReaction(
() => openProgress.value === 0, () => openProgress.get() === 0,
(isGone, wasGone) => { (isGone, wasGone) => {
if (isGone && !wasGone) { if (isGone && !wasGone) {
runOnJS(setActiveLightbox)(null) runOnJS(setActiveLightbox)(null)
@@ -130,7 +134,7 @@ export default function ImageViewRoot({
const onFlyAway = React.useCallback(() => { const onFlyAway = React.useCallback(() => {
'worklet' 'worklet'
openProgress.value = 0 openProgress.set(0)
runOnJS(onRequestClose)() runOnJS(onRequestClose)()
}, [onRequestClose, openProgress]) }, [onRequestClose, openProgress])
@@ -187,7 +191,7 @@ function ImageView({
const isFlyingAway = useSharedValue(false) const isFlyingAway = useSharedValue(false)
const containerStyle = useAnimatedStyle(() => { const containerStyle = useAnimatedStyle(() => {
if (openProgress.value < 1 || isFlyingAway.value) { if (openProgress.get() < 1 || isFlyingAway.get()) {
return {pointerEvents: 'none'} return {pointerEvents: 'none'}
} }
return {pointerEvents: 'auto'} return {pointerEvents: 'auto'}
@@ -196,11 +200,12 @@ function ImageView({
const backdropStyle = useAnimatedStyle(() => { const backdropStyle = useAnimatedStyle(() => {
const screenSize = measure(safeAreaRef) const screenSize = measure(safeAreaRef)
let opacity = 1 let opacity = 1
if (openProgress.value < 1) { const openProgressValue = openProgress.get()
opacity = Math.sqrt(openProgress.value) if (openProgressValue < 1) {
opacity = Math.sqrt(openProgressValue)
} else if (screenSize) { } else if (screenSize) {
const dragProgress = Math.min( const dragProgress = Math.min(
Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2), Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2),
1, 1,
) )
opacity -= dragProgress opacity -= dragProgress
@@ -212,11 +217,11 @@ function ImageView({
}) })
const animatedHeaderStyle = useAnimatedStyle(() => { const animatedHeaderStyle = useAnimatedStyle(() => {
const show = showControls && dismissSwipeTranslateY.value === 0 const show = showControls && dismissSwipeTranslateY.get() === 0
return { return {
pointerEvents: show ? 'box-none' : 'none', pointerEvents: show ? 'box-none' : 'none',
opacity: withClampedSpring( opacity: withClampedSpring(
show && openProgress.value === 1 ? 1 : 0, show && openProgress.get() === 1 ? 1 : 0,
FAST_SPRING, FAST_SPRING,
), ),
transform: [ transform: [
@@ -227,12 +232,12 @@ function ImageView({
} }
}) })
const animatedFooterStyle = useAnimatedStyle(() => { const animatedFooterStyle = useAnimatedStyle(() => {
const show = showControls && dismissSwipeTranslateY.value === 0 const show = showControls && dismissSwipeTranslateY.get() === 0
return { return {
flexGrow: 1, flexGrow: 1,
pointerEvents: show ? 'box-none' : 'none', pointerEvents: show ? 'box-none' : 'none',
opacity: withClampedSpring( opacity: withClampedSpring(
show && openProgress.value === 1 ? 1 : 0, show && openProgress.get() === 1 ? 1 : 0,
FAST_SPRING, FAST_SPRING,
), ),
transform: [ transform: [
@@ -259,7 +264,7 @@ function ImageView({
const screenSize = measure(safeAreaRef) const screenSize = measure(safeAreaRef)
return ( return (
!screenSize || !screenSize ||
Math.abs(dismissSwipeTranslateY.value) > screenSize.height Math.abs(dismissSwipeTranslateY.get()) > screenSize.height
) )
}, },
(isOut, wasOut) => { (isOut, wasOut) => {
@@ -397,10 +402,11 @@ function LightboxImage({
const transforms = useDerivedValue(() => { const transforms = useDerivedValue(() => {
'worklet' 'worklet'
const safeArea = measureSafeArea() const safeArea = measureSafeArea()
const openProgressValue = openProgress.get()
const dismissTranslateY = const dismissTranslateY =
isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0 isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0
if (openProgress.value === 0 && isFlyingAway.value) { if (openProgressValue === 0 && isFlyingAway.get()) {
return { return {
isHidden: true, isHidden: true,
isResting: false, isResting: false,
@@ -410,9 +416,9 @@ function LightboxImage({
} }
} }
if (isActive && thumbRect && imageAspect && openProgress.value < 1) { if (isActive && thumbRect && imageAspect && openProgressValue < 1) {
return interpolateTransform( return interpolateTransform(
openProgress.value, openProgressValue,
thumbRect, thumbRect,
safeArea, safeArea,
imageAspect, imageAspect,
@@ -434,33 +440,37 @@ function LightboxImage({
.maxPointers(1) .maxPointers(1)
.onUpdate(e => { .onUpdate(e => {
'worklet' 'worklet'
if (openProgress.value !== 1 || isFlyingAway.value) { if (openProgress.get() !== 1 || isFlyingAway.get()) {
return return
} }
dismissSwipeTranslateY.value = e.translationY dismissSwipeTranslateY.set(e.translationY)
}) })
.onEnd(e => { .onEnd(e => {
'worklet' 'worklet'
if (openProgress.value !== 1 || isFlyingAway.value) { if (openProgress.get() !== 1 || isFlyingAway.get()) {
return return
} }
if (Math.abs(e.velocityY) > 200) { if (Math.abs(e.velocityY) > 200) {
isFlyingAway.value = true isFlyingAway.set(true)
if (dismissSwipeTranslateY.value === 0) { if (dismissSwipeTranslateY.get() === 0) {
// HACK: If the initial value is 0, withDecay() animation doesn't start. // HACK: If the initial value is 0, withDecay() animation doesn't start.
// This is a bug in Reanimated, but for now we'll work around it like this. // This is a bug in Reanimated, but for now we'll work around it like this.
dismissSwipeTranslateY.value = 1 dismissSwipeTranslateY.set(1)
} }
dismissSwipeTranslateY.value = withDecay({ dismissSwipeTranslateY.set(() =>
velocity: e.velocityY, withDecay({
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. velocity: e.velocityY,
deceleration: 1, // Danger! This relies on the reaction below stopping it. velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
}) deceleration: 1, // Danger! This relies on the reaction below stopping it.
}),
)
} else { } else {
dismissSwipeTranslateY.value = withSpring(0, { dismissSwipeTranslateY.set(() =>
stiffness: 700, withSpring(0, {
damping: 50, stiffness: 700,
}) damping: 50,
}),
)
} }
}) })
+6 -6
View File
@@ -131,11 +131,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const lastForcedScrollY = useSharedValue(0) const lastForcedScrollY = useSharedValue(0)
const adjustScrollForOtherPages = () => { const adjustScrollForOtherPages = () => {
'worklet' 'worklet'
const currentScrollY = scrollY.value const currentScrollY = scrollY.get()
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
if (lastForcedScrollY.value !== forcedScrollY) { if (lastForcedScrollY.get() !== forcedScrollY) {
lastForcedScrollY.value = forcedScrollY lastForcedScrollY.set(forcedScrollY)
const refs = scrollRefs.value const refs = scrollRefs.get()
for (let i = 0; i < refs.length; i++) { for (let i = 0; i < refs.length; i++) {
const scollRef = refs[i] const scollRef = refs[i]
if (i !== currentPage && scollRef != null) { if (i !== currentPage && scollRef != null) {
@@ -167,7 +167,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
const isPossiblyInvalid = const isPossiblyInvalid =
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
if (!isPossiblyInvalid) { if (!isPossiblyInvalid) {
scrollY.value = nextScrollY scrollY.set(nextScrollY)
runOnJS(queueThrottledOnScroll)() runOnJS(queueThrottledOnScroll)()
} }
}, },
@@ -246,7 +246,7 @@ let PagerTabBar = ({
allowHeaderOverScroll?: boolean allowHeaderOverScroll?: boolean
}): React.ReactNode => { }): React.ReactNode => {
const headerTransform = useAnimatedStyle(() => { const headerTransform = useAnimatedStyle(() => {
const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1 const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1
return { return {
transform: [ transform: [
{ {
@@ -18,7 +18,7 @@ export function createCustomBackdrop(
// animated variables // animated variables
const opacity = useAnimatedStyle(() => ({ const opacity = useAnimatedStyle(() => ({
opacity: interpolate( opacity: interpolate(
animatedIndex.value, // current snap index animatedIndex.get(), // current snap index
[-1, 0], // input range [-1, 0], // input range
[0, 0.5], // output range [0, 0.5], // output range
Extrapolation.CLAMP, Extrapolation.CLAMP,
+2 -2
View File
@@ -79,8 +79,8 @@ function ListImpl<ItemT>(
onScrollFromContext?.(e, ctx) onScrollFromContext?.(e, ctx)
const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT
if (isScrolledDown.value !== didScrollDown) { if (isScrolledDown.get() !== didScrollDown) {
isScrolledDown.value = didScrollDown isScrolledDown.set(didScrollDown)
if (onScrolledDownChange != null) { if (onScrolledDownChange != null) {
runOnJS(handleScrolledDownChange)(didScrollDown) runOnJS(handleScrolledDownChange)(didScrollDown)
} }
+27 -24
View File
@@ -44,7 +44,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(v: boolean) => { (v: boolean) => {
'worklet' 'worklet'
cancelAnimation(headerMode) cancelAnimation(headerMode)
headerMode.value = v ? V1.value : V0.value headerMode.set(v ? V1.get() : V0.get())
}, },
[headerMode], [headerMode],
) )
@@ -52,9 +52,9 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
useEffect(() => { useEffect(() => {
if (isWeb) { if (isWeb) {
return listenToForcedWindowScroll(() => { return listenToForcedWindowScroll(() => {
startDragOffset.value = null startDragOffset.set(null)
startMode.value = null startMode.set(null)
didJustRestoreScroll.value = true didJustRestoreScroll.set(true)
}) })
} }
}) })
@@ -63,13 +63,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => { (e: NativeScrollEvent) => {
'worklet' 'worklet'
if (isNative) { if (isNative) {
if (startDragOffset.value === null) { const startDragOffsetValue = startDragOffset.get()
if (startDragOffsetValue === null) {
return return
} }
const didScrollDown = e.contentOffset.y > startDragOffset.value const didScrollDown = e.contentOffset.y > startDragOffsetValue
startDragOffset.value = null startDragOffset.set(null)
startMode.value = null startMode.set(null)
if (e.contentOffset.y < headerHeight.value) { if (e.contentOffset.y < headerHeight.get()) {
// If we're close to the top, show the shell. // If we're close to the top, show the shell.
setMode(false) setMode(false)
} else if (didScrollDown) { } else if (didScrollDown) {
@@ -77,7 +78,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
setMode(true) setMode(true)
} else { } else {
// Snap to whichever state is the closest. // Snap to whichever state is the closest.
setMode(Math.round(headerMode.value) === 1) setMode(Math.round(headerMode.get()) === 1)
} }
} }
}, },
@@ -88,8 +89,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => { (e: NativeScrollEvent) => {
'worklet' 'worklet'
if (isNative) { if (isNative) {
startDragOffset.value = e.contentOffset.y startDragOffset.set(e.contentOffset.y)
startMode.value = headerMode.value startMode.set(headerMode.get())
} }
}, },
[headerMode, startDragOffset, startMode], [headerMode, startDragOffset, startMode],
@@ -123,10 +124,12 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
(e: NativeScrollEvent) => { (e: NativeScrollEvent) => {
'worklet' 'worklet'
if (isNative) { if (isNative) {
if (startDragOffset.value === null || startMode.value === null) { const startDragOffsetValue = startDragOffset.get()
const startModeValue = startMode.get()
if (startDragOffsetValue === null || startModeValue === null) {
if ( if (
headerMode.value !== 0 && headerMode.get() !== 0 &&
e.contentOffset.y < headerHeight.value e.contentOffset.y < headerHeight.get()
) { ) {
// If we're close enough to the top, always show the shell. // If we're close enough to the top, always show the shell.
// Even if we're not dragging. // Even if we're not dragging.
@@ -137,29 +140,29 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
// The "mode" value is always between 0 and 1. // The "mode" value is always between 0 and 1.
// Figure out how much to move it based on the current dragged distance. // Figure out how much to move it based on the current dragged distance.
const dy = e.contentOffset.y - startDragOffset.value const dy = e.contentOffset.y - startDragOffsetValue
const dProgress = interpolate( const dProgress = interpolate(
dy, dy,
[-headerHeight.value, headerHeight.value], [-headerHeight.get(), headerHeight.get()],
[-1, 1], [-1, 1],
) )
const newValue = clamp(startMode.value + dProgress, 0, 1) const newValue = clamp(startModeValue + dProgress, 0, 1)
if (newValue !== headerMode.value) { if (newValue !== headerMode.get()) {
// Manually adjust the value. This won't be (and shouldn't be) animated. // Manually adjust the value. This won't be (and shouldn't be) animated.
// Cancel any any existing animation // Cancel any any existing animation
cancelAnimation(headerMode) cancelAnimation(headerMode)
headerMode.value = newValue headerMode.set(newValue)
} }
} else { } else {
if (didJustRestoreScroll.value) { if (didJustRestoreScroll.get()) {
didJustRestoreScroll.value = false didJustRestoreScroll.set(false)
// Don't hide/show navbar based on scroll restoratoin. // Don't hide/show navbar based on scroll restoratoin.
return return
} }
// On the web, we don't try to follow the drag because we don't know when it ends. // On the web, we don't try to follow the drag because we don't know when it ends.
// Instead, show/hide immediately based on whether we're scrolling up or down. // Instead, show/hide immediately based on whether we're scrolling up or down.
const dy = e.contentOffset.y - (startDragOffset.value ?? 0) const dy = e.contentOffset.y - (startDragOffset.get() ?? 0)
startDragOffset.value = e.contentOffset.y startDragOffset.set(e.contentOffset.y)
if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) { if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
setMode(false) setMode(false)
+1 -1
View File
@@ -134,7 +134,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
footerMinimalShellTransform, footerMinimalShellTransform,
]} ]}
onLayout={e => { onLayout={e => {
footerHeight.value = e.nativeEvent.layout.height footerHeight.set(e.nativeEvent.layout.height)
}}> }}>
{hasSession ? ( {hasSession ? (
<> <>