/*
 * 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.Animator.AnimatorListener;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
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.Property;
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 dalvik.annotation.optimization.FastNative;

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 animates properties of a VectorDrawable with animations defined using ObjectAnimator or AnimatorSet.

Starting from API 25, AnimatedVectorDrawable runs on RenderThread (as opposed to on UI thread for earlier APIs). This means animations in AnimatedVectorDrawable can remain smooth even when there is heavy workload on the UI thread. Note: If the UI thread is unresponsive, RenderThread may continue animating until the UI thread is capable of pushing another frame. Therefore, it is not possible to precisely coordinate a RenderThread-enabled AnimatedVectorDrawable with UI thread animations. Additionally, AnimationCallback.onAnimationEnd(Drawable) will be called the frame after the AnimatedVectorDrawable finishes on the RenderThread.

AnimatedVectorDrawable can be defined in either three separate XML files, or one XML.

Define an AnimatedVectorDrawable in three separate XML files

  • XML for the VectorDrawable containing properties to be animated

    Animations can be performed on the animatable attributes in VectorDrawable. These attributes will be animated by ObjectAnimator. The ObjectAnimator's target can be the root element, a group element or a path element. The targeted elements need to be named uniquely within the same VectorDrawable. Elements without animation do not need to be named.

    Here are all the animatable attributes in VectorDrawable:

    Element Name Animatable attribute name
    <vector> alpha
    <group> rotation
    pivotX
    pivotY
    scaleX
    scaleY
    translateX
    translateY
    <path> pathData
    fillColor
    strokeColor
    strokeWidth
    strokeAlpha
    fillAlpha
    trimPathStart
    trimPathEnd
    trimPathOffset
    <clip-path> pathData

    Below is an example of a VectorDrawable defined in vectordrawable.xml. This VectorDrawable is referred to by its file name (not including file suffix) in the AnimatedVectorDrawable XML example.
    <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>
    
  • XML for AnimatedVectorDrawable

    An AnimatedVectorDrawable element has a VectorDrawable attribute, and one or more target element(s). The target element can specify its target by android:name attribute, and link the target with the proper ObjectAnimator or AnimatorSet by android:animation attribute.

    The following code sample defines an AnimatedVectorDrawable. Note that the names refer to the groups and paths in the VectorDrawable XML above.
    <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:drawable="@drawable/vectordrawable" >
        <target
            android:name="rotationGroup"
            android:animation="@animator/rotation" />
        <target
            android:name="v"
            android:animation="@animator/path_morph" />
    </animated-vector>
    
  • XML for Animations defined using ObjectAnimator or AnimatorSet

    From the previous example of AnimatedVectorDrawable, two animations were used: rotation.xml and path_morph.xml.

    rotation.xml rotates the target group from 0 degree to 360 degrees over 6000ms:
    <objectAnimator
        android:duration="6000"
        android:propertyName="rotation"
        android:valueFrom="0"
        android:valueTo="360" />
    
    path_morph.xml morphs the path from one shape into the other. Note that the paths must be compatible for morphing. Specifically, the paths must have the same commands, in the same order, and must have the same number of parameters for each command. It is recommended to store path strings as string resources for reuse.
    <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>
    

Define an AnimatedVectorDrawable all in one XML file

Since the AAPT tool supports a new format that bundles several related XML files together, we can merge the XML files from the previous examples into one XML file:

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:aapt=&quothttp://schemas.android.com/aapt" >
    <aapt:attr name="android:drawable">
        <vector
            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>
    </aapt:attr>
    <target android:name="rotationGroup"> *
        <aapt:attr name="android:animation">
            <objectAnimator
            android:duration="6000"
            android:propertyName="rotation"
            android:valueFrom="0"
            android:valueTo="360" />
        </aapt:attr>
    </target>
    <target android:name="v" >
        <aapt:attr name="android:animation">
            <set>
                <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>
        </aapt:attr>
     </target>
</animated-vector>
@attrref android.R.styleable#AnimatedVectorDrawable_drawable
@attrref android.R.styleable#AnimatedVectorDrawableTarget_name
@attrref android.R.styleable#AnimatedVectorDrawableTarget_animation
/** * This class animates properties of a {@link android.graphics.drawable.VectorDrawable} with * animations defined using {@link android.animation.ObjectAnimator} or * {@link android.animation.AnimatorSet}. * <p> * Starting from API 25, AnimatedVectorDrawable runs on RenderThread (as opposed to on UI thread for * earlier APIs). This means animations in AnimatedVectorDrawable can remain smooth even when there * is heavy workload on the UI thread. Note: If the UI thread is unresponsive, RenderThread may * continue animating until the UI thread is capable of pushing another frame. Therefore, it is not * possible to precisely coordinate a RenderThread-enabled AnimatedVectorDrawable with UI thread * animations. Additionally, * {@link android.graphics.drawable.Animatable2.AnimationCallback#onAnimationEnd(Drawable)} will be * called the frame after the AnimatedVectorDrawable finishes on the RenderThread. * </p> * <p> * AnimatedVectorDrawable can be defined in either <a href="#ThreeXML">three separate XML files</a>, * or <a href="#OneXML">one XML</a>. * </p> * <a name="ThreeXML"></a> * <h3>Define an AnimatedVectorDrawable in three separate XML files</h3> * <ul> * <a name="VDExample"></a> * <li><h4>XML for the VectorDrawable containing properties to be animated</h4> * <p> * Animations can be performed on the animatable attributes in * {@link android.graphics.drawable.VectorDrawable}. These attributes will be animated by * {@link android.animation.ObjectAnimator}. The ObjectAnimator's target can be the root element, * a group element or a path element. The targeted elements need to be named uniquely within * the same VectorDrawable. Elements without animation do not need to be named. * </p> * <p> * Here are all the animatable attributes in {@link android.graphics.drawable.VectorDrawable}: * <table border="2" align="center" cellpadding="5"> * <thead> * <tr> * <th>Element Name</th> * <th>Animatable attribute name</th> * </tr> * </thead> * <tr> * <td>&lt;vector&gt;</td> * <td>alpha</td> * </tr> * <tr> * <td rowspan="7">&lt;group&gt;</td> * <td>rotation</td> * </tr> * <tr> * <td>pivotX</td> * </tr> * <tr> * <td>pivotY</td> * </tr> * <tr> * <td>scaleX</td> * </tr> * <tr> * <td>scaleY</td> * </tr> * <tr> * <td>translateX</td> * </tr> * <tr> * <td>translateY</td> * </tr> * <tr> * <td rowspan="9">&lt;path&gt;</td> * <td>pathData</td> * </tr> * <tr> * <td>fillColor</td> * </tr> * <tr> * <td>strokeColor</td> * </tr> * <tr> * <td>strokeWidth</td> * </tr> * <tr> * <td>strokeAlpha</td> * </tr> * <tr> * <td>fillAlpha</td> * </tr> * <tr> * <td>trimPathStart</td> * </tr> * <tr> * <td>trimPathEnd</td> * </tr> * <tr> * <td>trimPathOffset</td> * </tr> * <tr> * <td>&lt;clip-path&gt;</td> * <td>pathData</td> * </tr> * </table> * </p> * Below is an example of a VectorDrawable defined in vectordrawable.xml. This VectorDrawable is * referred to by its file name (not including file suffix) in the * <a href="#AVDExample">AnimatedVectorDrawable XML example</a>. * <pre> * &lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; * android:height=&quot;64dp&quot; * android:width=&quot;64dp&quot; * android:viewportHeight=&quot;600&quot; * android:viewportWidth=&quot;600&quot; &gt; * &lt;group * android:name=&quot;rotationGroup&quot; * android:pivotX=&quot;300.0&quot; * android:pivotY=&quot;300.0&quot; * android:rotation=&quot;45.0&quot; &gt; * &lt;path * android:name=&quot;v&quot; * android:fillColor=&quot;#000000&quot; * android:pathData=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; /&gt; * &lt;/group&gt; * &lt;/vector&gt; * </pre></li> * * <a name="AVDExample"></a> * <li><h4>XML for AnimatedVectorDrawable</h4> * <p> * An AnimatedVectorDrawable element has a VectorDrawable attribute, and one or more target * element(s). The target element can specify its target by android:name attribute, and link the * target with the proper ObjectAnimator or AnimatorSet by android:animation attribute. * </p> * The following code sample defines an AnimatedVectorDrawable. Note that the names refer to the * groups and paths in the <a href="#VDExample">VectorDrawable XML above</a>. * <pre> * &lt;animated-vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; * android:drawable=&quot;@drawable/vectordrawable&quot; &gt; * &lt;target * android:name=&quot;rotationGroup&quot; * android:animation=&quot;@animator/rotation&quot; /&gt; * &lt;target * android:name=&quot;v&quot; * android:animation=&quot;@animator/path_morph&quot; /&gt; * &lt;/animated-vector&gt; * </pre> * </li> * * <li><h4>XML for Animations defined using ObjectAnimator or AnimatorSet</h4> * <p> * From the previous <a href="#AVDExample">example of AnimatedVectorDrawable</a>, two animations * were used: rotation.xml and path_morph.xml. * </p> * rotation.xml rotates the target group from 0 degree to 360 degrees over 6000ms: * <pre> * &lt;objectAnimator * android:duration=&quot;6000&quot; * android:propertyName=&quot;rotation&quot; * android:valueFrom=&quot;0&quot; * android:valueTo=&quot;360&quot; /&gt; * </pre> * * path_morph.xml morphs the path from one shape into the other. Note that the paths must be * compatible for morphing. Specifically, the paths must have the same commands, in the same order, * and must have the same number of parameters for each command. It is recommended to store path * strings as string resources for reuse. * <pre> * &lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt; * &lt;objectAnimator * android:duration=&quot;3000&quot; * android:propertyName=&quot;pathData&quot; * android:valueFrom=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; * android:valueTo=&quot;M300,70 l 0,-70 70,0 0,140 -70,0 z&quot; * android:valueType=&quot;pathType&quot;/&gt; * &lt;/set&gt; * </pre> * </ul> * <a name="OneXML"></a> * <h3>Define an AnimatedVectorDrawable all in one XML file</h3> * <p> * Since the AAPT tool supports a new format that bundles several related XML files together, we can * merge the XML files from the previous examples into one XML file: * </p> * <pre> * &lt;animated-vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; * xmlns:aapt=&quothttp://schemas.android.com/aapt&quot; &gt; * &lt;aapt:attr name="android:drawable"&gt; * &lt;vector * android:height=&quot;64dp&quot; * android:width=&quot;64dp&quot; * android:viewportHeight=&quot;600&quot; * android:viewportWidth=&quot;600&quot; &gt; * &lt;group * android:name=&quot;rotationGroup&quot; * android:pivotX=&quot;300.0&quot; * android:pivotY=&quot;300.0&quot; * android:rotation=&quot;45.0&quot; &gt; * &lt;path * android:name=&quot;v&quot; * android:fillColor=&quot;#000000&quot; * android:pathData=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; /&gt; * &lt;/group&gt; * &lt;/vector&gt; * &lt;/aapt:attr&gt; * * &lt;target android:name=&quot;rotationGroup&quot;&gt; * * &lt;aapt:attr name="android:animation"&gt; * &lt;objectAnimator * android:duration=&quot;6000&quot; * android:propertyName=&quot;rotation&quot; * android:valueFrom=&quot;0&quot; * android:valueTo=&quot;360&quot; /&gt; * &lt;/aapt:attr&gt; * &lt;/target&gt; * * &lt;target android:name=&quot;v&quot; &gt; * &lt;aapt:attr name="android:animation"&gt; * &lt;set&gt; * &lt;objectAnimator * android:duration=&quot;3000&quot; * android:propertyName=&quot;pathData&quot; * android:valueFrom=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; * android:valueTo=&quot;M300,70 l 0,-70 70,0 0,140 -70,0 z&quot; * android:valueType=&quot;pathType&quot;/&gt; * &lt;/set&gt; * &lt;/aapt:attr&gt; * &lt;/target&gt; * &lt;/animated-vector&gt; * </pre> * * @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.
/** Local, mutable animator set. */
private VectorDrawableAnimator mAnimatorSet;
The resources against which this drawable was created. Used to attempt to inflate animators if applyTheme() doesn't get called.
/** * 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.
/** 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.
/** Use a internal AnimatorListener to support callbacks during animation events. */
private ArrayList<Animatable2.AnimationCallback> mAnimationCallbacks = null; private AnimatorListener mAnimatorListener = null; public AnimatedVectorDrawable() { this(null, null); } private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); mAnimatorSet = new VectorDrawableAnimatorRT(this); mRes = res; } @Override public Drawable mutate() { if (!mMutated && super.mutate() == this) { mAnimatedVectorState = new AnimatedVectorDrawableState( mAnimatedVectorState, mCallback, mRes); mMutated = true; } return this; }
@hide
/** * @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.
Returns:whether invalid animations for vector drawable should be ignored.
/** * 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(); }
Draws the AnimatedVectorDrawable into the given canvas.

Note: Calling this method with a software canvas when the AnimatedVectorDrawable is being animated on RenderThread (for API 25 and later) may yield outdated result, as the UI thread is not guaranteed to be in sync with RenderThread on VectorDrawable's property changes during RenderThread animations.

Params:
  • canvas – The canvas to draw into
/** * Draws the AnimatedVectorDrawable into the given canvas. * <p> * <strong>Note:</strong> Calling this method with a software canvas when the * AnimatedVectorDrawable is being animated on RenderThread (for API 25 and later) may yield * outdated result, as the UI thread is not guaranteed to be in sync with RenderThread on * VectorDrawable's property changes during RenderThread animations. * </p> * * @param canvas The canvas to draw into */
@Override public void draw(Canvas canvas) { if (!canvas.isHardwareAccelerated() && mAnimatorSet instanceof VectorDrawableAnimatorRT) { // If we have SW canvas and the RT animation is waiting to start, We need to fallback // to UI thread animation for AVD. if (!mAnimatorSet.isRunning() && ((VectorDrawableAnimatorRT) mAnimatorSet).mPendingAnimationActions.size() > 0) { fallbackOntoUI(); } } 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); }
For API 25 and later, AnimatedVectorDrawable runs on RenderThread. Therefore, when the root alpha is being animated, this getter does not guarantee to return an up-to-date alpha value.
Returns:the containing vector drawable's root alpha value.
/** * For API 25 and later, AnimatedVectorDrawable runs on RenderThread. Therefore, when the * root alpha is being animated, this getter does not guarantee to return an up-to-date alpha * 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
/** @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; final int innerDepth = parser.getDepth() + 1; // Parse everything until the end of the animated-vector element. while (eventType != XmlPullParser.END_DOCUMENT && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) { 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) { // The animator here could be ObjectAnimator or AnimatorSet. final Animator animator = AnimatorInflater.loadAnimator( res, theme, animResId, pathErrorScale); updateAnimatorProperty(animator, target, state.mVectorDrawable, state.mShouldIgnoreInvalidAnim); state.addTargetAnimator(target, animator); } 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; } private static void updateAnimatorProperty(Animator animator, String targetName, VectorDrawable vectorDrawable, boolean ignoreInvalidAnim) { if (animator instanceof ObjectAnimator) { // Change the property of the Animator from using reflection based on the property // name to a Property object that wraps the setter and getter for modifying that // specific property for a given object. By replacing the reflection with a direct call, // we can largely reduce the time it takes for a animator to modify a VD property. PropertyValuesHolder[] holders = ((ObjectAnimator) animator).getValues(); for (int i = 0; i < holders.length; i++) { PropertyValuesHolder pvh = holders[i]; String propertyName = pvh.getPropertyName(); Object targetNameObj = vectorDrawable.getTargetByName(targetName); Property property = null; if (targetNameObj instanceof VectorDrawable.VObject) { property = ((VectorDrawable.VObject) targetNameObj).getProperty(propertyName); } else if (targetNameObj instanceof VectorDrawable.VectorDrawableState) { property = ((VectorDrawable.VectorDrawableState) targetNameObj) .getProperty(propertyName); } if (property != null) { if (containsSameValueType(pvh, property)) { pvh.setProperty(property); } else if (!ignoreInvalidAnim) { throw new RuntimeException("Wrong valueType for Property: " + propertyName + ". Expected type: " + property.getType().toString() + ". Actual " + "type defined in resources: " + pvh.getValueType().toString()); } } } } else if (animator instanceof AnimatorSet) { for (Animator anim : ((AnimatorSet) animator).getChildAnimations()) { updateAnimatorProperty(anim, targetName, vectorDrawable, ignoreInvalidAnim); } } } private static boolean containsSameValueType(PropertyValuesHolder holder, Property property) { Class type1 = holder.getValueType(); Class type2 = property.getType(); if (type1 == float.class || type1 == Float.class) { return type2 == float.class || type2 == Float.class; } else if (type1 == int.class || type1 == Integer.class) { return type2 == int.class || type2 == Integer.class; } else { return type1 == type2; } }
Force to animate on UI thread.
@hide
/** * 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."); } fallbackOntoUI(); } } private void fallbackOntoUI() { if (mAnimatorSet instanceof VectorDrawableAnimatorRT) { VectorDrawableAnimatorRT oldAnim = (VectorDrawableAnimatorRT) mAnimatorSet; mAnimatorSet = new VectorDrawableAnimatorUI(this); if (mAnimatorSetFromXml != null) { mAnimatorSet.init(mAnimatorSetFromXml); } // Transfer the listener from RT animator to UI animator if (oldAnim.mListener != null) { mAnimatorSet.setListener(oldAnim.mListener); } oldAnim.transferPendingActions(mAnimatorSet); } } @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; private final boolean mShouldIgnoreInvalidAnim;
Animators that require a theme before inflation.
/** Animators that require a theme before inflation. */
ArrayList<PendingAnimator> mPendingAnims;
Fully inflated animators awaiting cloning into an AnimatorSet.
/** Fully inflated animators awaiting cloning into an AnimatorSet. */
ArrayList<Animator> mAnimators;
Map of animators to their target object names
/** Map of animators to their target object names */
ArrayMap<Animator, String> mTargetNameMap; public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, Callback owner, Resources res) { mShouldIgnoreInvalidAnim = shouldIgnoreInvalidAnimation(); 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.

Params:
  • animatorSet – the animator set to which the animators should be added
  • res – the resources against which to inflate any pending animators, or null if not available
/** * Prepares a local set of mutable animators based on the constant * state. * <p> * 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.
Params:
  • index – the index of the animator within the constant state
/** * 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); if (!mShouldIgnoreInvalidAnim) { if (target == null) { throw new IllegalStateException("Target with the name \"" + targetName + "\" cannot be found in the VectorDrawable to be animated."); } else if (!(target instanceof VectorDrawable.VectorDrawableState) && !(target instanceof VectorDrawable.VObject)) { throw new UnsupportedOperationException("Target should be either VGroup, VPath," + " or ConstantState, " + target.getClass() + " is not supported"); } } localAnimator.setTarget(target); return localAnimator; }
Inflates pending animators, if any, against a theme. Clears the list of pending animators.
Params:
  • t – the theme against which to inflate the animators
/** * 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<PendingAnimator> 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 animator = pendingAnimator.newInstance(res, t); updateAnimatorProperty(animator, pendingAnimator.target, mVectorDrawable, mShouldIgnoreInvalidAnim); addTargetAnimator(pendingAnimator.target, animator); } } }
Basically a constant state for Animators until we actually implement constant states for Animators.
/** * 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.
/** * 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
/** * Reverses ongoing animations or starts pending animations in reverse. * <p> * 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
/** * @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<AnimationCallback> 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<AnimationCallback> 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<AnimatorListener> 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<AnimatorListener>(); } 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
/** * @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; // If the duration of an animation is more than 300 frames, we cap the sample size to 300. private static final int MAX_SAMPLE_POINTS = 300; 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; // TODO: Consider using NativeAllocationRegistery to track native allocation private final VirtualRefBasePtr mSetRefBasePtr; private WeakReference<RenderNode> mLastSeenTarget = null; private int mLastListenerId = 0; private final IntArray mPendingAnimationActions = new IntArray(); private final AnimatedVectorDrawable 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"); } parseAnimatorSet(set, 0); long vectorDrawableTreePtr = mDrawable.mAnimatedVectorState.mVectorDrawable .getNativeTree(); nSetVectorDrawableTarget(mSetPtr, vectorDrawableTreePtr); 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<Animator> 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 (!mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) { throw new IllegalArgumentException("ClipPath only supports PathData " + "property"); } } } else if (target instanceof VectorDrawable.VectorDrawableState) { createRTAnimatorForRootGroup(values, animator, (VectorDrawable.VectorDrawableState) target, startTime); } } 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 = createFloatDataPoints(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 (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) { return; } else { throw new IllegalArgumentException("Property: " + mTmpValues.propertyName + " is not supported for FullPath"); } } propertyPtr = nCreatePathPropertyHolder(nativePtr, propertyId, (Float) mTmpValues.startValue, (Float) mTmpValues.endValue); if (mTmpValues.dataSource != null) { // Pass keyframe data to native, if any. float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource, animator.getDuration()); nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length); } } else if (mTmpValues.type == Integer.class || mTmpValues.type == int.class) { propertyPtr = nCreatePathColorPropertyHolder(nativePtr, propertyId, (Integer) mTmpValues.startValue, (Integer) mTmpValues.endValue); if (mTmpValues.dataSource != null) { // Pass keyframe data to native, if any. int[] dataPoints = createIntDataPoints(mTmpValues.dataSource, animator.getDuration()); nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length); } } else { if (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) { return; } else { throw new UnsupportedOperationException("Unsupported type: " + mTmpValues.type + ". Only float, int or PathData value is " + "supported for Paths."); } } 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 (mDrawable.mAnimatedVectorState.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 (mDrawable.mAnimatedVectorState.mShouldIgnoreInvalidAnim) { return; } else { throw new UnsupportedOperationException("No alpha values are specified"); } } long propertyPtr = nCreateRootAlphaPropertyHolder(nativePtr, startValue, endValue); if (mTmpValues.dataSource != null) { // Pass keyframe data to native, if any. float[] dataPoints = createFloatDataPoints(mTmpValues.dataSource, animator.getDuration()); nSetPropertyHolderData(propertyPtr, dataPoints, dataPoints.length); } createNativeChildAnimator(propertyPtr, startTime, animator); }
Calculate the amount of frames an animation will run based on duration.
/** * Calculate the amount of frames an animation will run based on duration. */
private static int getFrameCount(long duration) { long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos(); int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS); int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs); // We need 2 frames of data minimum. numAnimFrames = Math.max(2, numAnimFrames); if (numAnimFrames > MAX_SAMPLE_POINTS) { Log.w("AnimatedVectorDrawable", "Duration for the animation is too long :" + duration + ", the animation will subsample the keyframe or path data."); numAnimFrames = MAX_SAMPLE_POINTS; } return numAnimFrames; } // 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[] createFloatDataPoints( PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) { int numAnimFrames = getFrameCount(duration); 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 static int[] createIntDataPoints( PropertyValuesHolder.PropertyValues.DataSource dataSource, long duration) { int numAnimFrames = getFrameCount(duration); int values[] = new int[numAnimFrames]; float lastFrame = numAnimFrames - 1; for (int i = 0; i < numAnimFrames; i++) { float fraction = i / lastFrame; values[i] = (Integer) 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, animator.getRepeatMode()); }
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.
/** * 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) { final RenderNode node = RenderNodeAnimatorSetHelper.getTarget(canvas); mLastSeenTarget = new WeakReference<RenderNode>(node); // Add the animator to the list of animators on every draw if (mInitialized || mPendingAnimationActions.size() > 0) { if (useTarget(node)) { 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(); return useTarget(target); } return false; } private boolean useTarget(RenderNode target) { if (target != null && target.isAttached()) { target.registerVectorDrawableAnimator(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 void transferPendingActions(VectorDrawableAnimator animatorSet) { for (int i = 0; i < mPendingAnimationActions.size(); i++) { int pendingAction = mPendingAnimationActions.get(i); if (pendingAction == START_ANIMATION) { animatorSet.start(); } else if (pendingAction == END_ANIMATION) { animatorSet.end(); } else if (pendingAction == REVERSE_ANIMATION) { animatorSet.reverse(); } else if (pendingAction == RESET_ANIMATION) { animatorSet.reset(); } else { throw new UnsupportedOperationException("Animation action " + pendingAction + "is not supported"); } } mPendingAnimationActions.clear(); } } private static native long nCreateAnimatorSet(); private static native void nSetVectorDrawableTarget(long animatorPtr, long vectorDrawablePtr); private static native void nAddAnimator(long setPtr, long propertyValuesHolder, long nativeInterpolator, long startDelay, long duration, int repeatCount, int repeatMode); private static native void nSetPropertyHolderData(long nativePtr, float[] data, int length); private static native void nSetPropertyHolderData(long nativePtr, int[] 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); // ------------- @FastNative ------------------- @FastNative private static native long nCreateGroupPropertyHolder(long nativePtr, int propertyId, float startValue, float endValue); @FastNative private static native long nCreatePathDataPropertyHolder(long nativePtr, long startValuePtr, long endValuePtr); @FastNative private static native long nCreatePathColorPropertyHolder(long nativePtr, int propertyId, int startValue, int endValue); @FastNative private static native long nCreatePathPropertyHolder(long nativePtr, int propertyId, float startValue, float endValue); @FastNative private static native long nCreateRootAlphaPropertyHolder(long nativePtr, float startValue, float endValue); @FastNative private static native void nEnd(long animatorSetPtr); @FastNative private static native void nReset(long animatorSetPtr); }