/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import android.support.annotation.IntDef; import android.support.v4.util.Pools; import android.support.v4.util.SimpleArrayMap; import android.util.Log; import android.view.View; import android.view.ViewParent; import com.facebook.litho.animation.AnimatedPropertyNode; import com.facebook.litho.animation.AnimationBinding; import com.facebook.litho.animation.AnimationBindingListener; import com.facebook.litho.animation.AnimatedProperty; import com.facebook.litho.animation.Resolver; import com.facebook.litho.animation.ComponentProperty; import com.facebook.litho.animation.RuntimeValue; import com.facebook.litho.internal.ArraySet; /** * Unique per MountState instance. Called from MountState on mount calls to process the transition * keys and handles which transitions to run and when. */ public class DataFlowTransitionManager { @IntDef({KeyStatus.APPEARED, KeyStatus.UNCHANGED, KeyStatus.DISAPPEARED, KeyStatus.UNSET}) @Retention(RetentionPolicy.SOURCE) @interface KeyStatus { int UNSET = -1; int APPEARED = 0; int UNCHANGED = 1; int DISAPPEARED = 2; } /** * A listener that will be invoked when a mount item has stopped animating. */ public interface OnMountItemAnimationComplete { void onMountItemAnimationComplete(Object mountItem); } private static final boolean DEBUG = false; private static final String TAG = "LithoAnimationDebug"; private static final Pools.SimplePool<AnimationState> sAnimationStatePool = new Pools.SimplePool<>(20); /** * The before and after values of single component undergoing a transition. */ private static class TransitionDiff { public final SimpleArrayMap<AnimatedProperty, Float> beforeValues = new SimpleArrayMap<>(); public final SimpleArrayMap<AnimatedProperty, Float> afterValues = new SimpleArrayMap<>(); public void reset() { beforeValues.clear(); afterValues.clear(); } } /** * Animation state of a MountItem. */ private static class AnimationState { public final ArraySet<AnimationBinding> activeAnimations = new ArraySet<>(); public final SimpleArrayMap<AnimatedProperty, AnimatedPropertyNode> animatedPropertyNodes = new SimpleArrayMap<>(); public ArraySet<AnimatedProperty> animatingProperties = new ArraySet<>(); public Object mountItem; public ArrayList<OnMountItemAnimationComplete> mAnimationCompleteListeners = new ArrayList<>(); public TransitionDiff currentDiff = new TransitionDiff(); public int changeType = KeyStatus.UNSET; public boolean sawInPreMount = false; public void reset() { activeAnimations.clear(); animatedPropertyNodes.clear(); animatingProperties.clear(); mountItem = null; mAnimationCompleteListeners.clear(); currentDiff.reset(); changeType = KeyStatus.UNSET; sawInPreMount = false; } } private final ArrayList<AnimationBinding> mAnimationBindings = new ArrayList<>(); private final SimpleArrayMap<AnimationBinding, ArraySet<String>> mAnimationsToKeys = new SimpleArrayMap<>(); private final SimpleArrayMap<String, AnimationState> mAnimationStates = new SimpleArrayMap<>(); private final TransitionsAnimationBindingListener mAnimationBindingListener = new TransitionsAnimationBindingListener(); private final TransitionsResolver mResolver = new TransitionsResolver(); void onNewTransitionContext(TransitionContext transitionContext) { mAnimationBindings.clear(); mAnimationBindings.addAll(transitionContext.getTransitionAnimationBindings()); if (DEBUG) { Log.d(TAG, "Got new TransitionContext with " + mAnimationBindings.size() + " animations"); } for (int i = 0, size = mAnimationStates.size(); i < size; i++) { final AnimationState animationState = mAnimationStates.valueAt(i); animationState.sawInPreMount = false; animationState.currentDiff.reset(); } recordAllTransitioningProperties(); } void onPreMountItem(String transitionKey, Object mountItem) { final AnimationState animationState = mAnimationStates.get(transitionKey); if (animationState != null) { for (int i = 0; i < animationState.animatingProperties.size(); i++) { final AnimatedProperty prop = animationState.animatingProperties.valueAt(i); if (animationState.currentDiff.beforeValues.put(prop, prop.get(mountItem)) != null) { throw new RuntimeException("TransitionDiff wasn't cleared properly!"); } // Unfortunately, we have no guarantee that this mountItem won't be re-used for another // different component during the coming mount, so we need to reset it before the actual // mount happens. The proper before-values will be set again before any animations start. prop.reset(mountItem); } setMountItem(animationState, mountItem); // We set the change type to disappeared for now: if we see it again in onPostMountItem we'll // update it there animationState.changeType = KeyStatus.DISAPPEARED; animationState.sawInPreMount = true; } } void onPostMountItem(String transitionKey, Object mountItem) { final AnimationState animationState = mAnimationStates.get(transitionKey); if (animationState != null) { if (!animationState.sawInPreMount) { animationState.changeType = KeyStatus.APPEARED; } else { animationState.changeType = KeyStatus.UNCHANGED; } setMountItem(animationState, mountItem); for (int i = 0; i < animationState.animatingProperties.size(); i++) { final AnimatedProperty prop = animationState.animatingProperties.valueAt(i); if (animationState.currentDiff.afterValues.put(prop, prop.get(mountItem)) != null) { throw new RuntimeException("TransitionDiff wasn't cleared properly!"); } } } } void runTransitions() { restoreInitialStates(); setDisappearToValues(); if (DEBUG) { debugLogStartingAnimations(); } for (int i = 0, size = mAnimationBindings.size(); i < size; i++) { final AnimationBinding binding = mAnimationBindings.get(i); binding.addListener(mAnimationBindingListener); binding.start(mResolver); } } void addMountItemAnimationCompleteListener(String key, OnMountItemAnimationComplete listener) { final AnimationState state = mAnimationStates.get(key); state.mAnimationCompleteListeners.add(listener); } boolean isKeyAnimating(String key) { return mAnimationStates.containsKey(key); } void onContentUnmounted(String transitionKey) { if (DEBUG) { Log.d(TAG, "Content unmounted for key: " + transitionKey); } final AnimationState animationState = mAnimationStates.get(transitionKey); if (animationState == null) { return; } setMountItem(animationState, null); } private void restoreInitialStates() { for (int i = 0, size = mAnimationStates.size(); i < size; i++) { final String transitionKey = mAnimationStates.keyAt(i); final AnimationState animationState = mAnimationStates.valueAt(i); // If the component is appearing, we will instead restore the initial value in // setAppearFromValues. This is necessary since appearFrom values can be written in terms of // the end state (e.g. appear from an offset of -10dp) if (animationState.changeType != KeyStatus.APPEARED) { for (int j = 0; j < animationState.currentDiff.beforeValues.size(); j++) { final AnimatedProperty property = animationState.currentDiff.beforeValues.keyAt(j); property.set( animationState.mountItem, animationState.currentDiff.beforeValues.valueAt(j)); } } } setAppearFromValues(); } private void setAppearFromValues() { SimpleArrayMap<ComponentProperty, RuntimeValue> appearFromValues = new SimpleArrayMap<>(); for (int i = 0, size = mAnimationBindings.size(); i < size; i++) { final AnimationBinding binding = mAnimationBindings.get(i); binding.collectAppearFromValues(appearFromValues); } for (int i = 0, size = appearFromValues.size(); i < size; i++) { final ComponentProperty property = appearFromValues.keyAt(i); final RuntimeValue runtimeValue = appearFromValues.valueAt(i); final AnimationState animationState = mAnimationStates.get(property.getTransitionKey()); final float value = runtimeValue.resolve(mResolver, property); property.getProperty().set(animationState.mountItem, value); if (animationState.changeType != KeyStatus.APPEARED) { throw new RuntimeException( "Wrong transition type for appear of key " + property.getTransitionKey() + ": " + keyStatusToString(animationState.changeType)); } animationState.currentDiff.beforeValues.put(property.getProperty(), value); } } private void setDisappearToValues() { SimpleArrayMap<ComponentProperty, RuntimeValue> disappearToValues = new SimpleArrayMap<>(); for (int i = 0, size = mAnimationBindings.size(); i < size; i++) { final AnimationBinding binding = mAnimationBindings.get(i); binding.collectDisappearToValues(disappearToValues); } for (int i = 0, size = disappearToValues.size(); i < size; i++) { final ComponentProperty property = disappearToValues.keyAt(i); final RuntimeValue runtimeValue = disappearToValues.valueAt(i); final AnimationState animationState = mAnimationStates.get(property.getTransitionKey()); if (animationState.changeType != KeyStatus.DISAPPEARED) { throw new RuntimeException( "Wrong transition type for disappear of key " + property.getTransitionKey() + ": " + keyStatusToString(animationState.changeType)); } final float value = runtimeValue.resolve(mResolver, property); animationState.currentDiff.afterValues.put(property.getProperty(), value); } } /** * This method should record the transition key and animated properties of all animating mount * items so that we know whether to record them in onPre/PostMountItem */ private void recordAllTransitioningProperties() { final ArraySet<ComponentProperty> transitioningProperties = ComponentsPools.acquireArraySet(); for (int i = 0, size = mAnimationBindings.size(); i < size; i++) { final AnimationBinding binding = mAnimationBindings.get(i); final ArraySet<String> animatedKeys = ComponentsPools.acquireArraySet(); mAnimationsToKeys.put(binding, animatedKeys); binding.collectTransitioningProperties(transitioningProperties); for (int j = 0, propSize = transitioningProperties.size(); j < propSize; j++) { final ComponentProperty property = transitioningProperties.valueAt(j); final String key = property.getTransitionKey(); final AnimatedProperty animatedProperty = property.getProperty(); animatedKeys.add(key); // This key will be animating - make sure it has an AnimationState AnimationState animationState = mAnimationStates.get(key); if (animationState == null) { animationState = acquireAnimationState(); mAnimationStates.put(key, animationState); } animationState.animatingProperties.add(animatedProperty); animationState.activeAnimations.add(binding); } transitioningProperties.clear(); } ComponentsPools.release(transitioningProperties); } private AnimatedPropertyNode getOrCreateAnimatedPropertyNode( String key, AnimatedProperty animatedProperty) { final AnimationState state = mAnimationStates.get(key); AnimatedPropertyNode node = state.animatedPropertyNodes.get(animatedProperty); if (node == null) { node = new AnimatedPropertyNode(state.mountItem, animatedProperty); state.animatedPropertyNodes.put(animatedProperty, node); } return node; } private void setMountItem(AnimationState animationState, Object newMountItem) { // If the mount item changes, this means this transition key will be rendered with a different // mount item (View or Drawable) than it was during the last mount, so we need to migrate // animation state from the old mount item to the new one. if (animationState.mountItem == newMountItem) { return; } if (animationState.mountItem != null) { final ArraySet<AnimatedProperty> animatingProperties = animationState.animatingProperties; for (int i = 0, size = animatingProperties.size(); i < size; i++) { animatingProperties.valueAt(i).reset(animationState.mountItem); } onMountItemAnimationComplete(animationState); } for (int i = 0, size = animationState.animatedPropertyNodes.size(); i < size; i++) { animationState.animatedPropertyNodes.valueAt(i).setMountItem(newMountItem); } recursivelySetChildClipping(newMountItem, false); animationState.mountItem = newMountItem; } private void onMountItemAnimationComplete(AnimationState animationState) { recursivelySetChildClipping(animationState.mountItem, true); fireMountItemAnimationCompleteListeners(animationState); } private void fireMountItemAnimationCompleteListeners(AnimationState animationState) { if (animationState.mountItem == null) { return; } final ArrayList<OnMountItemAnimationComplete> listeners = animationState.mAnimationCompleteListeners; for (int i = 0, listenerSize = listeners.size(); i < listenerSize; i++) { listeners.get(i).onMountItemAnimationComplete(animationState.mountItem); } listeners.clear(); } /** * Set the clipChildren properties to all Views in the same tree branch from the given one, up to * the top LithoView. * * TODO(17934271): Handle the case where two+ animations with different lifespans share the same * parent, in which case we shouldn't unset clipping until the last item is done animating. */ private void recursivelySetChildClipping(Object mountItem, boolean clipChildren) { if (!(mountItem instanceof View)) { return; } recursivelySetChildClippingForView((View) mountItem, clipChildren); } private void recursivelySetChildClippingForView(View view, boolean clipChildren) { if (view instanceof ComponentHost) { ((ComponentHost) view).setClipChildren(clipChildren); } final ViewParent parent = view.getParent(); if (parent instanceof ComponentHost) { recursivelySetChildClippingForView((View) parent, clipChildren); } } private void debugLogStartingAnimations() { if (!DEBUG) { throw new RuntimeException("Trying to debug log animations without debug flag set!"); } Log.d(TAG, "Starting animations:"); final ArraySet<ComponentProperty> transitioningProperties = new ArraySet<>(); for (int i = 0, size = mAnimationBindings.size(); i < size; i++) { final AnimationBinding binding = mAnimationBindings.get(i); binding.collectTransitioningProperties(transitioningProperties); for (int j = 0, propSize = transitioningProperties.size(); j < propSize; j++) { final ComponentProperty property = transitioningProperties.valueAt(j); final String key = property.getTransitionKey(); final AnimatedProperty animatedProperty = property.getProperty(); final AnimationState animationState = mAnimationStates.get(key); final float beforeValue = animationState.currentDiff.beforeValues.get(animatedProperty); final float afterValue = animationState.currentDiff.afterValues.get(animatedProperty); final String changeType = keyStatusToString(animationState.changeType); Log.d( TAG, " - " + key + "." + animatedProperty.getName() + " will animate from " + beforeValue + " to " + afterValue + " (" + changeType + ")"); } transitioningProperties.clear(); } } private static String keyStatusToString(int keyStatus) { switch (keyStatus) { case KeyStatus.APPEARED: return "APPEARED"; case KeyStatus.UNCHANGED: return "UNCHANGED"; case KeyStatus.DISAPPEARED: return "DISAPPEARED"; case KeyStatus.UNSET: return "UNSET"; default: throw new RuntimeException("Unknown keyStatus: " + keyStatus); } } private static AnimationState acquireAnimationState() { AnimationState animationState = sAnimationStatePool.acquire(); if (animationState == null) { animationState = new AnimationState(); } return animationState; } private static void releaseAnimationState(AnimationState animationState) { animationState.reset(); sAnimationStatePool.release(animationState); } private class TransitionsAnimationBindingListener implements AnimationBindingListener { @Override public void onStart(AnimationBinding binding) { } @Override public void onFinish(AnimationBinding binding) { final ArraySet<String> transitioningKeys = mAnimationsToKeys.remove(binding); // When an animation finishes, we want to go through all the mount items it was animating and // see if it was the last active animation. If it was, we know that item is no longer // animating and we can release the animation state. for (int i = 0, size = transitioningKeys.size(); i < size; i++) { final String key = transitioningKeys.valueAt(i); final AnimationState animationState = mAnimationStates.get(key); if (!animationState.activeAnimations.remove(binding)) { throw new RuntimeException( "Some animation bookkeeping is wrong: tried to remove an animation from the list " + "of active animations, but it wasn't there."); } if (animationState.activeAnimations.size() == 0) { if (animationState.changeType == KeyStatus.DISAPPEARED && animationState.mountItem != null) { for (int j = 0; j < animationState.animatingProperties.size(); j++) { animationState.animatingProperties.valueAt(j).reset(animationState.mountItem); } } onMountItemAnimationComplete(animationState); mAnimationStates.remove(key); releaseAnimationState(animationState); } } ComponentsPools.release(transitioningKeys); } } private class TransitionsResolver implements Resolver { @Override public float getCurrentState(ComponentProperty property) { final AnimationState animationState = mAnimationStates.get(property.getTransitionKey()); return property.getProperty().get(animationState.mountItem); } @Override public float getEndState(ComponentProperty property) { final AnimationState animationState = mAnimationStates.get(property.getTransitionKey()); return animationState.currentDiff.afterValues.get(property.getProperty()); } @Override public AnimatedPropertyNode getAnimatedPropertyNode(ComponentProperty property) { return getOrCreateAnimatedPropertyNode(property.getTransitionKey(), property.getProperty()); } } }