Open kkafar opened 1 day ago
This is interesting! It looks like there could certainly be issues with the current code largely ignoring transitions.
The current commit looks a little too simple, since this is the only call to removeViewWithSubviewClippingEnabled()
in the library. So we'd effectively never be running the subview clipping logic on view removal.
The "first approach" seems like it should work, as long as the implementation is careful not to hold onto views after they're otherwise removed.
I'm not quite clear on how you envision the second approach - where/how would you be checking parent vs. child?
Summary:
Related PR in
react-native-screens
:Additional context:
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 untilendViewTransition
is not called.This mechanism is implemented in Android by keeping track of "transitioning" children in auxiliary
mTransitioningViews
array. Then when such "transitioning" child is removed, it is removed from children array but it's parent-child relationship is not cleared 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
Full code
```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 (Explanation (copied from here):
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:startViewTransition
.IV.removeView(ChV)
is made, which effectively removes ChV fromIV.children
, HOWEVER it does not clearChV.parent
, meaning that after the call,ChV.parent == IV
. This happens, due to view being marked as in-transition by our call tostartViewTransition
. If the view is not marked as in-transition this parent-child relationship is removed.removeClippedSubviews
enabled, therefore a call toIV.removeViewWithSubviewsClippingEnabled(ChV)
is made. This function does effectively two things:IV.children
(Android.ViewGroup's state) and remove it from the array,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, becauseChV.parent
is nulled in step no. 6.Rationale for
startViewTransition
usageTransitions. 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) (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 wherestartViewTransition
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.Possible solutions
Android does not expose a method to verify whether a view is in transition (it has
package
visibility), therefore we need to retrieve this information with some workaround. I see two posibilities:startViewTransition
&endViewTransition
in ReactViewGroup and keep the state on whether the view is transitioning there,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
ifparent.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:
to make this parameter optional.
Changelog:
[ANDROID] [FIXED] - Handle removal of in-transition views.
Test Plan:
WIP WIP