Skip to content
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 handling removal of transitioning views #47634

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ public abstract class ReactClippingViewManager<T : ReactViewGroup> : ViewGroupMa
if (removeClippedSubviews) {
val child = getChildAt(parent, index)
if (child != null) {
if (child.parent != null) {
parent.removeView(child)
}
Comment on lines -65 to -67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is really interesting, and may explain a crash we're seeing. I wonder why we were previously removing this subview twice - potentially messing up some state in ReactViewGroup.

What's your reasoning for removing this?

Copy link
Contributor Author

@kkafar kkafar Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javache thanks for review. My logic was as follows:

before 0b22b95 you relied on handleRemoveView being called to perform side effects regarding child drawing order. Implementation of ReactViewgroup.removeView took care of handling these sideeffects and detaching child view from parent.

On the other hand ReactViewGroup.removeViewWithSubviewClippingEnabled had also two responsibilities:

  1. detach child from parent if it has not been detached yet (notice the guard here!)
  2. remove child from mAllChildren array, which also retained children already detached due to clipping.

Notice, that it had not performed handleRemoveView.

My theory is that in clipping view manager you had call to parent.removeView(child) to ensure sideeffects of handleRemoveView were performed and then you called parent.removeViewWithSubviewClippingEnabled(child) to remove child from mAllChildren array, relying on the guard from (1), which prevented Android crash potentially caused by removing twice the same child view.

After 0b22b95 side effects of handleRemoveView have been moved to onViewRemoved which is called on any attempt of child removal by the Android framework. Therefore we can remove the call to parent.removeView as all required side effects will be performed by the call to parent.removeViewWithSubviewClippingEnabled(child) - drawing order code, removal from mAllChildren and child detachment. I've left the check for nullish child, as I couldn't determine what was the logic behind it.

parent.removeViewWithSubviewClippingEnabled(child)
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
import java.util.HashSet;
import java.util.Set;

import java.util.HashSet;
import java.util.Set;

kkafar marked this conversation as resolved.
Show resolved Hide resolved
/**
* Backing for a React View. Has support for borders, but since borders aren't common, lazy
* initializes most of the storage needed for them.
Expand Down Expand Up @@ -137,6 +140,8 @@ public void shutdown() {
private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper;
private float mBackfaceOpacity;
private String mBackfaceVisibility;
private boolean mIsTransitioning = false;
private @Nullable Set<Integer> mChildrenRemovedWhileTransitioning = null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using Set<Integer> (holding only the view tag) instead of Set<View> to avoid any retain issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using androidx.collection.MutableIntSet instead to avoid Integer boxing.

Copy link
Contributor Author

@kkafar kkafar Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I was not aware of this API! Hover it seems that it has been added few months ago in androidx.collection.MutableIntSet 1.4.0, while my development project (using recent react-native 0.76) uses version 1.1.0. I'm not sure where would I need to bump / add this dependency in RN to make use of it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this would need to be a new dependency in https://github.com/facebook/react-native/blob/main/packages/react-native/gradle/libs.versions.toml#L45 - let's add a comment to change this in the future.

kkafar marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a new `ReactViewGroup` instance.
Expand Down Expand Up @@ -405,6 +410,28 @@ public void updateClippingRect() {
updateClippingToRect(mClippingRect);
}

@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mIsTransitioning = true;
}

@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
mIsTransitioning = false;
if (mChildrenRemovedWhileTransitioning != null) {
mChildrenRemovedWhileTransitioning = null;
}
kkafar marked this conversation as resolved.
Show resolved Hide resolved
}

public Set<Integer> ensureChildrenRemovedWhileTransitioning() {
kkafar marked this conversation as resolved.
Show resolved Hide resolved
if (mChildrenRemovedWhileTransitioning == null) {
mChildrenRemovedWhileTransitioning = new HashSet<>();
}
return mChildrenRemovedWhileTransitioning;
}

private void updateClippingToRect(Rect clippingRect) {
Assertions.assertNotNull(mAllChildren);
int clippedSoFar = 0;
Expand Down Expand Up @@ -564,6 +591,11 @@ public void onViewRemoved(View child) {
} else {
setChildrenDrawingOrderEnabled(false);
}

if (mIsTransitioning) {
ensureChildrenRemovedWhileTransitioning().add(child.getId());
}

super.onViewRemoved(child);
}

Expand Down Expand Up @@ -679,11 +711,12 @@ public void run() {

/*package*/ void removeViewWithSubviewClippingEnabled(View view) {
UiThreadUtil.assertOnUiThread();

Assertions.assertCondition(mRemoveClippedSubviews);
Assertions.assertNotNull(mClippingRect);
View[] childArray = Assertions.assertNotNull(mAllChildren);

view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener);

kkafar marked this conversation as resolved.
Show resolved Hide resolved
int index = indexOfChildInAllChildren(view);
if (!isViewClipped(childArray[index])) {
int clippedSoFar = 0;
Expand Down Expand Up @@ -712,7 +745,8 @@ public void run() {
*/
private boolean isViewClipped(View view) {
ViewParent parent = view.getParent();
if (parent == null) {

if (parent == null || (mChildrenRemovedWhileTransitioning != null && mChildrenRemovedWhileTransitioning.contains(view.getId()))) {
return true;
} else {
Assertions.assertCondition(parent == this);
Expand Down
Loading