-
-
Notifications
You must be signed in to change notification settings - Fork 530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(Android): going back on fabric with removeClippedSubviews #2495
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initial notes below 👇🏻
2e73c1f
to
b4416e2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not tested the runtime. Let me know that you've tested this (some casual navigation, flatlists, etc.) or that you need me to test this. When you're ready, please mark the PR as open.
Right now the code changes look good, my only concern is runtime behaviour, as I do not understand the issue at hand (leaving it up to you).
@kkafar I tested this change using the example app and the updated If you have any more concerns about the runtime behaviour please let me know. I'll do my best to test it, although my availability may be limited. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is looking good. If you tested and the logic makes sense, then I think we should proceed.
Please do not merge it yet. I just thought of something, but I'll be able to test my hypothesis tomorrow / early next week. |
I've debugged this for a while now & I have good understanding of what's going on. This bug is caused by our usage of
<View <-- ContainerView (CV)
removeClippedSubviews
style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }}>
<View removeClippedSubviews style={{ height: '100%' }}> <--- IntermediateView (IV)
<View removeClippedSubviews style={{ backgroundColor: 'pink', width: '100%', height: 50 }} /> <--- ChildView (ChV)
</View>
</View>
The crash happens in 7.1, because ChV has been removed from Possible solutions: WIP |
Related PR in react-native repo: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should proceed with merging to salvage the situation before (and if at all) the fix for this lands in core.
…child (#2531) ## Description Attempt to patch changes from #2495. It improved the situation in most of the cases but caused crashes in cases where we tried to insert subviews into viewgroups that have any kind of assertions on their child count, e.g. `NestedScrollView` or `ViewPager`. Fixes #2529 Supersedes #2527 ## Changes In this PR I do two things: * apply the workaround only to views that could actually have clipped subviews * catch any kind of exception thrown on view insertion; if one is thrown we stop to add subviews for given child ## Test code and steps to reproduce Test2282 works in both configurations (complex screen with nested flatlists + my simple reproduction of the issue) ## Checklist - [ ] Included code example that can be used to test this change - [ ] Updated TS types - [ ] Updated documentation: <!-- For adding new props to native-stack --> - [ ] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [ ] Ensured that CI passes
Summary: Related PR in `react-native-screens`: * software-mansion/react-native-screens#2495 Additional context: * [my detailed explanation of **one of the issues**](software-mansion/react-native-screens#2495 (comment)) * [Android Developer: ViewGroup.startViewTransition docs](https://developer.android.com/reference/android/view/ViewGroup#startViewTransition(android.view.View)) ### Background On Android view groups can be marked as "transitioning" with a `ViewGroup.startViewTransition` call. This effectively ensures, that in case a view group is marked with this call and its children are removed, they will be still drawn until `endViewTransition` is not called. This mechanism is implemented in Android by [keeping track of "transitioning" children in auxiliary `mTransitioningViews` array](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#7178). Then when such "transitioning" child is removed, [it is removed from children array](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#5595) but it's [parent-child relationship is not cleared](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#5397) and it is still retained in the auxiliary array. Having that established we can proceed with problem description. ### Problem https://github.com/user-attachments/assets/d0356bf5-2f17-4b06-ba53-bfca659a1071 <details> <summary>Full code</summary> ```javascript import { NavigationContainer } from 'react-navigation/native'; import React from 'react'; import { createNativeStackNavigator } from 'react-navigation/native-stack'; import { enableScreens } from 'react-native-screens'; import { StyleSheet, Text, View, FlatList, Button, ViewProps, Image, FlatListProps, findNodeHandle, } from 'react-native'; enableScreens(true); function Item({ children, ...props }: ViewProps) { return ( <View style={styles.item} {...props}> <Image source={require('../assets/trees.jpg')} style={styles.image} /> <Text style={styles.text}>{children}</Text> </View> ); } function Home({ navigation }: any) { return ( <View style={styles.container}> <Button title="Go to List" onPress={() => navigation.navigate('List')} /> </View> ); } function ListScreenSimplified({secondVisible}: {secondVisible?: (visible: boolean) => void}) { const containerRef = React.useRef<View>(null); const innerViewRef = React.useRef<View>(null); const childViewRef = React.useRef<View>(null); React.useEffect(() => { if (containerRef.current != null) { const tag = findNodeHandle(containerRef.current); console.log(`Container has tag [${tag}]`); } if (innerViewRef.current != null) { const tag = findNodeHandle(innerViewRef.current); console.log(`InnerView has tag [${tag}]`); } if (childViewRef.current != null) { const tag = findNodeHandle(childViewRef.current); console.log(`ChildView has tag [${tag}]`); } }, [containerRef.current, innerViewRef.current, childViewRef.current]); return ( <View ref={containerRef} style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }} removeClippedSubviews={false}> <View ref={innerViewRef} removeClippedSubviews style={{ height: '100%' }}> <View ref={childViewRef} style={{ backgroundColor: 'pink', width: '100%', height: 50 }} removeClippedSubviews={false}> {secondVisible && (<Button title='Hide second' onPress={() => secondVisible(false)} />)} </View> </View> </View> ); } function ParentFlatlist(props: Partial<FlatListProps<number>>) { return ( <FlatList data={Array.from({ length: 30 }).fill(0) as number[]} renderItem={({ index }) => { if (index === 10) { return <NestedFlatlist key={index} />; } else if (index === 15) { return <ExtraNestedFlatlist key={index} />; } else if (index === 20) { return <NestedFlatlist key={index} horizontal />; } else if (index === 25) { return <ExtraNestedFlatlist key={index} horizontal />; } else { return <Item key={index}>List item {index + 1}</Item>; } }} {...props} /> ); } function NestedFlatlist(props: Partial<FlatListProps<number>>) { return ( <FlatList style={[styles.nestedList, props.style]} data={Array.from({ length: 10 }).fill(0) as number[]} renderItem={({ index }) => ( <Item key={'nested' + index}>Nested list item {index + 1}</Item> )} {...props} /> ); } function ExtraNestedFlatlist(props: Partial<FlatListProps<number>>) { return ( <FlatList style={styles.nestedList} data={Array.from({ length: 10 }).fill(0) as number[]} renderItem={({ index }) => index === 4 ? ( <NestedFlatlist key={index} style={{ backgroundColor: '#d24729' }} /> ) : ( <Item key={'nested' + index}>Nested list item {index + 1}</Item> ) } {...props} /> ); } const Stack = createNativeStackNavigator(); export default function App(): React.JSX.Element { return ( <NavigationContainer> <Stack.Navigator screenOptions={{ animation: 'slide_from_right' }}> <Stack.Screen name="Home" component={Home} /> <Stack.Screen name="List" component={ListScreenSimplified}/> </Stack.Navigator> </NavigationContainer> ); } export function AppSimple(): React.JSX.Element { const [secondVisible, setSecondVisible] = React.useState(false); return ( <View style={{ flex: 1, backgroundColor: 'lightsalmon' }}> {!secondVisible && ( <View style={{ flex: 1, backgroundColor: 'lightblue' }} > <Button title='Show second' onPress={() => setSecondVisible(true)} /> </View> )} {secondVisible && ( <ListScreenSimplified secondVisible={setSecondVisible} /> )} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, nestedList: { backgroundColor: '#FFA07A', }, item: { flexDirection: 'row', alignItems: 'center', padding: 10, gap: 10, }, text: { fontSize: 24, fontWeight: 'bold', color: 'black', }, image: { width: 50, height: 50, }, }); ``` </details> Explanation (copied from [here](software-mansion/react-native-screens#2495 (comment))): I've debugged this for a while now & I have good understanding of what's going on. This bug is caused by our usage of `startViewTransition` and its implications. We use it well, however React does not account for case that some view might be in transition. Error mechanism is as follows: 1. Let's have initially simple stack with two screens: "A, B". This is component rendered under "B": ```javascript <View //<-- ContainerView (CV) removeClippedSubviews={false} style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }}> <View removeClippedSubviews style={{ height: '100%' }}> // <--- IntermediateView (IV) <View removeClippedSubviews={false} style={{ backgroundColor: 'pink', width: '100%', height: 50 }} /> // <--- ChildView (ChV) </View> </View> ``` 2. We press the back button. 3. We're on Fabric, therefore subtree of B gets destroyed before B itself is unmounted -> in our commit hook we detect that the screen B will be unmounted & we mark every node under B as transitioning by calling `startViewTransition`. 4. React Mounting stage starts, view hierarchy is disassembled in bottom-up fashion (leafs first). 5. ReactViewGroupManager receives MountItem to detach ChV from IV. 6. A call to [`IV.removeView(ChV)` is made](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt#L58-L73), which effectively removes ChV from `IV.children`, ***HOWEVER*** it does not clear `ChV.parent`, meaning that after the call, `ChV.parent == IV`. This happens, due to view being marked as in-transition by our call to `startViewTransition`. If the view is not marked as in-transition this parent-child relationship is removed. 7. IV has `removeClippedSubviews` enabled, therefore a [call to `IV.removeViewWithSubviewsClippingEnabled(ChV)` is made](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt#L68). [This function](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java#L726-L744) does effectively two things: 1. if the ChV has parent (interpretation: it has not yet been detached from parent), we compute it's index in `IV.children` (Android.ViewGroup's state) and remove it from the array, 2. remove the ChV from `mAllChildren` array (this is state maintained by ReactViewGroup for purposes of implementing the "subview clipping" mechanism". The crash happens in 7.1, because ChV has been removed from `IV.children` in step 6, but the parent-child relationship has not been broken up there. Under usual circumstances (this is my hypothesis now, yet unconfirmed) 7.1 does not execute, because `ChV.parent` is nulled in step no. 6. ### Rationale for `startViewTransition` usage Transitions. On Fabric, when some subtree is unmounted, views in the subtree are unmounted in bottom-up order. This leads to uncomfortable situation, where our components (react-native-screens), who want to drive & manage transitions are notified that their children will be removed after the subtrees mounted in screen subviews are already disassembled. **If we start animation in this very moment we will have staggering effect of white flash** [(issue)](software-mansion/react-native-screens#1685) (we animate just the screen with white background without it's children). This was not a problem on Paper, because the order of subtree disassembling was opposite - top-down. While we've managed to workaround this issue on Fabric using `MountingTransactionObserving` protocol on iOS and a commit hook on Android (we can inspect mutations in incoming transaction before it starts being applied) we still need to prevent view hierarchy from being disassembled in the middle of transition (on Paper this has also been less of an issue) - and this is where `startViewTransition` comes in. It allows us to draw views throughout transition after React Native removes them from HostTree model. On iOS we exchange subtree for its snapshot for transition time, however this approach isn't feasible on Android, because [snapshots do not capture shadows](https://stackoverflow.com/questions/42212600/android-screenshot-of-view-with-shadow). ### Possible solutions [Android does not expose a method to verify whether a view is in transition](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#7162) (it has `package` visibility), therefore we need to retrieve this information with some workaround. I see two posibilities: * first approach would be to override `startViewTransition` & `endViewTransition` in ReactViewGroup and keep the state on whether the view is transitioning there, * second possible approach would be as follows: we can check for "transitioning" view by checking whether a view has parent but is not it's parent child (this **should** be reliable), Having information on whether the view is in transition or not, we can prevent multiple removals of the same view in every call site (currently only in `removeViewAt` if `parent.removeClippingSubviews == true`). Another option would be to do just as this PR does: having in mind this "transitioning" state we can pass a flag to `removeViewWithSubviewClippingEnabled` and prevent duplicated removal from parent if we already know that this has been requested. I can also add override of this method: ```java /*package*/ void removeViewWithSubviewClippingEnabled(View view) { this.removeViewWithSubviewClippingEnabled(view, false); } ``` to make this parameter optional. ## Changelog: [ANDROID] [FIXED] - Handle removal of in-transition views. Pull Request resolved: #47634 Test Plan: WIP WIP Reviewed By: javache Differential Revision: D66539065 Pulled By: tdn120 fbshipit-source-id: cf1add67000ebd1b5dfdb2048461a55deac10b16
Summary: Related PR in `react-native-screens`: * software-mansion/react-native-screens#2495 Additional context: * [my detailed explanation of **one of the issues**](software-mansion/react-native-screens#2495 (comment)) * [Android Developer: ViewGroup.startViewTransition docs](https://developer.android.com/reference/android/view/ViewGroup#startViewTransition(android.view.View)) On Android view groups can be marked as "transitioning" with a `ViewGroup.startViewTransition` call. This effectively ensures, that in case a view group is marked with this call and its children are removed, they will be still drawn until `endViewTransition` is not called. This mechanism is implemented in Android by [keeping track of "transitioning" children in auxiliary `mTransitioningViews` array](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#7178). Then when such "transitioning" child is removed, [it is removed from children array](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#5595) but it's [parent-child relationship is not cleared](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#5397) and it is still retained in the auxiliary array. Having that established we can proceed with problem description. https://github.com/user-attachments/assets/d0356bf5-2f17-4b06-ba53-bfca659a1071 <details> <summary>Full code</summary> ```javascript import { NavigationContainer } from 'react-navigation/native'; import React from 'react'; import { createNativeStackNavigator } from 'react-navigation/native-stack'; import { enableScreens } from 'react-native-screens'; import { StyleSheet, Text, View, FlatList, Button, ViewProps, Image, FlatListProps, findNodeHandle, } from 'react-native'; enableScreens(true); function Item({ children, ...props }: ViewProps) { return ( <View style={styles.item} {...props}> <Image source={require('../assets/trees.jpg')} style={styles.image} /> <Text style={styles.text}>{children}</Text> </View> ); } function Home({ navigation }: any) { return ( <View style={styles.container}> <Button title="Go to List" onPress={() => navigation.navigate('List')} /> </View> ); } function ListScreenSimplified({secondVisible}: {secondVisible?: (visible: boolean) => void}) { const containerRef = React.useRef<View>(null); const innerViewRef = React.useRef<View>(null); const childViewRef = React.useRef<View>(null); React.useEffect(() => { if (containerRef.current != null) { const tag = findNodeHandle(containerRef.current); console.log(`Container has tag [${tag}]`); } if (innerViewRef.current != null) { const tag = findNodeHandle(innerViewRef.current); console.log(`InnerView has tag [${tag}]`); } if (childViewRef.current != null) { const tag = findNodeHandle(childViewRef.current); console.log(`ChildView has tag [${tag}]`); } }, [containerRef.current, innerViewRef.current, childViewRef.current]); return ( <View ref={containerRef} style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }} removeClippedSubviews={false}> <View ref={innerViewRef} removeClippedSubviews style={{ height: '100%' }}> <View ref={childViewRef} style={{ backgroundColor: 'pink', width: '100%', height: 50 }} removeClippedSubviews={false}> {secondVisible && (<Button title='Hide second' onPress={() => secondVisible(false)} />)} </View> </View> </View> ); } function ParentFlatlist(props: Partial<FlatListProps<number>>) { return ( <FlatList data={Array.from({ length: 30 }).fill(0) as number[]} renderItem={({ index }) => { if (index === 10) { return <NestedFlatlist key={index} />; } else if (index === 15) { return <ExtraNestedFlatlist key={index} />; } else if (index === 20) { return <NestedFlatlist key={index} horizontal />; } else if (index === 25) { return <ExtraNestedFlatlist key={index} horizontal />; } else { return <Item key={index}>List item {index + 1}</Item>; } }} {...props} /> ); } function NestedFlatlist(props: Partial<FlatListProps<number>>) { return ( <FlatList style={[styles.nestedList, props.style]} data={Array.from({ length: 10 }).fill(0) as number[]} renderItem={({ index }) => ( <Item key={'nested' + index}>Nested list item {index + 1}</Item> )} {...props} /> ); } function ExtraNestedFlatlist(props: Partial<FlatListProps<number>>) { return ( <FlatList style={styles.nestedList} data={Array.from({ length: 10 }).fill(0) as number[]} renderItem={({ index }) => index === 4 ? ( <NestedFlatlist key={index} style={{ backgroundColor: '#d24729' }} /> ) : ( <Item key={'nested' + index}>Nested list item {index + 1}</Item> ) } {...props} /> ); } const Stack = createNativeStackNavigator(); export default function App(): React.JSX.Element { return ( <NavigationContainer> <Stack.Navigator screenOptions={{ animation: 'slide_from_right' }}> <Stack.Screen name="Home" component={Home} /> <Stack.Screen name="List" component={ListScreenSimplified}/> </Stack.Navigator> </NavigationContainer> ); } export function AppSimple(): React.JSX.Element { const [secondVisible, setSecondVisible] = React.useState(false); return ( <View style={{ flex: 1, backgroundColor: 'lightsalmon' }}> {!secondVisible && ( <View style={{ flex: 1, backgroundColor: 'lightblue' }} > <Button title='Show second' onPress={() => setSecondVisible(true)} /> </View> )} {secondVisible && ( <ListScreenSimplified secondVisible={setSecondVisible} /> )} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, nestedList: { backgroundColor: '#FFA07A', }, item: { flexDirection: 'row', alignItems: 'center', padding: 10, gap: 10, }, text: { fontSize: 24, fontWeight: 'bold', color: 'black', }, image: { width: 50, height: 50, }, }); ``` </details> Explanation (copied from [here](software-mansion/react-native-screens#2495 (comment))): I've debugged this for a while now & I have good understanding of what's going on. This bug is caused by our usage of `startViewTransition` and its implications. We use it well, however React does not account for case that some view might be in transition. Error mechanism is as follows: 1. Let's have initially simple stack with two screens: "A, B". This is component rendered under "B": ```javascript <View //<-- ContainerView (CV) removeClippedSubviews={false} style={{ flex: 1, backgroundColor: 'slateblue', overflow: 'hidden' }}> <View removeClippedSubviews style={{ height: '100%' }}> // <--- IntermediateView (IV) <View removeClippedSubviews={false} style={{ backgroundColor: 'pink', width: '100%', height: 50 }} /> // <--- ChildView (ChV) </View> </View> ``` 2. We press the back button. 3. We're on Fabric, therefore subtree of B gets destroyed before B itself is unmounted -> in our commit hook we detect that the screen B will be unmounted & we mark every node under B as transitioning by calling `startViewTransition`. 4. React Mounting stage starts, view hierarchy is disassembled in bottom-up fashion (leafs first). 5. ReactViewGroupManager receives MountItem to detach ChV from IV. 6. A call to [`IV.removeView(ChV)` is made](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt#L58-L73), which effectively removes ChV from `IV.children`, ***HOWEVER*** it does not clear `ChV.parent`, meaning that after the call, `ChV.parent == IV`. This happens, due to view being marked as in-transition by our call to `startViewTransition`. If the view is not marked as in-transition this parent-child relationship is removed. 7. IV has `removeClippedSubviews` enabled, therefore a [call to `IV.removeViewWithSubviewsClippingEnabled(ChV)` is made](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactClippingViewManager.kt#L68). [This function](https://github.com/facebook/react-native/blob/9c11d7ca68c5c62ab7bab9919161d8417e96b28b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java#L726-L744) does effectively two things: 1. if the ChV has parent (interpretation: it has not yet been detached from parent), we compute it's index in `IV.children` (Android.ViewGroup's state) and remove it from the array, 2. remove the ChV from `mAllChildren` array (this is state maintained by ReactViewGroup for purposes of implementing the "subview clipping" mechanism". The crash happens in 7.1, because ChV has been removed from `IV.children` in step 6, but the parent-child relationship has not been broken up there. Under usual circumstances (this is my hypothesis now, yet unconfirmed) 7.1 does not execute, because `ChV.parent` is nulled in step no. 6. Transitions. On Fabric, when some subtree is unmounted, views in the subtree are unmounted in bottom-up order. This leads to uncomfortable situation, where our components (react-native-screens), who want to drive & manage transitions are notified that their children will be removed after the subtrees mounted in screen subviews are already disassembled. **If we start animation in this very moment we will have staggering effect of white flash** [(issue)](software-mansion/react-native-screens#1685) (we animate just the screen with white background without it's children). This was not a problem on Paper, because the order of subtree disassembling was opposite - top-down. While we've managed to workaround this issue on Fabric using `MountingTransactionObserving` protocol on iOS and a commit hook on Android (we can inspect mutations in incoming transaction before it starts being applied) we still need to prevent view hierarchy from being disassembled in the middle of transition (on Paper this has also been less of an issue) - and this is where `startViewTransition` comes in. It allows us to draw views throughout transition after React Native removes them from HostTree model. On iOS we exchange subtree for its snapshot for transition time, however this approach isn't feasible on Android, because [snapshots do not capture shadows](https://stackoverflow.com/questions/42212600/android-screenshot-of-view-with-shadow). [Android does not expose a method to verify whether a view is in transition](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java#7162) (it has `package` visibility), therefore we need to retrieve this information with some workaround. I see two posibilities: * first approach would be to override `startViewTransition` & `endViewTransition` in ReactViewGroup and keep the state on whether the view is transitioning there, * second possible approach would be as follows: we can check for "transitioning" view by checking whether a view has parent but is not it's parent child (this **should** be reliable), Having information on whether the view is in transition or not, we can prevent multiple removals of the same view in every call site (currently only in `removeViewAt` if `parent.removeClippingSubviews == true`). Another option would be to do just as this PR does: having in mind this "transitioning" state we can pass a flag to `removeViewWithSubviewClippingEnabled` and prevent duplicated removal from parent if we already know that this has been requested. I can also add override of this method: ```java /*package*/ void removeViewWithSubviewClippingEnabled(View view) { this.removeViewWithSubviewClippingEnabled(view, false); } ``` to make this parameter optional. [ANDROID] [FIXED] - Handle removal of in-transition views. Pull Request resolved: #47634 Test Plan: WIP WIP Reviewed By: javache Differential Revision: D66539065 Pulled By: tdn120 fbshipit-source-id: cf1add67000ebd1b5dfdb2048461a55deac10b16
Description
This PR intents to fix going back on fabric issue when using a
View
with removeClippedSubviews prop set to true.Previous bug fixes addressed this issue primarily for FlatLists, where it's set to true by default on Android. See #2383.
Additionally, this PR greatly improves the performance of
startTransitionRecursive
as it does not climb up the tree in search for a parent withremoveClippedSubview
set to true anymore.Fixes #2491 .
Changes
Test2282.tsx
reproTest code and steps to reproduce
Test2282.tsx
reproChecklist