The goal for this change is to get the timing for animation update. Since AnimatorSet does not support update listeners, we have to go with the a different approach, which sets up a value animator that runs between the start and end of the AnimatorSet. We can then get the animation update timing from the ValueAnimator. Bug: 26329675 Change-Id: Ibe7fce81eb6da5e05c87a732c1d3bc904b4e5e6f (cherry picked from commit I378a0964da8f7090f65f6b56275a302e30668835)
771 lines
28 KiB
Java
771 lines
28 KiB
Java
/*
|
|
* 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.ValueAnimator;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
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.PorterDuff;
|
|
import android.graphics.Rect;
|
|
import android.util.ArrayMap;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
|
|
import com.android.internal.R;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import java.io.IOException;
|
|
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.
|
|
* <p>
|
|
* AnimatedVectorDrawable are normally defined as 3 separate XML files.
|
|
* </p>
|
|
* <p>
|
|
* 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.
|
|
* </p>
|
|
* <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
|
|
* <pre>
|
|
* <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>
|
|
* </pre></li>
|
|
* <p>
|
|
* 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.
|
|
* </p>
|
|
* <li>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.
|
|
* <pre>
|
|
* <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>
|
|
* </pre></li>
|
|
* <p>
|
|
* 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.
|
|
* </p>
|
|
* <li>Here is the rotation.xml, which will rotate the target group for 360 degrees.
|
|
* <pre>
|
|
* <objectAnimator
|
|
* android:duration="6000"
|
|
* android:propertyName="rotation"
|
|
* android:valueFrom="0"
|
|
* android:valueTo="360" />
|
|
* </pre></li>
|
|
* <li>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.
|
|
* <pre>
|
|
* <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>
|
|
* </pre></li>
|
|
*
|
|
* @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 final AnimatorSet mAnimatorSet = new AnimatorSet();
|
|
|
|
// Setup a value animator to get animation update callbacks.
|
|
private final ValueAnimator mUpdateAnim = ValueAnimator.ofFloat(0f, 1f);
|
|
private final ValueAnimator.AnimatorUpdateListener mUpdateListener =
|
|
new ValueAnimator.AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
invalidateSelf();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/** Whether the animator set has been prepared. */
|
|
private boolean mHasAnimatorSet;
|
|
|
|
private boolean mMutated;
|
|
|
|
/** 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);
|
|
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;
|
|
}
|
|
|
|
@Override
|
|
public ConstantState getConstantState() {
|
|
mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
|
|
return mAnimatedVectorState;
|
|
}
|
|
|
|
@Override
|
|
public int getChangingConfigurations() {
|
|
return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations();
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
mAnimatedVectorState.mVectorDrawable.draw(canvas);
|
|
if (isStarted()) {
|
|
invalidateSelf();
|
|
}
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
@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 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) {
|
|
mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
|
|
return super.setVisible(visible, restart);
|
|
}
|
|
|
|
@Override
|
|
public boolean isStateful() {
|
|
return mAnimatedVectorState.mVectorDrawable.isStateful();
|
|
}
|
|
|
|
@Override
|
|
public int getOpacity() {
|
|
return mAnimatedVectorState.mVectorDrawable.getOpacity();
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
@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 {
|
|
int mChangingConfigurations;
|
|
VectorDrawable mVectorDrawable;
|
|
|
|
/** Animators that require a theme before inflation. */
|
|
ArrayList<PendingAnimator> mPendingAnims;
|
|
|
|
/** Fully inflated animators awaiting cloning into an AnimatorSet. */
|
|
ArrayList<Animator> mAnimators;
|
|
|
|
/** Map of animators to their target object names */
|
|
ArrayMap<Animator, String> 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 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.
|
|
* <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.
|
|
*
|
|
* @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<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 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();
|
|
}
|
|
|
|
private boolean isStarted() {
|
|
return mAnimatorSet.isStarted();
|
|
}
|
|
|
|
/**
|
|
* Resets the AnimatedVectorDrawable to the start state as specified in the animators.
|
|
*/
|
|
public void reset() {
|
|
// TODO: Use reverse or seek to implement reset, when AnimatorSet supports them.
|
|
start();
|
|
mAnimatorSet.cancel();
|
|
}
|
|
|
|
@Override
|
|
public void start() {
|
|
ensureAnimatorSet();
|
|
|
|
// If any one of the animator has not ended, do nothing.
|
|
if (isStarted()) {
|
|
return;
|
|
}
|
|
|
|
mAnimatorSet.start();
|
|
invalidateSelf();
|
|
}
|
|
|
|
@NonNull
|
|
private void ensureAnimatorSet() {
|
|
if (!mHasAnimatorSet) {
|
|
mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet, mRes);
|
|
mHasAnimatorSet = true;
|
|
// Setup an infinitely running ValueAnimator, start it when AnimatorSet starts and
|
|
// end it when AnimatorSet ends, so we get the animation update timing for
|
|
// invalidating the drawable. Ideally, we would set an update listener on AnimatorSet,
|
|
// but since AnimatorSet doesn't support that yet, this is the alternative to achieve
|
|
// the same goal.
|
|
mUpdateAnim.setRepeatCount(ValueAnimator.INFINITE);
|
|
mUpdateAnim.addUpdateListener(mUpdateListener);
|
|
mAnimatorSet.addListener(new AnimatorListener() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
mUpdateAnim.start();
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mUpdateAnim.end();
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(Animator animation) {
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationRepeat(Animator animation) {
|
|
}
|
|
});
|
|
mRes = null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
mAnimatorSet.end();
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
invalidateSelf();
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
public boolean canReverse() {
|
|
return mAnimatorSet.canReverse();
|
|
}
|
|
|
|
private final Callback mCallback = new Callback() {
|
|
@Override
|
|
public void invalidateDrawable(Drawable who) {
|
|
invalidateSelf();
|
|
}
|
|
|
|
@Override
|
|
public void scheduleDrawable(Drawable who, Runnable what, long when) {
|
|
scheduleSelf(what, when);
|
|
}
|
|
|
|
@Override
|
|
public void unscheduleDrawable(Drawable who, 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.addListener(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();
|
|
}
|
|
|
|
}
|