Repository navigation
Conversation
A parent that holds a child with `ViewGroup.startViewTransition` keeps `child.parent` after the child leaves its child list. When Fabric then inserts that view into another parent, `addViewAt` sees a stale parent. Its recovery calls `removeView`, which does nothing for a view that is no longer listed, so `addView` throws "failed to insert view". The recovery also marks the tag as re-added, so a later Remove of that tag is skipped. React Native Screens holds the content of a removed screen this way. Reanimated shared element transitions move the source view out of such a screen into the surface root, and crash on Android when going back. Before the recovery, end the view transition when the old parent no longer lists the view, then read the parent again. If it is cleared, insert the view normally. If not, keep the existing recovery. Holds of removed views that are not inserted again stay in place.
| endViewTransitionIfRemovedFromParent(view) | ||
| val currParentView = view.parent | ||
| if (currParentView != null) { | ||
| val actualParentId = if (currParentView is ViewGroup) currParentView.id else View.NO_ID |
There was a problem hiding this comment.
Can you add this logic inline here, so we can avoid doing the same null / class checks?
There was a problem hiding this comment.
Thanks for the review! I did inline it in d7addde. As a result view.parent is now read once, and read again only after endViewTransition. Is this what you had in mind here? I could also potentially fold the two older is ViewGroup checks in the recovery block onto one as? ViewGroup value, so the parent's class is checked only once if you prefer
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D129333127. |
mangeshraut712
left a comment
There was a problem hiding this comment.
In SurfaceMountingManager.addViewAt (SurfaceMountingManager.kt ~339–341), a held child is released with a single endViewTransition(view) when indexOfChild is -1.
ViewGroup.startViewTransition can be nested: each call appends the view to mTransitioningViews, and endViewTransition only removes one occurrence. The new nested test (addViewAt_movesNestedHeldChildren...) calls startViewTransition twice per parent via repeat(2) before removeViewAt, then relies on this one endViewTransition during addViewAt.
On a real ViewGroup that still leaves a hold, view.parent can stay non-null after the move/remove path and the “final removal” assertions can fail outside Robolectric. Loop endViewTransition while indexOfChild(view) == -1 && parent === currParentView (or while the view remains in the transitioning set), matching the number of outstanding starts.
Summary:
On Android,
SurfaceMountingManager.addViewAtfails when it inserts a view that a parent still holds withViewGroup.startViewTransition.A held child can leave its old parent's child list while
child.parentstill points to that old parent:The existing recovery in
addViewAtcallsremoveViewon the old parent. That does nothing for a view that is no longer in the child list, soaddViewthrowsaddViewAt: failed to insert view ... into parent. The recovery also adds the tag toerroneouslyReaddedReactTags, so a later Remove of that tag returns early and the view is never removed.React Native Screens holds the content of a removed screen this way during its exit animation (
Screen.startRemovalTransition,NativeProxy.notifyScreenRemoved).Reanimated shared element transitions will soon move the source view out of such a screen into the surface root (react-native-reanimated#11054), and the app crashes when going back.
Analysis: comment on #11054.
Reanimated is getting a workaround that releases the holds before the mount batch (react-native-reanimated#11101), but
addViewAtis the better place for the fix.The fix
Before the existing recovery, if the old parent no longer lists the view, call
endViewTransitionon it, then readview.parentagain. If the parent is cleared, the view is inserted normally and the tag is not marked as re-added. If the parent is still set, the existing recovery and its diagnostics run as before.endViewTransitionclears the parent when it removes the view from the disappearing children.ReactViewGroup.endViewTransitionalso clears its ownchildrenRemovedWhileTransitioningentry, so RN's clipping state stays in sync.Changelog:
[ANDROID] [FIXED] - Fix
addViewAtcrash when a view that a parent still holds withstartViewTransitionis inserted into another parentTest Plan:
New Robolectric tests in
SurfaceMountingManagerViewTransitionTest, with the realSurfaceMountingManagerand real AndroidViewGroups:./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \ --tests 'com.facebook.react.fabric.SurfaceMountingManagerViewTransitionTest'addViewAt: failed to insert view [12] into parent [1] at index 1... into parent [10] at index 0Other checks:
Device test: Reanimated
apps/fabric-example(branch of #11054, without #11101), Android emulator, Debug,--active-arch-only, React Native built from source. The example app does not build againstmain(it needs RN 0.86 to 0.88), so this usedv0.88.0with only thisSurfaceMountingManager.ktchange applied.addViewAt: failed to insert view [666] into parent [1] at index 3. The specified child already has a parent.addViewAtor "already has a parent" lines in logcat, and no shared view is left on the root after the pop.Recordings
Before:
before-fade-content-tap-back.mp4
After:
after-fade-content-tap-back.mp4