// Copyright 2004-present Facebook. All Rights Reserved. package com.facebook.react.uimanager.layoutanimation; 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; import com.facebook.react.bridge.UiThreadUtil; /** * Class responsible for animation layout changes, if a valid layout animation config has been * supplied. If not animation is available, layout change is applied immediately instead of * performing an animation. * * TODO(7613721): Invoke success callback at the end of animation and when animation gets cancelled. */ @NotThreadSafe public class LayoutAnimationController { private static final boolean ENABLED = true; 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) { if (!ENABLED) { return; } if (config == null) { reset(); return; } mShouldAnimateLayout = false; int globalDuration = config.hasKey("duration") ? config.getInt("duration") : 0; if (config.hasKey(LayoutAnimationType.CREATE.toString())) { mLayoutCreateAnimation.initializeFromConfig( config.getMap(LayoutAnimationType.CREATE.toString()), globalDuration); mShouldAnimateLayout = true; } if (config.hasKey(LayoutAnimationType.UPDATE.toString())) { mLayoutUpdateAnimation.initializeFromConfig( 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; } public boolean shouldAnimateLayout(View viewToAnimate) { // if view parent is null, skip animation: view have been clipped, we don't want animation to // resume when view is re-attached to parent, which is the standard android animation behavior. return mShouldAnimateLayout && viewToAnimate.getParent() != null; } /** * Update layout of given view, via immediate update or animation depending on the current batch * 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 * @param y the new Y position for the view * @param width the new width value for the view * @param height the new height value for the view */ 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, // 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; Animation animation = layoutAnimation.createAnimation(view, x, y, width, height); if (animation == null || !(animation instanceof HandleLayout)) { view.layout(x, y, x + width, y + height); } if (animation != null) { 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)); } } } }