[iOS][Old Arch] Layout animations fail when component with `useAnimatedScrollHandlers` remounts.
Description
Wild title. Please bear with me.
In the follow video I have a small black box which expands and shrinks. This is implemented as a layout animation (i.e. the layout prop in an animated view). When the background is red, the box should be small. When the background is blue, the box should be tall.
The layout animation works fine until I add a ScrollView with a useAnimatedScrollHandler and initial non-zero contentOffset. If the ScrollView remounts as the layout animation is playing, it seems to sometimes "pause" or "stop" the layout animation. I must then wait some time before the layout animation is responsive again.
https://github.com/user-attachments/assets/8c192045-5b50-46e7-920b-af9ffda2fd8c
Here are some of the insights that I can gleam:
- This issue only happens on Paper. I cannot repro on Fabric.
-
useAnimatedScrollHandlerleverages the privateWorkletEventHandlerAPI. If I hack the library to callWorkletEventHandlerdirectly, the same issue occurs. - The remount seems important. Triggering a normal scroll event does seems to repro.
- Passing in
{x: 0, y: 0}to theScrollView'scontentOffsetdoes not trigger the bug. - The
onScrollhandler can be empty. - The
ScrollViewcontents can be empty.
Code:
import * as React from 'react';
import {Button, SafeAreaView, ScrollView, View} from 'react-native';
import Reanimated, {
LayoutAnimation,
LayoutAnimationsValues,
useAnimatedScrollHandler,
withSpring,
} from 'react-native-reanimated';
const AnimatedScrollView = Reanimated.createAnimatedComponent(ScrollView);
const BAR_SPRING_PHYSICS = {
mass: 10,
damping: 100,
stiffness: 200,
};
interface ThingProps {
selected: boolean;
}
function Thing({selected}: ThingProps) {
const height = selected ? 40 : 8;
const style = React.useMemo(
() => ({
backgroundColor: 'black',
width: 50,
height,
marginTop: 50 + (height / 2) * -1,
}),
[height],
);
const layout = React.useCallback(
(values: LayoutAnimationsValues): LayoutAnimation => {
'worklet';
return {
animations: {
originY: withSpring(values.targetOriginY, BAR_SPRING_PHYSICS),
originX: withSpring(values.targetOriginX, BAR_SPRING_PHYSICS),
height: withSpring(values.targetHeight, BAR_SPRING_PHYSICS),
},
initialValues: {
height: values.currentHeight,
originY: values.currentOriginY,
originX: values.currentOriginX,
},
};
},
[],
);
return (
<View
style={{
backgroundColor: selected ? 'blue' : 'red',
width: '100%',
height: 100,
}}>
<Reanimated.View style={style} layout={layout} />
</View>
);
}
function WeirdScrollView({foo}: {foo?: string}) {
const handlers = useAnimatedScrollHandler({
onScroll: () => {
'worklet';
},
});
const ref = React.useRef(null);
React.useEffect(() => {
ref.current.scrollTo({
x: 0,
y: 234,
});
}, [foo]);
return (
<AnimatedScrollView
ref={ref}
contentOffset={{x: 0, y: 234}}
onScroll={handlers}>
<View style={{backgroundColor: 'black', width: 10, height: 2000}} />
</AnimatedScrollView>
);
}
const uiManager = global?.nativeFabricUIManager ? 'Fabric' : 'Paper';
export default function HomeScreen() {
const [selected, setSelected] = React.useState(false);
function toggle() {
setSelected(prev => !prev);
}
// Pass key to WeirdScrollView: breaks
// Pass foo to WeirdScrollView: works
return (
<SafeAreaView>
<Button onPress={toggle} title={`Toggle (${uiManager})`} />
<Thing selected={selected} />
<WeirdScrollView key={selected ? '1' : '2'} />
</SafeAreaView>
);
}
Steps to reproduce
- Check out my repo or paste the code somewhere.
- Make sure you're on Paper and not Fabric.
- Click toggle twice.
You should see the box stuck in a middle transition state.
Snack or a link to a repository
https://github.com/tpcstld/rn-repro/tree/layout-bug
Reanimated version
3.17.5
React Native version
0.79.2
Platforms
iOS
JavaScript runtime
Hermes
Workflow
React Native
Architecture
Paper (Old Architecture)
Build type
Debug app & dev bundle
Device
iOS simulator
Host machine
macOS
Device model
No response
Acknowledgements
Yes