diff --git a/Examples/UIExplorer/UIExplorerApp.android.js b/Examples/UIExplorer/UIExplorerApp.android.js index 3553b812d..bd9f03ab4 100644 --- a/Examples/UIExplorer/UIExplorerApp.android.js +++ b/Examples/UIExplorer/UIExplorerApp.android.js @@ -37,9 +37,12 @@ const UIExplorerExampleList = require('./UIExplorerExampleList'); const UIExplorerList = require('./UIExplorerList'); const UIExplorerNavigationReducer = require('./UIExplorerNavigationReducer'); const UIExplorerStateTitleMap = require('./UIExplorerStateTitleMap'); +const UIManager = require('UIManager'); const URIActionMap = require('./URIActionMap'); const View = require('View'); +UIManager.setLayoutAnimationEnabledExperimental(true); + const DRAWER_WIDTH_LEFT = 56; type Props = { 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 eeeb4c11e..36ab83c8f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -35,6 +35,7 @@ import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.touch.JSResponderHandler; import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController; +import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener; import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; @@ -294,8 +295,8 @@ public class NativeViewHierarchyManager { @Nullable int[] indicesToRemove, @Nullable ViewAtIndex[] viewsToAdd, @Nullable int[] tagsToDelete) { - ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag); - ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag); + final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag); + final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag); if (viewToManage == null) { throw new IllegalViewOperationException("Trying to manageChildren view with tag " + tag + " which doesn't exist\n detail: " + @@ -344,7 +345,17 @@ public class NativeViewHierarchyManager { viewsToAdd, tagsToDelete)); } - viewManager.removeViewAt(viewToManage, indicesToRemove[i]); + + View viewToRemove = viewToManage.getChildAt(indexToRemove); + + if (mLayoutAnimator.shouldAnimateLayout(viewToRemove) && + arrayContains(tagsToDelete, viewToRemove.getId())) { + // The view will be removed and dropped by the 'delete' layout animation + // instead, so do nothing + } else { + viewManager.removeViewAt(viewToManage, indexToRemove); + } + lastIndexToRemove = indexToRemove; } } @@ -371,7 +382,7 @@ public class NativeViewHierarchyManager { if (tagsToDelete != null) { for (int i = 0; i < tagsToDelete.length; i++) { int tagToDelete = tagsToDelete[i]; - View viewToDestroy = mTagsToViews.get(tagToDelete); + final View viewToDestroy = mTagsToViews.get(tagToDelete); if (viewToDestroy == null) { throw new IllegalViewOperationException( "Trying to destroy unknown view tag: " @@ -383,11 +394,31 @@ public class NativeViewHierarchyManager { viewsToAdd, tagsToDelete)); } - dropView(viewToDestroy); + + if (mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) { + mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() { + @Override + public void onAnimationEnd() { + viewManager.removeView(viewToManage, viewToDestroy); + dropView(viewToDestroy); + } + }); + } else { + dropView(viewToDestroy); + } } } } + private boolean arrayContains(int[] array, int ele) { + for (int curEle : array) { + if (curEle == ele) { + return true; + } + } + return false; + } + /** * Simplified version of constructManageChildrenErrorMessage that only deals with adding children * views diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java index 153f7b815..e91be0e20 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewGroupManager.java @@ -48,6 +48,15 @@ public abstract class ViewGroupManager parent.removeViewAt(index); } + public void removeView(T parent, View view) { + for (int i = 0; i < getChildCount(parent); i++) { + if (getChildAt(parent, i) == view) { + removeViewAt(parent, i); + break; + } + } + } + public void removeAllViews(T parent) { for (int i = getChildCount(parent) - 1; i >= 0; i--) { removeViewAt(parent, i); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java index d3522947f..d5a08e6b2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java @@ -6,6 +6,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import android.view.View; +import android.view.ViewGroup; import android.view.animation.Animation; import com.facebook.react.bridge.ReadableMap; @@ -25,6 +26,7 @@ public class LayoutAnimationController { private final AbstractLayoutAnimation mLayoutCreateAnimation = new LayoutCreateAnimation(); private final AbstractLayoutAnimation mLayoutUpdateAnimation = new LayoutUpdateAnimation(); + private final AbstractLayoutAnimation mLayoutDeleteAnimation = new LayoutDeleteAnimation(); private boolean mShouldAnimateLayout; public void initializeFromConfig(final @Nullable ReadableMap config) { @@ -49,11 +51,17 @@ public class LayoutAnimationController { config.getMap(LayoutAnimationType.UPDATE.toString()), globalDuration); mShouldAnimateLayout = true; } + if (config.hasKey(LayoutAnimationType.DELETE.toString())) { + mLayoutDeleteAnimation.initializeFromConfig( + config.getMap(LayoutAnimationType.DELETE.toString()), globalDuration); + mShouldAnimateLayout = true; + } } public void reset() { mLayoutCreateAnimation.reset(); mLayoutUpdateAnimation.reset(); + mLayoutDeleteAnimation.reset(); mShouldAnimateLayout = false; } @@ -65,7 +73,8 @@ public class LayoutAnimationController { /** * Update layout of given view, via immediate update or animation depending on the current batch - * layout animation configuration supplied during initialization. + * layout animation configuration supplied during initialization. Handles create and update + * animations. * * @param view the view to update layout of * @param x the new X position for the view @@ -76,9 +85,9 @@ public class LayoutAnimationController { public void applyLayoutUpdate(View view, int x, int y, int width, int height) { UiThreadUtil.assertOnUiThread(); - // Determine which animation to use : if view is initially invisible, use create animation. - // If view is becoming invisible, use delete animation. Otherwise, use update animation. - // This approach is easier than maintaining a list of tags for recently created/deleted views. + // Determine which animation to use : if view is initially invisible, use create animation, + // otherwise use update animation. This approach is easier than maintaining a list of tags + // for recently created views. AbstractLayoutAnimation layoutAnimation = (view.getWidth() == 0 || view.getHeight() == 0) ? mLayoutCreateAnimation : mLayoutUpdateAnimation; @@ -91,4 +100,54 @@ public class LayoutAnimationController { view.startAnimation(animation); } } + + /** + * Animate a view deletion using the layout animation configuration supplied during initialization. + * + * @param view The view to animate. + * @param listener Called once the animation is finished, should be used to + * completely remove the view. + */ + public void deleteView(final View view, final LayoutAnimationListener listener) { + UiThreadUtil.assertOnUiThread(); + + AbstractLayoutAnimation layoutAnimation = mLayoutDeleteAnimation; + + Animation animation = layoutAnimation.createAnimation( + view, view.getLeft(), view.getTop(), view.getWidth(), view.getHeight()); + + if (animation != null) { + disableUserInteractions(view); + + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation anim) {} + + @Override + public void onAnimationRepeat(Animation anim) {} + + @Override + public void onAnimationEnd(Animation anim) { + listener.onAnimationEnd(); + } + }); + + view.startAnimation(animation); + } else { + listener.onAnimationEnd(); + } + } + + /** + * Disables user interactions for a view and all it's subviews. + */ + private void disableUserInteractions(View view) { + view.setClickable(false); + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup)view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + disableUserInteractions(viewGroup.getChildAt(i)); + } + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java new file mode 100644 index 000000000..7edd4251d --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationListener.java @@ -0,0 +1,10 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager.layoutanimation; + +/** + * Listener invoked when a layout animation has completed. + */ +public interface LayoutAnimationListener { + void onAnimationEnd(); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java index 0f3d9d7fd..a21c0d49c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java @@ -7,7 +7,8 @@ package com.facebook.react.uimanager.layoutanimation; */ /* package */ enum LayoutAnimationType { CREATE("create"), - UPDATE("update"); + UPDATE("update"), + DELETE("delete"); private final String mName; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java new file mode 100644 index 000000000..f2c3ccb2c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutDeleteAnimation.java @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager.layoutanimation; + +/** + * Class responsible for handling layout view deletion animation, applied to view whenever a + * valid config was supplied for the layout animation of DELETE type. + */ +/* package */ class LayoutDeleteAnimation extends BaseLayoutAnimation { + + @Override + boolean isReverse() { + return true; + } +}