/* * 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.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 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 {@link android.graphics.drawable.VectorDrawable} with * animations defined using {@link android.animation.ObjectAnimator} or * {@link android.animation.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, * {@link android.graphics.drawable.Animatable2.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. *
* ** Animations can be performed on both group and path attributes, which requires groups and paths to * have unique names in the same VectorDrawable. Groups and paths without animations do not need to * be named. *
* 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> *
* An AnimatedVectorDrawable element has a VectorDrawable attribute, and one or more target * element(s). The target elements can be the path or group to be animated. Each target element * contains a name attribute that references a property (of a path or a group) to animate, and an * animation attribute that points to an ObjectAnimator or an AnimatorSet. *
* 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="@anim/rotation" /> * <target * android:name="v" * android:animation="@anim/path_morph" /> * </animated-vector> **
* 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> **
* 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" > * <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> ** * @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; /** * 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
* 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. *
* * @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. * * @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; 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 */ 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. */ ArrayList
* 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
* 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