Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React, { useCallback } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import Animated, {
interpolate,
useAnimatedScrollHandler,
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';

type AndroidDrawPassParamList = {
Description: undefined;
ScrollList: undefined;
};

const Stack = createNativeStackNavigator<AndroidDrawPassParamList>();
const LIST_ITEM_COUNT = 80;

export default function AndroidDrawPassExample() {
return (
<View style={descriptionStyles.container}>
<Stack.Navigator>
<Stack.Screen name="Description" component={DescriptionScreen} />
<Stack.Screen name="ScrollList" component={ScrollListScreen} />
</Stack.Navigator>
</View>
);
}

function DescriptionScreen() {
const navigation =
useNavigation<NativeStackNavigationProp<AndroidDrawPassParamList>>();

return (
<View style={descriptionStyles.centered}>
<Text style={descriptionStyles.title}>Android Draw Pass Fix</Text>
<Text style={descriptionStyles.description}>
This example reproduces a bug that was fixed on Android where committing
updates during a draw pass caused a crash.
</Text>
<Text style={descriptionStyles.description}>
Previously, if you navigated to the scroll list and went back{' '}
<Text style={descriptionStyles.bold}>while actively scrolling</Text>,
the app would crash. This happened because Reanimated was attempting to
commit UI updates while Android was in the middle of a draw pass, and
sometimes React would have some hierarchy mutations queued up that would
get bundled with these updates, triggering the crash.
</Text>
<Text style={descriptionStyles.description}>
The fix detects when a draw pass is in progress and pushes only
non-layout updates through the synchronous update path, while layout
updates are deferred to the next frame. This ensures that we never
attempt to mutate the view hierarchy during a draw pass.
</Text>
<Button
title="Go to scroll list"
onPress={() => navigation.navigate('ScrollList')}
/>
</View>
);
}

function ListItem({ index }: { index: number }) {
return (
<View style={listStyles.listItem}>
<Text>Item {index + 1}</Text>
<Text style={listStyles.listItemSubtext}>
Scroll fast and press Back — this used to crash on Android
</Text>
</View>
);
}

function ScrollListScreen() {
const scrollY = useSharedValue(0);

const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
scrollY.value = event.contentOffset.y;
},
});

const floatingBadgeStyle = useAnimatedStyle(() => {
const scale = interpolate(scrollY.value, [0, 100, 200], [1, 1.2, 0.9]);
const rotate = interpolate(scrollY.value, [0, 150, 300], [0, 5, -5]);

return {
width: 100,
transform: [{ scale }, { rotate: `${rotate}deg` }],
};
});

const renderItem = useCallback(
({ item }: { item: number }) => <ListItem index={item} />,
[]
);

const keyExtractor = useCallback((item: number) => `item-${item}`, []);
const data = Array.from({ length: LIST_ITEM_COUNT }, (_, index) => index);

return (
<View style={listStyles.container}>
<Animated.View style={[listStyles.floatingBadge, floatingBadgeStyle]}>
<Text style={listStyles.badgeText}>scrollY</Text>
</Animated.View>

<Animated.FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
onScroll={scrollHandler}
scrollEventThrottle={16}
contentContainerStyle={listStyles.listContent}
showsVerticalScrollIndicator
/>
</View>
);
}

const descriptionStyles = StyleSheet.create({
container: {
flex: 1,
},
centered: {
flex: 1,
alignItems: 'center',
gap: 16,
padding: 24,
},
title: {
fontSize: 20,
fontWeight: '700',
marginBottom: 4,
},
description: {
color: '#333',
fontSize: 15,
lineHeight: 22,
},
bold: {
fontWeight: '700',
},
});

const listStyles = StyleSheet.create({
container: {
flex: 1,
},
floatingBadge: {
position: 'absolute',
top: 100,
right: 20,
zIndex: 10,
paddingVertical: 8,
paddingHorizontal: 14,
borderRadius: 12,
backgroundColor: 'rgba(255, 149, 0, 0.9)',
},
badgeText: {
color: '#fff',
fontWeight: '600',
},
listContent: {
paddingTop: 24,
paddingBottom: 40,
paddingHorizontal: 20,
},
listItem: {
marginBottom: 8,
borderRadius: 8,
paddingVertical: 16,
paddingHorizontal: 12,
backgroundColor: 'rgba(128, 128, 128, 0.12)',
},
listItemSubtext: {
marginTop: 6,
opacity: 0.8,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export default function StickyHeaderExample() {
});

const animatedStyle = useAnimatedStyle(() => {
return { transform: [{ translateY: offset.value }] };
return {
transform: [{ translateY: offset.value }],
width: (offset.value % 200) + 100,
Comment thread
bartlomiejbloniarz marked this conversation as resolved.
};
});

return (
Expand Down
6 changes: 6 additions & 0 deletions apps/common-app/src/apps/reanimated/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ import WorkletFactoryCrash from './WorkletFactoryCrashExample';
import WorkletRuntimeExample from './WorkletRuntimeExample';
import InstanceDiscoveryExample from './InstanceDiscoveryExample';
import ShadowNodesCloningExample from './ShadowNodesCloningExample';
import AndroidDrawPassExample from './AndroidDrawPassExample';

export const REAPlatform = {
IOS: 'ios',
Expand Down Expand Up @@ -204,6 +205,11 @@ export const EXAMPLES: Record<string, Example> = {
title: 'Sync back to React',
screen: SyncBackToReactExample,
},
AndroidDrawPassExample: {
icon: '✍️',
title: 'Android Draw Pass',
screen: AndroidDrawPassExample,
},
DetachAnimatedStylesExample: {
icon: '⛓️‍💥',
title: 'Detach animated styles',
Expand Down
6 changes: 3 additions & 3 deletions apps/fabric-example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2519,7 +2519,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
FBLazyVector: c12d2108050e27952983d565a232f6f7b1ad5e69
hermes-engine: 53012435cc80627d41ad53d8c27ecc7efaef2f12
hermes-engine: f111e1a9cf7d7933bdceaf272d1777d0fb1d89ac
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
NitroMmkv: 7fe66a61d5acab6516098a64f42af575595e7566
NitroModules: 70861471ee1cc5616462aef869a164dac12db7b9
Expand All @@ -2531,7 +2531,7 @@ SPEC CHECKSUMS:
React: 7ef36630d07638043a134a7dd2ec17e0be10fc3c
React-callinvoker: af4e8fe1d60ab63dd8d74c2a68988064c2848954
React-Core: c609976c034ba9556bef9850a571a71bd458d73f
React-Core-prebuilt: ea396d0e40644e9e46245e9b4b6db931b6be006b
React-Core-prebuilt: 71485c8868eb75b30e2f564f19187f96b53403ee
React-CoreModules: 0ea85f3b3f4b8cbfb3afacd2ed85458fb878517a
React-cxxreact: 6752bab77c0599d6136e2b8b9b64b4a7d316d401
React-debug: 38389b86e3570558ec73dd4cbc0cd2f2eec47a51
Expand Down Expand Up @@ -2594,7 +2594,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 6b7e8d8d974ed13fb66698d82c30c5e70c1f7d3a
ReactCodegen: 2fe81bae8e7b638d8d9b78c3dc1ccc1eeabfc613
ReactCommon: 92b53b0bd7f7d86154dc9f512c1ea5dee717cc72
ReactNativeDependencies: 554aa4807a73d8b65ad8147af7b2d6042b7787be
ReactNativeDependencies: 5cedfcfef19b6597a3c9b4bfc42de85a7b3d39cc
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
RNCClipboard: 88d7eeb555d1183915f0885bdbc5c97eb6f7f3ba
RNCMaskedView: d2578d41c59b936db122b2798ba37e4722d21035
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ void UpdatesRegistry::flushUpdates(UpdatesBatch &updatesBatch) {
}
}

UpdatesBatch UpdatesRegistry::getPendingUpdates() {
auto lock = std::lock_guard<std::mutex>{mutex_};
flushUpdatesToRegistry(updatesBatch_);

Comment thread
bartlomiejbloniarz marked this conversation as resolved.
UpdatesBatch updatesBatch;
for (const auto &[tag, pair] : updatesRegistry_) {
const auto &[shadowNode, props] = pair;
updatesBatch.emplace_back(shadowNode, props);
}
return updatesBatch;
}

void UpdatesRegistry::collectProps(PropsMap &propsMap) {
std::lock_guard<std::mutex> lock{mutex_};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class UpdatesRegistry {

void flushUpdates(UpdatesBatch &updatesBatch);
void collectProps(PropsMap &propsMap);
UpdatesBatch getPendingUpdates();

protected:
mutable std::mutex mutex_;
Expand Down
Loading
Loading