Skip to content

Commit 1f88a71

Browse files
lnikkilafacebook-github-bot
authored andcommitted
Fix view indices with Android LayoutAnimation (attempt 2) (#19775)
Summary: /cc janicduplessis mdvacca This addresses the same issue as #18830 which was reverted since it didn’t handle `removeClippedSubviews` properly. When subview clipping is on, ReactViewGroup keeps track of its own children directly and accounts for the offset introduced by clipped views when calling ViewGroup methods that modify children. Instead of accounting for just clipped children (views with no parent), it should account for any children that aren’t in the ViewGroup which also includes children that are being transitioned. If you look at the ViewGroup source code, [it explicitly retains the view parent until the transition finishes](https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/core/java/android/view/ViewGroup.java#L5034-L5036) which caused the `getParent()` checks to pass, even though those views should be ignored. I added a new `isChildInViewGroup` method that handles both clipped and transitioning views to fix this. I reproduced the [earlier crash](#18830 (comment)) by enabling clipping in [this test app](#18830 (review)) and adding a “clear views” button that resets the state to an empty items array with an animation. - #18830 [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Pull Request resolved: #19775 Differential Revision: D9105838 Pulled By: hramos fbshipit-source-id: 5ccb0957d1f46c36add960c0e4ef2a545cb03cbe
1 parent cbfe159 commit 1f88a71

File tree

4 files changed

+85
-25
lines changed

4 files changed

+85
-25
lines changed

ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,13 @@ public synchronized void manageChildren(
409409
if (mLayoutAnimationEnabled &&
410410
mLayoutAnimator.shouldAnimateLayout(viewToRemove) &&
411411
arrayContains(tagsToDelete, viewToRemove.getId())) {
412-
// The view will be removed and dropped by the 'delete' layout animation
413-
// instead, so do nothing
414-
} else {
415-
viewManager.removeViewAt(viewToManage, indexToRemove);
412+
// Display the view in the parent after removal for the duration of the layout animation,
413+
// but pretend that it doesn't exist when calling other ViewGroup methods.
414+
viewManager.startViewTransition(viewToManage, viewToRemove);
416415
}
417416

417+
viewManager.removeViewAt(viewToManage, indexToRemove);
418+
418419
lastIndexToRemove = indexToRemove;
419420
}
420421
}
@@ -459,7 +460,9 @@ public synchronized void manageChildren(
459460
mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() {
460461
@Override
461462
public void onAnimationEnd() {
462-
viewManager.removeView(viewToManage, viewToDestroy);
463+
// Already removed from the ViewGroup, we can just end the transition here to
464+
// release the child.
465+
viewManager.endViewTransition(viewToManage, viewToDestroy);
463466
dropView(viewToDestroy);
464467
}
465468
});

ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ public void removeAllViews(T parent) {
9393
}
9494
}
9595

96+
public void startViewTransition(T parent, View view) {
97+
parent.startViewTransition(view);
98+
}
99+
100+
public void endViewTransition(T parent, View view) {
101+
parent.endViewTransition(view);
102+
}
103+
96104
/**
97105
* Returns whether this View type needs to handle laying out its own children instead of
98106
* deferring to the standard css-layout algorithm.

ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper;
4040
import com.facebook.react.uimanager.ViewProps;
4141
import com.facebook.yoga.YogaConstants;
42+
import java.util.ArrayList;
43+
import java.util.List;
4244
import javax.annotation.Nullable;
4345

4446
/**
@@ -106,6 +108,7 @@ public void onLayoutChange(
106108
private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener;
107109
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
108110
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
111+
private @Nullable List<View> mTransitioningViews;
109112
private boolean mNeedsOffscreenAlphaCompositing = false;
110113
private final ViewGroupDrawingOrderHelper mDrawingOrderHelper;
111114
private @Nullable Path mPath;
@@ -334,16 +337,16 @@ public void updateClippingRect() {
334337

335338
private void updateClippingToRect(Rect clippingRect) {
336339
Assertions.assertNotNull(mAllChildren);
337-
int clippedSoFar = 0;
340+
int childIndexOffset = 0;
338341
for (int i = 0; i < mAllChildrenCount; i++) {
339-
updateSubviewClipStatus(clippingRect, i, clippedSoFar);
340-
if (mAllChildren[i].getParent() == null) {
341-
clippedSoFar++;
342+
updateSubviewClipStatus(clippingRect, i, childIndexOffset);
343+
if (!isChildInViewGroup(mAllChildren[i])) {
344+
childIndexOffset++;
342345
}
343346
}
344347
}
345348

346-
private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) {
349+
private void updateSubviewClipStatus(Rect clippingRect, int idx, int childIndexOffset) {
347350
View child = Assertions.assertNotNull(mAllChildren)[idx];
348351
sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
349352
boolean intersects = clippingRect
@@ -360,10 +363,10 @@ private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFa
360363
if (!intersects && child.getParent() != null && !isAnimating) {
361364
// We can try saving on invalidate call here as the view that we remove is out of visible area
362365
// therefore invalidation is not necessary.
363-
super.removeViewsInLayout(idx - clippedSoFar, 1);
366+
super.removeViewsInLayout(idx - childIndexOffset, 1);
364367
needUpdateClippingRecursive = true;
365368
} else if (intersects && child.getParent() == null) {
366-
super.addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true);
369+
super.addViewInLayout(child, idx - childIndexOffset, sDefaultLayoutParam, true);
367370
invalidate();
368371
needUpdateClippingRecursive = true;
369372
} else if (intersects) {
@@ -399,19 +402,25 @@ private void updateSubviewClipStatus(View subview) {
399402
boolean oldIntersects = (subview.getParent() != null);
400403

401404
if (intersects != oldIntersects) {
402-
int clippedSoFar = 0;
405+
int childIndexOffset = 0;
403406
for (int i = 0; i < mAllChildrenCount; i++) {
404407
if (mAllChildren[i] == subview) {
405-
updateSubviewClipStatus(mClippingRect, i, clippedSoFar);
408+
updateSubviewClipStatus(mClippingRect, i, childIndexOffset);
406409
break;
407410
}
408-
if (mAllChildren[i].getParent() == null) {
409-
clippedSoFar++;
411+
if (!isChildInViewGroup(mAllChildren[i])) {
412+
childIndexOffset++;
410413
}
411414
}
412415
}
413416
}
414417

418+
private boolean isChildInViewGroup(View view) {
419+
// A child is in the group if it's not clipped and it's not transitioning.
420+
return view.getParent() != null
421+
&& (mTransitioningViews == null || !mTransitioningViews.contains(view));
422+
}
423+
415424
@Override
416425
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
417426
super.onSizeChanged(w, h, oldw, oldh);
@@ -509,13 +518,13 @@ protected void dispatchSetPressed(boolean pressed) {
509518
addInArray(child, index);
510519
// we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally
511520
// attach it
512-
int clippedSoFar = 0;
521+
int childIndexOffset = 0;
513522
for (int i = 0; i < index; i++) {
514-
if (mAllChildren[i].getParent() == null) {
515-
clippedSoFar++;
523+
if (!isChildInViewGroup(mAllChildren[i])) {
524+
childIndexOffset++;
516525
}
517526
}
518-
updateSubviewClipStatus(mClippingRect, index, clippedSoFar);
527+
updateSubviewClipStatus(mClippingRect, index, childIndexOffset);
519528
child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
520529
}
521530

@@ -525,14 +534,14 @@ protected void dispatchSetPressed(boolean pressed) {
525534
Assertions.assertNotNull(mAllChildren);
526535
view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
527536
int index = indexOfChildInAllChildren(view);
528-
if (mAllChildren[index].getParent() != null) {
529-
int clippedSoFar = 0;
537+
if (isChildInViewGroup(mAllChildren[index])) {
538+
int childIndexOffset = 0;
530539
for (int i = 0; i < index; i++) {
531-
if (mAllChildren[i].getParent() == null) {
532-
clippedSoFar++;
540+
if (!isChildInViewGroup(mAllChildren[i])) {
541+
childIndexOffset++;
533542
}
534543
}
535-
super.removeViewsInLayout(index - clippedSoFar, 1);
544+
super.removeViewsInLayout(index - childIndexOffset, 1);
536545
}
537546
removeFromArray(index);
538547
}
@@ -547,6 +556,26 @@ protected void dispatchSetPressed(boolean pressed) {
547556
mAllChildrenCount = 0;
548557
}
549558

559+
/*package*/ void startViewTransitionWithSubviewClippingEnabled(View view) {
560+
// We're mirroring ViewGroup's mTransitioningViews since when a transitioning child is removed,
561+
// its parent is not set to null unlike a regular child. Normally this wouldn't be an issue as
562+
// ViewGroup pretends the transitioning child doesn't exist when calling any methods that expose
563+
// child views, but we keep track of our children directly when subview clipping is enabled and
564+
// need to be aware of these.
565+
if (mTransitioningViews == null) {
566+
mTransitioningViews = new ArrayList<>();
567+
}
568+
mTransitioningViews.add(view);
569+
startViewTransition(view);
570+
}
571+
572+
/*package*/ void endViewTransitionWithSubviewClippingEnabled(View view) {
573+
if (mTransitioningViews != null) {
574+
mTransitioningViews.remove(view);
575+
}
576+
endViewTransition(view);
577+
}
578+
550579
private int indexOfChildInAllChildren(View child) {
551580
final int count = mAllChildrenCount;
552581
final View[] children = Assertions.assertNotNull(mAllChildren);

ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,24 @@ public void removeAllViews(ReactViewGroup parent) {
297297
parent.removeAllViews();
298298
}
299299
}
300+
301+
@Override
302+
public void startViewTransition(ReactViewGroup parent, View view) {
303+
boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
304+
if (removeClippedSubviews) {
305+
parent.startViewTransitionWithSubviewClippingEnabled(view);
306+
} else {
307+
parent.startViewTransition(view);
308+
}
309+
}
310+
311+
@Override
312+
public void endViewTransition(ReactViewGroup parent, View view) {
313+
boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
314+
if (removeClippedSubviews) {
315+
parent.endViewTransitionWithSubviewClippingEnabled(view);
316+
} else {
317+
parent.endViewTransition(view);
318+
}
319+
}
300320
}

0 commit comments

Comments
 (0)