Skip to content

Commit b5fde23

Browse files
[cherry-pick][4.1] Fix commits during the Android draw pass (#9124)
Cherry picks #9078 and #9072
1 parent 311fc6d commit b5fde23

12 files changed

Lines changed: 766 additions & 369 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { useNavigation } from '@react-navigation/native';
2+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
3+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
4+
import React, { useCallback } from 'react';
5+
import { Button, StyleSheet, Text, View } from 'react-native';
6+
import Animated, {
7+
interpolate,
8+
useAnimatedScrollHandler,
9+
useAnimatedStyle,
10+
useSharedValue,
11+
} from 'react-native-reanimated';
12+
13+
type AndroidDrawPassParamList = {
14+
Description: undefined;
15+
ScrollList: undefined;
16+
};
17+
18+
const Stack = createNativeStackNavigator<AndroidDrawPassParamList>();
19+
const LIST_ITEM_COUNT = 80;
20+
21+
export default function AndroidDrawPassExample() {
22+
return (
23+
<View style={descriptionStyles.container}>
24+
<Stack.Navigator>
25+
<Stack.Screen name="Description" component={DescriptionScreen} />
26+
<Stack.Screen name="ScrollList" component={ScrollListScreen} />
27+
</Stack.Navigator>
28+
</View>
29+
);
30+
}
31+
32+
function DescriptionScreen() {
33+
const navigation =
34+
useNavigation<NativeStackNavigationProp<AndroidDrawPassParamList>>();
35+
36+
return (
37+
<View style={descriptionStyles.centered}>
38+
<Text style={descriptionStyles.title}>Android Draw Pass Fix</Text>
39+
<Text style={descriptionStyles.description}>
40+
This example reproduces a bug that was fixed on Android where committing
41+
updates during a draw pass caused a crash.
42+
</Text>
43+
<Text style={descriptionStyles.description}>
44+
Previously, if you navigated to the scroll list and went back{' '}
45+
<Text style={descriptionStyles.bold}>while actively scrolling</Text>,
46+
the app would crash. This happened because Reanimated was attempting to
47+
commit UI updates while Android was in the middle of a draw pass, and
48+
sometimes React would have some hierarchy mutations queued up that would
49+
get bundled with these updates, triggering the crash.
50+
</Text>
51+
<Text style={descriptionStyles.description}>
52+
The fix detects when a draw pass is in progress and pushes only
53+
non-layout updates through the synchronous update path, while layout
54+
updates are deferred to the next frame. This ensures that we never
55+
attempt to mutate the view hierarchy during a draw pass.
56+
</Text>
57+
<Button
58+
title="Go to scroll list"
59+
onPress={() => navigation.navigate('ScrollList')}
60+
/>
61+
</View>
62+
);
63+
}
64+
65+
function ListItem({ index }: { index: number }) {
66+
return (
67+
<View style={listStyles.listItem}>
68+
<Text>Item {index + 1}</Text>
69+
<Text style={listStyles.listItemSubtext}>
70+
Scroll fast and press Back — this used to crash on Android
71+
</Text>
72+
</View>
73+
);
74+
}
75+
76+
function ScrollListScreen() {
77+
const scrollY = useSharedValue(0);
78+
79+
const scrollHandler = useAnimatedScrollHandler({
80+
onScroll: (event) => {
81+
scrollY.value = event.contentOffset.y;
82+
},
83+
});
84+
85+
const floatingBadgeStyle = useAnimatedStyle(() => {
86+
const scale = interpolate(scrollY.value, [0, 100, 200], [1, 1.2, 0.9]);
87+
const rotate = interpolate(scrollY.value, [0, 150, 300], [0, 5, -5]);
88+
89+
return {
90+
width: 100,
91+
transform: [{ scale }, { rotate: `${rotate}deg` }],
92+
};
93+
});
94+
95+
const renderItem = useCallback(
96+
({ item }: { item: number }) => <ListItem index={item} />,
97+
[]
98+
);
99+
100+
const keyExtractor = useCallback((item: number) => `item-${item}`, []);
101+
const data = Array.from({ length: LIST_ITEM_COUNT }, (_, index) => index);
102+
103+
return (
104+
<View style={listStyles.container}>
105+
<Animated.View style={[listStyles.floatingBadge, floatingBadgeStyle]}>
106+
<Text style={listStyles.badgeText}>scrollY</Text>
107+
</Animated.View>
108+
109+
<Animated.FlatList
110+
data={data}
111+
renderItem={renderItem}
112+
keyExtractor={keyExtractor}
113+
onScroll={scrollHandler}
114+
scrollEventThrottle={16}
115+
contentContainerStyle={listStyles.listContent}
116+
showsVerticalScrollIndicator
117+
/>
118+
</View>
119+
);
120+
}
121+
122+
const descriptionStyles = StyleSheet.create({
123+
container: {
124+
flex: 1,
125+
},
126+
centered: {
127+
flex: 1,
128+
alignItems: 'center',
129+
gap: 16,
130+
padding: 24,
131+
},
132+
title: {
133+
fontSize: 20,
134+
fontWeight: '700',
135+
marginBottom: 4,
136+
},
137+
description: {
138+
color: '#333',
139+
fontSize: 15,
140+
lineHeight: 22,
141+
},
142+
bold: {
143+
fontWeight: '700',
144+
},
145+
});
146+
147+
const listStyles = StyleSheet.create({
148+
container: {
149+
flex: 1,
150+
},
151+
floatingBadge: {
152+
position: 'absolute',
153+
top: 100,
154+
right: 20,
155+
zIndex: 10,
156+
paddingVertical: 8,
157+
paddingHorizontal: 14,
158+
borderRadius: 12,
159+
backgroundColor: 'rgba(255, 149, 0, 0.9)',
160+
},
161+
badgeText: {
162+
color: '#fff',
163+
fontWeight: '600',
164+
},
165+
listContent: {
166+
paddingTop: 24,
167+
paddingBottom: 40,
168+
paddingHorizontal: 20,
169+
},
170+
listItem: {
171+
marginBottom: 8,
172+
borderRadius: 8,
173+
paddingVertical: 16,
174+
paddingHorizontal: 12,
175+
backgroundColor: 'rgba(128, 128, 128, 0.12)',
176+
},
177+
listItemSubtext: {
178+
marginTop: 6,
179+
opacity: 0.8,
180+
},
181+
});

apps/common-app/src/apps/reanimated/examples/StickyHeaderExample.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export default function StickyHeaderExample() {
2424
});
2525

2626
const animatedStyle = useAnimatedStyle(() => {
27-
return { transform: [{ translateY: offset.value }] };
27+
return {
28+
transform: [{ translateY: offset.value }],
29+
width: (offset.value % 200) + 100,
30+
};
2831
});
2932

3033
return (

apps/common-app/src/apps/reanimated/examples/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import WorkletExample from './WorkletExample';
134134
import WorkletFactoryCrash from './WorkletFactoryCrashExample';
135135
import WorkletRuntimeExample from './WorkletRuntimeExample';
136136
import InstanceDiscoveryExample from './InstanceDiscoveryExample';
137+
import AndroidDrawPassExample from './AndroidDrawPassExample';
137138

138139
interface Example {
139140
icon?: string;
@@ -154,6 +155,11 @@ export const EXAMPLES: Record<string, Example> = {
154155
title: 'FPS',
155156
screen: FpsExample,
156157
},
158+
AndroidDrawPassExample: {
159+
icon: '✍️',
160+
title: 'Android Draw Pass',
161+
screen: AndroidDrawPassExample,
162+
},
157163
ThirdPartyComponentsExample: {
158164
icon: '3️⃣',
159165
title: 'Third party components',

packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ void UpdatesRegistry::flushUpdates(UpdatesBatch &updatesBatch) {
3232
}
3333
}
3434

35+
UpdatesBatch UpdatesRegistry::getPendingUpdates() {
36+
auto lock = std::lock_guard<std::mutex>{mutex_};
37+
flushUpdatesToRegistry(updatesBatch_);
38+
39+
UpdatesBatch updatesBatch;
40+
for (const auto &[tag, pair] : updatesRegistry_) {
41+
const auto &[shadowNode, props] = pair;
42+
updatesBatch.emplace_back(shadowNode, props);
43+
}
44+
return updatesBatch;
45+
}
46+
3547
void UpdatesRegistry::collectProps(PropsMap &propsMap) {
3648
std::lock_guard<std::mutex> lock{mutex_};
3749

packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class UpdatesRegistry {
4949

5050
void flushUpdates(UpdatesBatch &updatesBatch);
5151
void collectProps(PropsMap &propsMap);
52+
UpdatesBatch getPendingUpdates();
5253

5354
protected:
5455
mutable std::mutex mutex_;

0 commit comments

Comments
 (0)