diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 0d49abf79a05b6..e846052e09f4e7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -11,6 +11,7 @@ import android.os.Build; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.util.SparseIntArray; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -35,6 +36,8 @@ import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener; import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; +import java.util.HashMap; +import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @@ -73,6 +76,7 @@ public class NativeViewHierarchyManager { private final JSResponderHandler mJSResponderHandler = new JSResponderHandler(); private final RootViewManager mRootViewManager; private final LayoutAnimationController mLayoutAnimator = new LayoutAnimationController(); + private final Map mTagsToPendingIndicesToDelete = new HashMap<>(); private boolean mLayoutAnimationEnabled; private PopupMenu mPopupMenu; @@ -336,19 +340,49 @@ private static String constructManageChildrenErrorMessage( return stringBuilder.toString(); } + /** + * Given an index to action on under synchronous deletes, return an updated index factoring in + * asynchronous deletes (where the async delete operations have not yet been performed) + */ + private int normalizeIndex(int index, SparseIntArray pendingIndices) { + int normalizedIndex = index; + for (int i = 0; i <= index; i++) { + normalizedIndex += pendingIndices.get(i); + } + return normalizedIndex; + } + + /** + * Given React tag, return sparse array of direct child indices that are pending deletion (due to + * async view deletion) + */ + private SparseIntArray getOrCreatePendingIndicesToDelete(int tag) { + SparseIntArray pendingIndicesToDelete = mTagsToPendingIndicesToDelete.get(tag); + if (pendingIndicesToDelete == null) { + pendingIndicesToDelete = new SparseIntArray(); + mTagsToPendingIndicesToDelete.put(tag, pendingIndicesToDelete); + } + return pendingIndicesToDelete; + } + /** * @param tag react tag of the node we want to manage * @param indicesToRemove ordered (asc) list of indicies at which view should be removed * @param viewsToAdd ordered (asc based on mIndex property) list of tag-index pairs that represent * a view which should be added at the specified index * @param tagsToDelete list of tags corresponding to views that should be removed + * @param indicesToDelete parallel list to tagsToDelete, list of indices of those tags */ public synchronized void manageChildren( int tag, @Nullable int[] indicesToRemove, @Nullable ViewAtIndex[] viewsToAdd, - @Nullable int[] tagsToDelete) { + @Nullable int[] tagsToDelete, + @Nullable int[] indicesToDelete) { UiThreadUtil.assertOnUiThread(); + + final SparseIntArray pendingIndicesToDelete = getOrCreatePendingIndicesToDelete(tag); + final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag); final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag); if (viewToManage == null) { @@ -405,7 +439,8 @@ public synchronized void manageChildren( tagsToDelete)); } - View viewToRemove = viewManager.getChildAt(viewToManage, indexToRemove); + int normalizedIndexToRemove = normalizeIndex(indexToRemove, pendingIndicesToDelete); + View viewToRemove = viewManager.getChildAt(viewToManage, normalizedIndexToRemove); if (mLayoutAnimationEnabled && mLayoutAnimator.shouldAnimateLayout(viewToRemove) && @@ -413,7 +448,7 @@ public synchronized void manageChildren( // The view will be removed and dropped by the 'delete' layout animation // instead, so do nothing } else { - viewManager.removeViewAt(viewToManage, indexToRemove); + viewManager.removeViewAt(viewToManage, normalizedIndexToRemove); } lastIndexToRemove = indexToRemove; @@ -435,13 +470,15 @@ public synchronized void manageChildren( viewsToAdd, tagsToDelete)); } - viewManager.addView(viewToManage, viewToAdd, viewAtIndex.mIndex); + int normalizedIndexToAdd = normalizeIndex(viewAtIndex.mIndex, pendingIndicesToDelete); + viewManager.addView(viewToManage, viewToAdd, normalizedIndexToAdd); } } if (tagsToDelete != null) { for (int i = 0; i < tagsToDelete.length; i++) { int tagToDelete = tagsToDelete[i]; + final int indexToDelete = indicesToDelete[i]; final View viewToDestroy = mTagsToViews.get(tagToDelete); if (viewToDestroy == null) { throw new IllegalViewOperationException( @@ -457,13 +494,20 @@ public synchronized void manageChildren( if (mLayoutAnimationEnabled && mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) { - mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() { - @Override - public void onAnimationEnd() { - viewManager.removeView(viewToManage, viewToDestroy); - dropView(viewToDestroy); - } - }); + int updatedCount = pendingIndicesToDelete.get(indexToDelete, 0) + 1; + pendingIndicesToDelete.put(indexToDelete, updatedCount); + mLayoutAnimator.deleteView( + viewToDestroy, + new LayoutAnimationListener() { + @Override + public void onAnimationEnd() { + viewManager.removeView(viewToManage, viewToDestroy); + dropView(viewToDestroy); + + int count = pendingIndicesToDelete.get(indexToDelete, 0); + pendingIndicesToDelete.put(indexToDelete, Math.max(0, count - 1)); + } + }); } else { dropView(viewToDestroy); } @@ -580,6 +624,7 @@ protected synchronized void dropView(View view) { } viewGroupManager.removeAllViews(viewGroup); } + mTagsToPendingIndicesToDelete.remove(view.getId()); mTagsToViews.remove(view.getId()); mTagsToViewManagers.remove(view.getId()); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java index 084006fe4651cf..f371f7ed0a9a8c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java @@ -145,13 +145,15 @@ public void handleManageChildren( int[] indicesToRemove, int[] tagsToRemove, ViewAtIndex[] viewsToAdd, - int[] tagsToDelete) { + int[] tagsToDelete, + int[] indicesToDelete) { if (!ENABLED) { mUIViewOperationQueue.enqueueManageChildren( nodeToManage.getReactTag(), indicesToRemove, viewsToAdd, - tagsToDelete); + tagsToDelete, + indicesToDelete); return; } @@ -276,9 +278,10 @@ private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDe mUIViewOperationQueue.enqueueManageChildren( nativeNodeToRemoveFrom.getReactTag(), - new int[]{index}, + new int[] {index}, null, - shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null); + shouldDelete ? new int[] {nodeToRemove.getReactTag()} : null, + shouldDelete ? new int[] {index} : null); } else { for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) { removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete); @@ -301,7 +304,8 @@ private void addNonLayoutNode( mUIViewOperationQueue.enqueueManageChildren( parent.getReactTag(), null, - new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)}, + new ViewAtIndex[] {new ViewAtIndex(child.getReactTag(), index)}, + null, null); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index d9791957b70477..18ff42605f33f6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -347,6 +347,7 @@ public void manageChildren( int[] indicesToRemove = new int[numToMove + numToRemove]; int[] tagsToRemove = new int[indicesToRemove.length]; int[] tagsToDelete = new int[numToRemove]; + int[] indicesToDelete = new int[numToRemove]; if (numToMove > 0) { Assertions.assertNotNull(moveFrom); @@ -380,6 +381,7 @@ public void manageChildren( indicesToRemove[numToMove + i] = indexToRemove; tagsToRemove[numToMove + i] = tagToRemove; tagsToDelete[i] = tagToRemove; + indicesToDelete[i] = indexToRemove; } } @@ -423,7 +425,8 @@ public void manageChildren( indicesToRemove, tagsToRemove, viewsToAdd, - tagsToDelete); + tagsToDelete, + indicesToDelete); } for (int i = 0; i < tagsToDelete.length; i++) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index 9b86f083e2cdbe..4d84d7545c03ef 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -210,16 +210,19 @@ private final class ManageChildrenOperation extends ViewOperation { private final @Nullable int[] mIndicesToRemove; private final @Nullable ViewAtIndex[] mViewsToAdd; private final @Nullable int[] mTagsToDelete; + private final @Nullable int[] mIndicesToDelete; public ManageChildrenOperation( int tag, @Nullable int[] indicesToRemove, @Nullable ViewAtIndex[] viewsToAdd, - @Nullable int[] tagsToDelete) { + @Nullable int[] tagsToDelete, + @Nullable int[] indicesToDelete) { super(tag); mIndicesToRemove = indicesToRemove; mViewsToAdd = viewsToAdd; mTagsToDelete = tagsToDelete; + mIndicesToDelete = indicesToDelete; } @Override @@ -228,7 +231,8 @@ public void execute() { mTag, mIndicesToRemove, mViewsToAdd, - mTagsToDelete); + mTagsToDelete, + mIndicesToDelete); } } @@ -778,9 +782,10 @@ public void enqueueManageChildren( int reactTag, @Nullable int[] indicesToRemove, @Nullable ViewAtIndex[] viewsToAdd, - @Nullable int[] tagsToDelete) { + @Nullable int[] tagsToDelete, + @Nullable int[] indicesToDelete) { mOperations.add( - new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete)); + new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete, indicesToDelete)); } public void enqueueSetChildren(