Skip to content

Commit b2729ef

Browse files
lnikkilasoto-blvd
authored andcommitted
Fix view indices with Android LayoutAnimation (attempt 2) (facebook#19775)
Summary: /cc janicduplessis mdvacca This addresses the same issue as facebook#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](facebook#18830 (comment)) by enabling clipping in [this test app](facebook#18830 (review)) and adding a “clear views” button that resets the state to an empty items array with an animation. - facebook#18830 [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Pull Request resolved: facebook#19775 Differential Revision: D9105838 Pulled By: hramos fbshipit-source-id: 5ccb0957d1f46c36add960c0e4ef2a545cb03cbe
1 parent 47232e6 commit b2729ef

File tree

4 files changed

+85
-26
lines changed

4 files changed

+85
-26
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
@@ -453,12 +453,13 @@ public synchronized void manageChildren(
453453
if (mLayoutAnimationEnabled
454454
&& mLayoutAnimator.shouldAnimateLayout(viewToRemove)
455455
&& arrayContains(tagsToDelete, viewToRemove.getId())) {
456-
// The view will be removed and dropped by the 'delete' layout animation
457-
// instead, so do nothing
458-
} else {
459-
viewManager.removeViewAt(viewToManage, indexToRemove);
456+
// Display the view in the parent after removal for the duration of the layout animation,
457+
// but pretend that it doesn't exist when calling other ViewGroup methods.
458+
viewManager.startViewTransition(viewToManage, viewToRemove);
460459
}
461460

461+
viewManager.removeViewAt(viewToManage, indexToRemove);
462+
462463
lastIndexToRemove = indexToRemove;
463464
}
464465
}
@@ -487,7 +488,9 @@ public void onAnimationEnd() {
487488
// onAnimationEnd is called (indirectly) by Android View Animation.
488489
UiThreadUtil.assertOnUiThread();
489490

490-
viewManager.removeView(viewToManage, viewToDestroy);
491+
// Already removed from the ViewGroup, we can just end the transition here to
492+
// release the child.
493+
viewManager.endViewTransition(viewToManage, viewToDestroy);
491494
dropView(viewToDestroy);
492495
pendingDeletionTags.remove(viewToDestroy.getId());
493496
if (pendingDeletionTags.isEmpty()) {

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 deferring
98106
* to the standard css-layout algorithm. Returns true for the layout to *not* be automatically

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

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import com.facebook.react.uimanager.common.UIManagerType;
5555
import com.facebook.react.uimanager.common.ViewUtil;
5656
import com.facebook.yoga.YogaConstants;
57+
import java.util.ArrayList;
58+
import java.util.List;
5759

5860
/**
5961
* Backing for a React View. Has support for borders, but since borders aren't common, lazy
@@ -125,6 +127,7 @@ public void onLayoutChange(
125127
private @Nullable ChildrenLayoutChangeListener mChildrenLayoutChangeListener;
126128
private @Nullable ReactViewBackgroundDrawable mReactBackgroundDrawable;
127129
private @Nullable OnInterceptTouchEventListener mOnInterceptTouchEventListener;
130+
private @Nullable List<View> mTransitioningViews;
128131
private boolean mNeedsOffscreenAlphaCompositing = false;
129132
private @Nullable ViewGroupDrawingOrderHelper mDrawingOrderHelper = null;
130133
private @Nullable Path mPath;
@@ -340,18 +343,17 @@ public void updateClippingRect() {
340343

341344
private void updateClippingToRect(Rect clippingRect) {
342345
Assertions.assertNotNull(mAllChildren);
343-
int clippedSoFar = 0;
346+
int childIndexOffset = 0;
344347
for (int i = 0; i < mAllChildrenCount; i++) {
345-
updateSubviewClipStatus(clippingRect, i, clippedSoFar);
346-
if (mAllChildren[i].getParent() == null) {
347-
clippedSoFar++;
348+
updateSubviewClipStatus(clippingRect, i, childIndexOffset);
349+
if (!isChildInViewGroup(mAllChildren[i])) {
350+
childIndexOffset++;
348351
}
349352
}
350353
}
351354

352-
private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFar) {
355+
private void updateSubviewClipStatus(Rect clippingRect, int idx, int childIndexOffset) {
353356
UiThreadUtil.assertOnUiThread();
354-
355357
View child = Assertions.assertNotNull(mAllChildren)[idx];
356358
sHelperRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
357359
boolean intersects =
@@ -369,10 +371,10 @@ private void updateSubviewClipStatus(Rect clippingRect, int idx, int clippedSoFa
369371
if (!intersects && child.getParent() != null && !isAnimating) {
370372
// We can try saving on invalidate call here as the view that we remove is out of visible area
371373
// therefore invalidation is not necessary.
372-
super.removeViewsInLayout(idx - clippedSoFar, 1);
374+
super.removeViewsInLayout(idx - childIndexOffset, 1);
373375
needUpdateClippingRecursive = true;
374376
} else if (intersects && child.getParent() == null) {
375-
super.addViewInLayout(child, idx - clippedSoFar, sDefaultLayoutParam, true);
377+
super.addViewInLayout(child, idx - childIndexOffset, sDefaultLayoutParam, true);
376378
invalidate();
377379
needUpdateClippingRecursive = true;
378380
} else if (intersects) {
@@ -409,19 +411,25 @@ private void updateSubviewClipStatus(View subview) {
409411
boolean oldIntersects = (subview.getParent() != null);
410412

411413
if (intersects != oldIntersects) {
412-
int clippedSoFar = 0;
414+
int childIndexOffset = 0;
413415
for (int i = 0; i < mAllChildrenCount; i++) {
414416
if (mAllChildren[i] == subview) {
415-
updateSubviewClipStatus(mClippingRect, i, clippedSoFar);
417+
updateSubviewClipStatus(mClippingRect, i, childIndexOffset);
416418
break;
417419
}
418-
if (mAllChildren[i].getParent() == null) {
419-
clippedSoFar++;
420+
if (!isChildInViewGroup(mAllChildren[i])) {
421+
childIndexOffset++;
420422
}
421423
}
422424
}
423425
}
424426

427+
private boolean isChildInViewGroup(View view) {
428+
// A child is in the group if it's not clipped and it's not transitioning.
429+
return view.getParent() != null
430+
&& (mTransitioningViews == null || !mTransitioningViews.contains(view));
431+
}
432+
425433
@Override
426434
public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) {
427435
return super.getChildVisibleRect(child, r, offset);
@@ -562,13 +570,13 @@ protected void dispatchSetPressed(boolean pressed) {
562570
addInArray(child, index);
563571
// we add view as "clipped" and then run {@link #updateSubviewClipStatus} to conditionally
564572
// attach it
565-
int clippedSoFar = 0;
573+
int childIndexOffset = 0;
566574
for (int i = 0; i < index; i++) {
567-
if (mAllChildren[i].getParent() == null) {
568-
clippedSoFar++;
575+
if (!isChildInViewGroup(mAllChildren[i])) {
576+
childIndexOffset++;
569577
}
570578
}
571-
updateSubviewClipStatus(mClippingRect, index, clippedSoFar);
579+
updateSubviewClipStatus(mClippingRect, index, childIndexOffset);
572580
child.addOnLayoutChangeListener(mChildrenLayoutChangeListener);
573581

574582
if (child instanceof ReactClippingProhibitedView) {
@@ -603,14 +611,14 @@ public void run() {
603611
Assertions.assertNotNull(mAllChildren);
604612
view.removeOnLayoutChangeListener(mChildrenLayoutChangeListener);
605613
int index = indexOfChildInAllChildren(view);
606-
if (mAllChildren[index].getParent() != null) {
607-
int clippedSoFar = 0;
614+
if (isChildInViewGroup(mAllChildren[index])) {
615+
int childIndexOffset = 0;
608616
for (int i = 0; i < index; i++) {
609-
if (mAllChildren[i].getParent() == null) {
610-
clippedSoFar++;
617+
if (!isChildInViewGroup(mAllChildren[i])) {
618+
childIndexOffset++;
611619
}
612620
}
613-
super.removeViewsInLayout(index - clippedSoFar, 1);
621+
super.removeViewsInLayout(index - childIndexOffset, 1);
614622
}
615623
removeFromArray(index);
616624
}
@@ -625,6 +633,26 @@ public void run() {
625633
mAllChildrenCount = 0;
626634
}
627635

636+
/*package*/ void startViewTransitionWithSubviewClippingEnabled(View view) {
637+
// We're mirroring ViewGroup's mTransitioningViews since when a transitioning child is removed,
638+
// its parent is not set to null unlike a regular child. Normally this wouldn't be an issue as
639+
// ViewGroup pretends the transitioning child doesn't exist when calling any methods that expose
640+
// child views, but we keep track of our children directly when subview clipping is enabled and
641+
// need to be aware of these.
642+
if (mTransitioningViews == null) {
643+
mTransitioningViews = new ArrayList<>();
644+
}
645+
mTransitioningViews.add(view);
646+
startViewTransition(view);
647+
}
648+
649+
/*package*/ void endViewTransitionWithSubviewClippingEnabled(View view) {
650+
if (mTransitioningViews != null) {
651+
mTransitioningViews.remove(view);
652+
}
653+
endViewTransition(view);
654+
}
655+
628656
private int indexOfChildInAllChildren(View child) {
629657
final int count = mAllChildrenCount;
630658
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
@@ -342,4 +342,24 @@ private void handleHotspotUpdate(ReactViewGroup root, @Nullable ReadableArray ar
342342
float y = PixelUtil.toPixelFromDIP(args.getDouble(1));
343343
root.drawableHotspotChanged(x, y);
344344
}
345+
346+
@Override
347+
public void startViewTransition(ReactViewGroup parent, View view) {
348+
boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
349+
if (removeClippedSubviews) {
350+
parent.startViewTransitionWithSubviewClippingEnabled(view);
351+
} else {
352+
parent.startViewTransition(view);
353+
}
354+
}
355+
356+
@Override
357+
public void endViewTransition(ReactViewGroup parent, View view) {
358+
boolean removeClippedSubviews = parent.getRemoveClippedSubviews();
359+
if (removeClippedSubviews) {
360+
parent.endViewTransitionWithSubviewClippingEnabled(view);
361+
} else {
362+
parent.endViewTransition(view);
363+
}
364+
}
345365
}

0 commit comments

Comments
 (0)