/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.graphics.drawable; import android.animation.Animator; import android.animation.AnimatorInflater; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.Animator.AnimatorListener; import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.animation.ObjectAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread; import android.app.Application; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Insets; import android.graphics.Outline; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.Rect; import android.os.Build; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.IntArray; import android.util.Log; import android.util.LongArray; import android.util.PathParser; import android.util.TimeUtils; import android.view.Choreographer; import android.view.DisplayListCanvas; import android.view.RenderNode; import android.view.RenderNodeAnimatorSetHelper; import android.view.View; import com.android.internal.R; import com.android.internal.util.VirtualRefBasePtr; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; /** * This class uses {@link android.animation.ObjectAnimator} and * {@link android.animation.AnimatorSet} to animate the properties of a * {@link android.graphics.drawable.VectorDrawable} to create an animated drawable. *

* AnimatedVectorDrawable are normally defined as 3 separate XML files. *

*

* First is the XML file for {@link android.graphics.drawable.VectorDrawable}. * Note that we allow the animation to happen on the group's attributes and path's * attributes, which requires they are uniquely named in this XML file. Groups * and paths without animations do not need names. *

*
  • Here is a simple VectorDrawable in this vectordrawable.xml file. *
     * <vector xmlns:android="http://schemas.android.com/apk/res/android"
     *     android:height="64dp"
     *     android:width="64dp"
     *     android:viewportHeight="600"
     *     android:viewportWidth="600" >
     *     <group
     *         android:name="rotationGroup"
     *         android:pivotX="300.0"
     *         android:pivotY="300.0"
     *         android:rotation="45.0" >
     *         <path
     *             android:name="v"
     *             android:fillColor="#000000"
     *             android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
     *     </group>
     * </vector>
     * 
  • *

    * Second is the AnimatedVectorDrawable's XML file, which defines the target * VectorDrawable, the target paths and groups to animate, the properties of the * path and group to animate and the animations defined as the ObjectAnimators * or AnimatorSets. *

    *
  • Here is a simple AnimatedVectorDrawable defined in this avd.xml file. * Note how we use the names to refer to the groups and paths in the vectordrawable.xml. *
     * <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
     *   android:drawable="@drawable/vectordrawable" >
     *     <target
     *         android:name="rotationGroup"
     *         android:animation="@anim/rotation" />
     *     <target
     *         android:name="v"
     *         android:animation="@anim/path_morph" />
     * </animated-vector>
     * 
  • *

    * Last is the Animator XML file, which is the same as a normal ObjectAnimator * or AnimatorSet. * To complete this example, here are the 2 animator files used in avd.xml: * rotation.xml and path_morph.xml. *

    *
  • Here is the rotation.xml, which will rotate the target group for 360 degrees. *
     * <objectAnimator
     *     android:duration="6000"
     *     android:propertyName="rotation"
     *     android:valueFrom="0"
     *     android:valueTo="360" />
     * 
  • *
  • Here is the path_morph.xml, which will morph the path from one shape to * the other. Note that the paths must be compatible for morphing. * In more details, the paths should have exact same length of commands , and * exact same length of parameters for each commands. * Note that the path strings are better stored in strings.xml for reusing. *
     * <set xmlns:android="http://schemas.android.com/apk/res/android">
     *     <objectAnimator
     *         android:duration="3000"
     *         android:propertyName="pathData"
     *         android:valueFrom="M300,70 l 0,-70 70,70 0,0   -70,70z"
     *         android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
     *         android:valueType="pathType"/>
     * </set>
     * 
  • * * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation */ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { private static final String LOGTAG = "AnimatedVectorDrawable"; private static final String ANIMATED_VECTOR = "animated-vector"; private static final String TARGET = "target"; private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false; /** Local, mutable animator set. */ private VectorDrawableAnimator mAnimatorSet = new VectorDrawableAnimatorUI(this); /** * The resources against which this drawable was created. Used to attempt * to inflate animators if applyTheme() doesn't get called. */ private Resources mRes; private AnimatedVectorDrawableState mAnimatedVectorState; /** The animator set that is parsed from the xml. */ private AnimatorSet mAnimatorSetFromXml = null; private boolean mMutated; /** Use a internal AnimatorListener to support callbacks during animation events. */ private ArrayList mAnimationCallbacks = null; private AnimatorListener mAnimatorListener = null; public AnimatedVectorDrawable() { this(null, null); } private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); mRes = res; } @Override public Drawable mutate() { if (!mMutated && super.mutate() == this) { mAnimatedVectorState = new AnimatedVectorDrawableState( mAnimatedVectorState, mCallback, mRes); mMutated = true; } return this; } /** * @hide */ public void clearMutated() { super.clearMutated(); if (mAnimatedVectorState.mVectorDrawable != null) { mAnimatedVectorState.mVectorDrawable.clearMutated(); } mMutated = false; } /** * In order to avoid breaking old apps, we only throw exception on invalid VectorDrawable * animations * for apps targeting N and later. For older apps, we ignore (i.e. quietly skip) * these animations. * * @return whether invalid animations for vector drawable should be ignored. */ private static boolean shouldIgnoreInvalidAnimation() { Application app = ActivityThread.currentApplication(); if (app == null || app.getApplicationInfo() == null) { return true; } if (app.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { return true; } return false; } @Override public ConstantState getConstantState() { mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations(); return mAnimatedVectorState; } @Override public @Config int getChangingConfigurations() { return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations(); } @Override public void draw(Canvas canvas) { mAnimatorSet.onDraw(canvas); mAnimatedVectorState.mVectorDrawable.draw(canvas); } @Override protected void onBoundsChange(Rect bounds) { mAnimatedVectorState.mVectorDrawable.setBounds(bounds); } @Override protected boolean onStateChange(int[] state) { return mAnimatedVectorState.mVectorDrawable.setState(state); } @Override protected boolean onLevelChange(int level) { return mAnimatedVectorState.mVectorDrawable.setLevel(level); } @Override public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection); } /** * AnimatedVectorDrawable is running on render thread now. Therefore, if the root alpha is being * animated, then the root alpha value we get from this call could be out of sync with alpha * value used in the render thread. Otherwise, the root alpha should be always the same value. * * @return the containing vector drawable's root alpha value. */ @Override public int getAlpha() { return mAnimatedVectorState.mVectorDrawable.getAlpha(); } @Override public void setAlpha(int alpha) { mAnimatedVectorState.mVectorDrawable.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter colorFilter) { mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter); } @Override public ColorFilter getColorFilter() { return mAnimatedVectorState.mVectorDrawable.getColorFilter(); } @Override public void setTintList(ColorStateList tint) { mAnimatedVectorState.mVectorDrawable.setTintList(tint); } @Override public void setHotspot(float x, float y) { mAnimatedVectorState.mVectorDrawable.setHotspot(x, y); } @Override public void setHotspotBounds(int left, int top, int right, int bottom) { mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom); } @Override public void setTintMode(PorterDuff.Mode tintMode) { mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode); } @Override public boolean setVisible(boolean visible, boolean restart) { if (mAnimatorSet.isInfinite() && mAnimatorSet.isStarted()) { if (visible) { // Resume the infinite animation when the drawable becomes visible again. mAnimatorSet.resume(); } else { // Pause the infinite animation once the drawable is no longer visible. mAnimatorSet.pause(); } } mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart); return super.setVisible(visible, restart); } @Override public boolean isStateful() { return mAnimatedVectorState.mVectorDrawable.isStateful(); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public int getIntrinsicWidth() { return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth(); } @Override public int getIntrinsicHeight() { return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight(); } @Override public void getOutline(@NonNull Outline outline) { mAnimatedVectorState.mVectorDrawable.getOutline(outline); } /** @hide */ @Override public Insets getOpticalInsets() { return mAnimatedVectorState.mVectorDrawable.getOpticalInsets(); } @Override public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { final AnimatedVectorDrawableState state = mAnimatedVectorState; int eventType = parser.getEventType(); float pathErrorScale = 1; while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { final String tagName = parser.getName(); if (ANIMATED_VECTOR.equals(tagName)) { final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.AnimatedVectorDrawable); int drawableRes = a.getResourceId( R.styleable.AnimatedVectorDrawable_drawable, 0); if (drawableRes != 0) { VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable( drawableRes, theme).mutate(); vectorDrawable.setAllowCaching(false); vectorDrawable.setCallback(mCallback); pathErrorScale = vectorDrawable.getPixelSize(); if (state.mVectorDrawable != null) { state.mVectorDrawable.setCallback(null); } state.mVectorDrawable = vectorDrawable; } a.recycle(); } else if (TARGET.equals(tagName)) { final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.AnimatedVectorDrawableTarget); final String target = a.getString( R.styleable.AnimatedVectorDrawableTarget_name); final int animResId = a.getResourceId( R.styleable.AnimatedVectorDrawableTarget_animation, 0); if (animResId != 0) { if (theme != null) { final Animator objectAnimator = AnimatorInflater.loadAnimator( res, theme, animResId, pathErrorScale); state.addTargetAnimator(target, objectAnimator); } else { // The animation may be theme-dependent. As a // workaround until Animator has full support for // applyTheme(), postpone loading the animator // until we have a theme in applyTheme(). state.addPendingAnimator(animResId, pathErrorScale, target); } } a.recycle(); } } eventType = parser.next(); } // If we don't have any pending animations, we don't need to hold a // reference to the resources. mRes = state.mPendingAnims == null ? null : res; } /** * Force to animate on UI thread. * @hide */ public void forceAnimationOnUI() { if (mAnimatorSet instanceof VectorDrawableAnimatorRT) { VectorDrawableAnimatorRT animator = (VectorDrawableAnimatorRT) mAnimatorSet; if (animator.isRunning()) { throw new UnsupportedOperationException("Cannot force Animated Vector Drawable to" + " run on UI thread when the animation has started on RenderThread."); } mAnimatorSet = new VectorDrawableAnimatorUI(this); if (mAnimatorSetFromXml != null) { mAnimatorSet.init(mAnimatorSetFromXml); } } } @Override public boolean canApplyTheme() { return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme()) || super.canApplyTheme(); } @Override public void applyTheme(Theme t) { super.applyTheme(t); final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable; if (vectorDrawable != null && vectorDrawable.canApplyTheme()) { vectorDrawable.applyTheme(t); } if (t != null) { mAnimatedVectorState.inflatePendingAnimators(t.getResources(), t); } // If we don't have any pending animations, we don't need to hold a // reference to the resources. if (mAnimatedVectorState.mPendingAnims == null) { mRes = null; } } private static class AnimatedVectorDrawableState extends ConstantState { @Config int mChangingConfigurations; VectorDrawable mVectorDrawable; /** Animators that require a theme before inflation. */ ArrayList mPendingAnims; /** Fully inflated animators awaiting cloning into an AnimatorSet. */ ArrayList mAnimators; /** Map of animators to their target object names */ ArrayMap mTargetNameMap; public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, Callback owner, Resources res) { if (copy != null) { mChangingConfigurations = copy.mChangingConfigurations; if (copy.mVectorDrawable != null) { final ConstantState cs = copy.mVectorDrawable.getConstantState(); if (res != null) { mVectorDrawable = (VectorDrawable) cs.newDrawable(res); } else { mVectorDrawable = (VectorDrawable) cs.newDrawable(); } mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate(); mVectorDrawable.setCallback(owner); mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection()); mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds()); mVectorDrawable.setAllowCaching(false); } if (copy.mAnimators != null) { mAnimators = new ArrayList<>(copy.mAnimators); } if (copy.mTargetNameMap != null) { mTargetNameMap = new ArrayMap<>(copy.mTargetNameMap); } if (copy.mPendingAnims != null) { mPendingAnims = new ArrayList<>(copy.mPendingAnims); } } else { mVectorDrawable = new VectorDrawable(); } } @Override public boolean canApplyTheme() { return (mVectorDrawable != null && mVectorDrawable.canApplyTheme()) || mPendingAnims != null || super.canApplyTheme(); } @Override public Drawable newDrawable() { return new AnimatedVectorDrawable(this, null); } @Override public Drawable newDrawable(Resources res) { return new AnimatedVectorDrawable(this, res); } @Override public @Config int getChangingConfigurations() { return mChangingConfigurations; } public void addPendingAnimator(int resId, float pathErrorScale, String target) { if (mPendingAnims == null) { mPendingAnims = new ArrayList<>(1); } mPendingAnims.add(new PendingAnimator(resId, pathErrorScale, target)); } public void addTargetAnimator(String targetName, Animator animator) { if (mAnimators == null) { mAnimators = new ArrayList<>(1); mTargetNameMap = new ArrayMap<>(1); } mAnimators.add(animator); mTargetNameMap.put(animator, targetName); if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.v(LOGTAG, "add animator for target " + targetName + " " + animator); } } /** * Prepares a local set of mutable animators based on the constant * state. *

    * If there are any pending uninflated animators, attempts to inflate * them immediately against the provided resources object. * * @param animatorSet the animator set to which the animators should * be added * @param res the resources against which to inflate any pending * animators, or {@code null} if not available */ public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet, @Nullable Resources res) { // Check for uninflated animators. We can remove this after we add // support for Animator.applyTheme(). See comments in inflate(). if (mPendingAnims != null) { // Attempt to load animators without applying a theme. if (res != null) { inflatePendingAnimators(res, null); } else { Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable" + " must be created using a Resources object or applyTheme() must be" + " called with a non-null Theme object."); } mPendingAnims = null; } // Perform a deep copy of the constant state's animators. final int count = mAnimators == null ? 0 : mAnimators.size(); if (count > 0) { final Animator firstAnim = prepareLocalAnimator(0); final AnimatorSet.Builder builder = animatorSet.play(firstAnim); for (int i = 1; i < count; ++i) { final Animator nextAnim = prepareLocalAnimator(i); builder.with(nextAnim); } } } /** * Prepares a local animator for the given index within the constant * state's list of animators. * * @param index the index of the animator within the constant state */ private Animator prepareLocalAnimator(int index) { final Animator animator = mAnimators.get(index); final Animator localAnimator = animator.clone(); final String targetName = mTargetNameMap.get(animator); final Object target = mVectorDrawable.getTargetByName(targetName); localAnimator.setTarget(target); return localAnimator; } /** * Inflates pending animators, if any, against a theme. Clears the list of * pending animators. * * @param t the theme against which to inflate the animators */ public void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) { final ArrayList pendingAnims = mPendingAnims; if (pendingAnims != null) { mPendingAnims = null; for (int i = 0, count = pendingAnims.size(); i < count; i++) { final PendingAnimator pendingAnimator = pendingAnims.get(i); final Animator objectAnimator = pendingAnimator.newInstance(res, t); addTargetAnimator(pendingAnimator.target, objectAnimator); } } } /** * Basically a constant state for Animators until we actually implement * constant states for Animators. */ private static class PendingAnimator { public final int animResId; public final float pathErrorScale; public final String target; public PendingAnimator(int animResId, float pathErrorScale, String target) { this.animResId = animResId; this.pathErrorScale = pathErrorScale; this.target = target; } public Animator newInstance(Resources res, Theme theme) { return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale); } } } @Override public boolean isRunning() { return mAnimatorSet.isRunning(); } /** * Resets the AnimatedVectorDrawable to the start state as specified in the animators. */ public void reset() { ensureAnimatorSet(); if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.w(LOGTAG, "calling reset on AVD: " + ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState) getConstantState()).mVectorDrawable.getConstantState()).mRootName + ", at: " + this); } mAnimatorSet.reset(); } @Override public void start() { ensureAnimatorSet(); if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.w(LOGTAG, "calling start on AVD: " + ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState) getConstantState()).mVectorDrawable.getConstantState()).mRootName + ", at: " + this); } mAnimatorSet.start(); } @NonNull private void ensureAnimatorSet() { if (mAnimatorSetFromXml == null) { // TODO: Skip the AnimatorSet creation and init the VectorDrawableAnimator directly // with a list of LocalAnimators. mAnimatorSetFromXml = new AnimatorSet(); mAnimatedVectorState.prepareLocalAnimators(mAnimatorSetFromXml, mRes); mAnimatorSet.init(mAnimatorSetFromXml); mRes = null; } } @Override public void stop() { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.w(LOGTAG, "calling stop on AVD: " + ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState) getConstantState()).mVectorDrawable.getConstantState()) .mRootName + ", at: " + this); } mAnimatorSet.end(); } /** * Reverses ongoing animations or starts pending animations in reverse. *

    * NOTE: Only works if all animations support reverse. Otherwise, this will * do nothing. * @hide */ public void reverse() { ensureAnimatorSet(); // Only reverse when all the animators can be reversed. if (!canReverse()) { Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()"); return; } mAnimatorSet.reverse(); } /** * @hide */ public boolean canReverse() { return mAnimatorSet.canReverse(); } private final Callback mCallback = new Callback() { @Override public void invalidateDrawable(@NonNull Drawable who) { invalidateSelf(); } @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { scheduleSelf(what, when); } @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { unscheduleSelf(what); } }; @Override public void registerAnimationCallback(@NonNull AnimationCallback callback) { if (callback == null) { return; } // Add listener accordingly. if (mAnimationCallbacks == null) { mAnimationCallbacks = new ArrayList<>(); } mAnimationCallbacks.add(callback); if (mAnimatorListener == null) { // Create a animator listener and trigger the callback events when listener is // triggered. mAnimatorListener = new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { ArrayList tmpCallbacks = new ArrayList<>(mAnimationCallbacks); int size = tmpCallbacks.size(); for (int i = 0; i < size; i ++) { tmpCallbacks.get(i).onAnimationStart(AnimatedVectorDrawable.this); } } @Override public void onAnimationEnd(Animator animation) { ArrayList tmpCallbacks = new ArrayList<>(mAnimationCallbacks); int size = tmpCallbacks.size(); for (int i = 0; i < size; i ++) { tmpCallbacks.get(i).onAnimationEnd(AnimatedVectorDrawable.this); } } }; } mAnimatorSet.setListener(mAnimatorListener); } // A helper function to clean up the animator listener in the mAnimatorSet. private void removeAnimatorSetListener() { if (mAnimatorListener != null) { mAnimatorSet.removeListener(mAnimatorListener); mAnimatorListener = null; } } @Override public boolean unregisterAnimationCallback(@NonNull AnimationCallback callback) { if (mAnimationCallbacks == null || callback == null) { // Nothing to be removed. return false; } boolean removed = mAnimationCallbacks.remove(callback); // When the last call back unregistered, remove the listener accordingly. if (mAnimationCallbacks.size() == 0) { removeAnimatorSetListener(); } return removed; } @Override public void clearAnimationCallbacks() { removeAnimatorSetListener(); if (mAnimationCallbacks == null) { return; } mAnimationCallbacks.clear(); } private interface VectorDrawableAnimator { void init(@NonNull AnimatorSet set); void start(); void end(); void reset(); void reverse(); boolean canReverse(); void setListener(AnimatorListener listener); void removeListener(AnimatorListener listener); void onDraw(Canvas canvas); boolean isStarted(); boolean isRunning(); boolean isInfinite(); void pause(); void resume(); } private static class VectorDrawableAnimatorUI implements VectorDrawableAnimator { // mSet is only initialized in init(). So we need to check whether it is null before any // operation. private AnimatorSet mSet = null; private final Drawable mDrawable; // Caching the listener in the case when listener operation is called before the mSet is // setup by init(). private ArrayList mListenerArray = null; private boolean mIsInfinite = false; VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) { mDrawable = drawable; } @Override public void init(@NonNull AnimatorSet set) { if (mSet != null) { // Already initialized throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " + "re-initialized"); } // Keep a deep copy of the set, such that set can be still be constantly representing // the static content from XML file. mSet = set.clone(); mIsInfinite = mSet.getTotalDuration() == Animator.DURATION_INFINITE; // If there are listeners added before calling init(), now they should be setup. if (mListenerArray != null && !mListenerArray.isEmpty()) { for (int i = 0; i < mListenerArray.size(); i++) { mSet.addListener(mListenerArray.get(i)); } mListenerArray.clear(); mListenerArray = null; } } // Although start(), reset() and reverse() should call init() already, it is better to // protect these functions from NPE in any situation. @Override public void start() { if (mSet == null || mSet.isStarted()) { return; } mSet.start(); invalidateOwningView(); } @Override public void end() { if (mSet == null) { return; } mSet.end(); } @Override public void reset() { if (mSet == null) { return; } start(); mSet.cancel(); } @Override public void reverse() { if (mSet == null) { return; } mSet.reverse(); invalidateOwningView(); } @Override public boolean canReverse() { return mSet != null && mSet.canReverse(); } @Override public void setListener(AnimatorListener listener) { if (mSet == null) { if (mListenerArray == null) { mListenerArray = new ArrayList(); } mListenerArray.add(listener); } else { mSet.addListener(listener); } } @Override public void removeListener(AnimatorListener listener) { if (mSet == null) { if (mListenerArray == null) { return; } mListenerArray.remove(listener); } else { mSet.removeListener(listener); } } @Override public void onDraw(Canvas canvas) { if (mSet != null && mSet.isStarted()) { invalidateOwningView(); } } @Override public boolean isStarted() { return mSet != null && mSet.isStarted(); } @Override public boolean isRunning() { return mSet != null && mSet.isRunning(); } @Override public boolean isInfinite() { return mIsInfinite; } @Override public void pause() { if (mSet == null) { return; } mSet.pause(); } @Override public void resume() { if (mSet == null) { return; } mSet.resume(); } private void invalidateOwningView() { mDrawable.invalidateSelf(); } } /** * @hide */ public static class VectorDrawableAnimatorRT implements VectorDrawableAnimator { private static final int START_ANIMATION = 1; private static final int REVERSE_ANIMATION = 2; private static final int RESET_ANIMATION = 3; private static final int END_ANIMATION = 4; private AnimatorListener mListener = null; private final LongArray mStartDelays = new LongArray(); private PropertyValuesHolder.PropertyValues mTmpValues = new PropertyValuesHolder.PropertyValues(); private long mSetPtr = 0; private boolean mContainsSequentialAnimators = false; private boolean mStarted = false; private boolean mInitialized = false; private boolean mIsReversible = false; private boolean mIsInfinite = false; // This needs to be set before parsing starts. private boolean mShouldIgnoreInvalidAnim; // TODO: Consider using NativeAllocationRegistery to track native allocation private final VirtualRefBasePtr mSetRefBasePtr; private WeakReference mLastSeenTarget = null; private int mLastListenerId = 0; private final IntArray mPendingAnimationActions = new IntArray(); private final Drawable mDrawable; VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) { mDrawable = drawable; mSetPtr = nCreateAnimatorSet(); // Increment ref count on native AnimatorSet, so it doesn't get released before Java // side is done using it. mSetRefBasePtr = new VirtualRefBasePtr(mSetPtr); } @Override public void init(@NonNull AnimatorSet set) { if (mInitialized) { // Already initialized throw new UnsupportedOperationException("VectorDrawableAnimator cannot be " + "re-initialized"); } mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation(); parseAnimatorSet(set, 0); mInitialized = true; mIsInfinite = set.getTotalDuration() == Animator.DURATION_INFINITE; // Check reversible. mIsReversible = true; if (mContainsSequentialAnimators) { mIsReversible = false; } else { // Check if there's any start delay set on child for (int i = 0; i < mStartDelays.size(); i++) { if (mStartDelays.get(i) > 0) { mIsReversible = false; return; } } } } private void parseAnimatorSet(AnimatorSet set, long startTime) { ArrayList animators = set.getChildAnimations(); boolean playTogether = set.shouldPlayTogether(); // Convert AnimatorSet to VectorDrawableAnimatorRT for (int i = 0; i < animators.size(); i++) { Animator animator = animators.get(i); // Here we only support ObjectAnimator if (animator instanceof AnimatorSet) { parseAnimatorSet((AnimatorSet) animator, startTime); } else if (animator instanceof ObjectAnimator) { createRTAnimator((ObjectAnimator) animator, startTime); } // ignore ValueAnimators and others because they don't directly modify VD // therefore will be useless to AVD. if (!playTogether) { // Assume not play together means play sequentially startTime += animator.getTotalDuration(); mContainsSequentialAnimators = true; } } } // TODO: This method reads animation data from already parsed Animators. We need to move // this step further up the chain in the parser to avoid the detour. private void createRTAnimator(ObjectAnimator animator, long startTime) { PropertyValuesHolder[] values = animator.getValues(); Object target = animator.getTarget(); if (target instanceof VectorDrawable.VGroup) { createRTAnimatorForGroup(values, animator, (VectorDrawable.VGroup) target, startTime); } else if (target instanceof VectorDrawable.VPath) { for (int i = 0; i < values.length; i++) { values[i].getPropertyValues(mTmpValues); if (mTmpValues.endValue instanceof PathParser.PathData && mTmpValues.propertyName.equals("pathData")) { createRTAnimatorForPath(animator, (VectorDrawable.VPath) target, startTime); } else if (target instanceof VectorDrawable.VFullPath) { createRTAnimatorForFullPath(animator, (VectorDrawable.VFullPath) target, startTime); } else if (!mShouldIgnoreInvalidAnim) { throw new IllegalArgumentException("ClipPath only supports PathData " + "property"); } } } else if (target instanceof VectorDrawable.VectorDrawableState) { createRTAnimatorForRootGroup(values, animator, (VectorDrawable.VectorDrawableState) target, startTime); } else if (!mShouldIgnoreInvalidAnim) { // Should never get here throw new UnsupportedOperationException("Target should be either VGroup, VPath, " + "or ConstantState, " + target == null ? "Null target" : target.getClass() + " is not supported"); } } private void createRTAnimatorForGroup(PropertyValuesHolder[] values, ObjectAnimator animator, VectorDrawable.VGroup target, long startTime) { long nativePtr = target.getNativePtr(); int propertyId; for (int i = 0; i < values.length; i++) { // TODO: We need to support the rare case in AVD where no start value is provided values[i].getPropertyValues(mTmpValues); propertyId = VectorDrawable.VGroup.getPropertyIndex(mTmpValues.propertyName); if (mTmpValues.type != Float.class && mTmpValues.type != float.class) { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.e(LOGTAG, "Unsupported type: " + mTmpValues.type + ". Only float value is supported for Groups."); } continue; } if (propertyId < 0) { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.e(LOGTAG, "Unsupported property: " + mTmpValues.propertyName + " for Vector Drawable Group"); } continue; } long propertyPtr = nCreateGroupPropertyHolder(nativePtr, propertyId, (Float) mTmpValues.startValue, (Float) mTmpValues.endValue); if (mTmpValues.dataSource != null) { float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator .getDuration()); nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length); } createNativeChildAnimator(propertyPtr, startTime, animator); } } private void createRTAnimatorForPath( ObjectAnimator animator, VectorDrawable.VPath target, long startTime) { long nativePtr = target.getNativePtr(); long startPathDataPtr = ((PathParser.PathData) mTmpValues.startValue) .getNativePtr(); long endPathDataPtr = ((PathParser.PathData) mTmpValues.endValue) .getNativePtr(); long propertyPtr = nCreatePathDataPropertyHolder(nativePtr, startPathDataPtr, endPathDataPtr); createNativeChildAnimator(propertyPtr, startTime, animator); } private void createRTAnimatorForFullPath(ObjectAnimator animator, VectorDrawable.VFullPath target, long startTime) { int propertyId = target.getPropertyIndex(mTmpValues.propertyName); long propertyPtr; long nativePtr = target.getNativePtr(); if (mTmpValues.type == Float.class || mTmpValues.type == float.class) { if (propertyId < 0) { if (mShouldIgnoreInvalidAnim) { return; } else { throw new IllegalArgumentException("Property: " + mTmpValues.propertyName + " is not supported for FullPath"); } } propertyPtr = nCreatePathPropertyHolder(nativePtr, propertyId, (Float) mTmpValues.startValue, (Float) mTmpValues.endValue); } else if (mTmpValues.type == Integer.class || mTmpValues.type == int.class) { propertyPtr = nCreatePathColorPropertyHolder(nativePtr, propertyId, (Integer) mTmpValues.startValue, (Integer) mTmpValues.endValue); } else { if (mShouldIgnoreInvalidAnim) { return; } else { throw new UnsupportedOperationException("Unsupported type: " + mTmpValues.type + ". Only float, int or PathData value is " + "supported for Paths."); } } if (mTmpValues.dataSource != null) { float[] dataPoints = createDataPoints(mTmpValues.dataSource, animator .getDuration()); nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length); } createNativeChildAnimator(propertyPtr, startTime, animator); } private void createRTAnimatorForRootGroup(PropertyValuesHolder[] values, ObjectAnimator animator, VectorDrawable.VectorDrawableState target, long startTime) { long nativePtr = target.getNativeRenderer(); if (!animator.getPropertyName().equals("alpha")) { if (mShouldIgnoreInvalidAnim) { return; } else { throw new UnsupportedOperationException("Only alpha is supported for root " + "group"); } } Float startValue = null; Float endValue = null; for (int i = 0; i < values.length; i++) { values[i].getPropertyValues(mTmpValues); if (mTmpValues.propertyName.equals("alpha")) { startValue = (Float) mTmpValues.startValue; endValue = (Float) mTmpValues.endValue; break; } } if (startValue == null && endValue == null) { if (mShouldIgnoreInvalidAnim) { return; } else { throw new UnsupportedOperationException("No alpha values are specified"); } } long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue); createNativeChildAnimator(propertyPtr, startTime, animator); } // These are the data points that define the value of the animating properties. // e.g. translateX and translateY can animate along a Path, at any fraction in [0, 1] // a point on the path corresponds to the values of translateX and translateY. // TODO: (Optimization) We should pass the path down in native and chop it into segments // in native. private static float[] createDataPoints( PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) { long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos(); int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS); int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs); float values[] = new float[numAnimFrames]; float lastFrame = numAnimFrames - 1; for (int i = 0; i < numAnimFrames; i++) { float fraction = i / lastFrame; values[i] = (Float) dataSource.getValueAtFraction(fraction); } return values; } private void createNativeChildAnimator(long propertyPtr, long extraDelay, ObjectAnimator animator) { long duration = animator.getDuration(); int repeatCount = animator.getRepeatCount(); long startDelay = extraDelay + animator.getStartDelay(); TimeInterpolator interpolator = animator.getInterpolator(); long nativeInterpolator = RenderNodeAnimatorSetHelper.createNativeInterpolator(interpolator, duration); startDelay *= ValueAnimator.getDurationScale(); duration *= ValueAnimator.getDurationScale(); mStartDelays.add(startDelay); nAddAnimator(mSetPtr, propertyPtr, nativeInterpolator, startDelay, duration, repeatCount); } /** * Holds a weak reference to the target that was last seen (through the DisplayListCanvas * in the last draw call), so that when animator set needs to start, we can add the animator * to the last seen RenderNode target and start right away. */ protected void recordLastSeenTarget(DisplayListCanvas canvas) { mLastSeenTarget = new WeakReference( RenderNodeAnimatorSetHelper.getTarget(canvas)); if (mPendingAnimationActions.size() > 0 && useLastSeenTarget()) { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.d(LOGTAG, "Target is set in the next frame"); } for (int i = 0; i < mPendingAnimationActions.size(); i++) { handlePendingAction(mPendingAnimationActions.get(i)); } mPendingAnimationActions.clear(); } } private void handlePendingAction(int pendingAnimationAction) { if (pendingAnimationAction == START_ANIMATION) { startAnimation(); } else if (pendingAnimationAction == REVERSE_ANIMATION) { reverseAnimation(); } else if (pendingAnimationAction == RESET_ANIMATION) { resetAnimation(); } else if (pendingAnimationAction == END_ANIMATION) { endAnimation(); } else { throw new UnsupportedOperationException("Animation action " + pendingAnimationAction + "is not supported"); } } private boolean useLastSeenTarget() { if (mLastSeenTarget != null) { final RenderNode target = mLastSeenTarget.get(); if (target != null && target.isAttached()) { target.addAnimator(this); return true; } } return false; } private void invalidateOwningView() { mDrawable.invalidateSelf(); } private void addPendingAction(int pendingAnimationAction) { invalidateOwningView(); mPendingAnimationActions.add(pendingAnimationAction); } @Override public void start() { if (!mInitialized) { return; } if (useLastSeenTarget()) { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.d(LOGTAG, "Target is set. Starting VDAnimatorSet from java"); } startAnimation(); } else { addPendingAction(START_ANIMATION); } } @Override public void end() { if (!mInitialized) { return; } if (useLastSeenTarget()) { endAnimation(); } else { addPendingAction(END_ANIMATION); } } @Override public void reset() { if (!mInitialized) { return; } if (useLastSeenTarget()) { resetAnimation(); } else { addPendingAction(RESET_ANIMATION); } } // Current (imperfect) Java AnimatorSet cannot be reversed when the set contains sequential // animators or when the animator set has a start delay @Override public void reverse() { if (!mIsReversible || !mInitialized) { return; } if (useLastSeenTarget()) { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.d(LOGTAG, "Target is set. Reversing VDAnimatorSet from java"); } reverseAnimation(); } else { addPendingAction(REVERSE_ANIMATION); } } // This should only be called after animator has been added to the RenderNode target. private void startAnimation() { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.w(LOGTAG, "starting animation on VD: " + ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState) mDrawable.getConstantState()).mVectorDrawable.getConstantState()) .mRootName); } mStarted = true; nStart(mSetPtr, this, ++mLastListenerId); invalidateOwningView(); if (mListener != null) { mListener.onAnimationStart(null); } } // This should only be called after animator has been added to the RenderNode target. private void endAnimation() { if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.w(LOGTAG, "ending animation on VD: " + ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState) mDrawable.getConstantState()).mVectorDrawable.getConstantState()) .mRootName); } nEnd(mSetPtr); invalidateOwningView(); } // This should only be called after animator has been added to the RenderNode target. private void resetAnimation() { nReset(mSetPtr); invalidateOwningView(); } // This should only be called after animator has been added to the RenderNode target. private void reverseAnimation() { mStarted = true; nReverse(mSetPtr, this, ++mLastListenerId); invalidateOwningView(); if (mListener != null) { mListener.onAnimationStart(null); } } public long getAnimatorNativePtr() { return mSetPtr; } @Override public boolean canReverse() { return mIsReversible; } @Override public boolean isStarted() { return mStarted; } @Override public boolean isRunning() { if (!mInitialized) { return false; } return mStarted; } @Override public void setListener(AnimatorListener listener) { mListener = listener; } @Override public void removeListener(AnimatorListener listener) { mListener = null; } @Override public void onDraw(Canvas canvas) { if (canvas.isHardwareAccelerated()) { recordLastSeenTarget((DisplayListCanvas) canvas); } } @Override public boolean isInfinite() { return mIsInfinite; } @Override public void pause() { // TODO: Implement pause for Animator On RT. } @Override public void resume() { // TODO: Implement resume for Animator On RT. } private void onAnimationEnd(int listenerId) { if (listenerId != mLastListenerId) { return; } if (DBG_ANIMATION_VECTOR_DRAWABLE) { Log.d(LOGTAG, "on finished called from native"); } mStarted = false; // Invalidate in the end of the animation to make sure the data in // RT thread is synced back to UI thread. invalidateOwningView(); if (mListener != null) { mListener.onAnimationEnd(null); } } // onFinished: should be called from native private static void callOnFinished(VectorDrawableAnimatorRT set, int id) { set.onAnimationEnd(id); } } private static native long nCreateAnimatorSet(); private static native void nAddAnimator(long setPtr, long propertyValuesHolder, long nativeInterpolator, long startDelay, long duration, int repeatCount); private static native long nCreateGroupPropertyHolder(long nativePtr, int propertyId, float startValue, float endValue); private static native long nCreatePathDataPropertyHolder(long nativePtr, long startValuePtr, long endValuePtr); private static native long nCreatePathColorPropertyHolder(long nativePtr, int propertyId, int startValue, int endValue); private static native long nCreatePathPropertyHolder(long nativePtr, int propertyId, float startValue, float endValue); private static native long nCreateRootAlphaPropertyHolder(long nativePtr, float startValue, float endValue); private static native void nSetPropertyHolderData(long nativePtr, float[] data, int length); private static native void nStart(long animatorSetPtr, VectorDrawableAnimatorRT set, int id); private static native void nReverse(long animatorSetPtr, VectorDrawableAnimatorRT set, int id); private static native void nEnd(long animatorSetPtr); private static native void nReset(long animatorSetPtr); }