import android.animation.AnimatorSet;
import android.animation.Animator.AnimatorListener;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
* @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation
*/
public class AnimatedVectorDrawable extends Drawable implements Animatable {
- private static final String LOGTAG = AnimatedVectorDrawable.class.getSimpleName();
+ 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();
+
+ /**
+ * 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;
public AnimatedVectorDrawable() {
private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
+ mRes = res;
}
@Override
*/
public void clearMutated() {
super.clearMutated();
- mAnimatedVectorState.mVectorDrawable.clearMutated();
+ if (mAnimatedVectorState.mVectorDrawable != null) {
+ mAnimatedVectorState.mVectorDrawable.clearMutated();
+ }
mMutated = false;
}
@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;
vectorDrawable.setAllowCaching(false);
vectorDrawable.setCallback(mCallback);
pathErrorScale = vectorDrawable.getPixelSize();
- if (mAnimatedVectorState.mVectorDrawable != null) {
- mAnimatedVectorState.mVectorDrawable.setCallback(null);
+ if (state.mVectorDrawable != null) {
+ state.mVectorDrawable.setCallback(null);
}
- mAnimatedVectorState.mVectorDrawable = vectorDrawable;
+ state.mVectorDrawable = vectorDrawable;
}
a.recycle();
} else if (TARGET.equals(tagName)) {
R.styleable.AnimatedVectorDrawableTarget);
final String target = a.getString(
R.styleable.AnimatedVectorDrawableTarget_name);
-
- int id = a.getResourceId(
+ final int animResId = a.getResourceId(
R.styleable.AnimatedVectorDrawableTarget_animation, 0);
- if (id != 0) {
- Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id,
- pathErrorScale);
- setupAnimatorsForTarget(target, objectAnimator);
+ 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();
}
- setupAnimatorSet();
- }
- private void setupAnimatorSet() {
- if (mAnimatedVectorState.mTempAnimators != null) {
- mAnimatedVectorState.mAnimatorSet.playTogether(mAnimatedVectorState.mTempAnimators);
- mAnimatedVectorState.mTempAnimators.clear();
- mAnimatedVectorState.mTempAnimators = null;
- }
+ // 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
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;
+ }
}
/**
* @param listener the listener to be added to the current set of listeners for this animation.
*/
public void addListener(AnimatorListener listener) {
- mAnimatedVectorState.mAnimatorSet.addListener(listener);
+ mAnimatorSet.addListener(listener);
}
/**
* animation.
*/
public void removeListener(AnimatorListener listener) {
- mAnimatedVectorState.mAnimatorSet.removeListener(listener);
+ mAnimatorSet.removeListener(listener);
}
/**
* @return List<AnimatorListener> The set of listeners.
*/
public List<AnimatorListener> getListeners() {
- return mAnimatedVectorState.mAnimatorSet.getListeners();
+ return mAnimatorSet.getListeners();
}
private static class AnimatedVectorDrawableState extends ConstantState {
int mChangingConfigurations;
VectorDrawable mVectorDrawable;
- // Always have a valid animatorSet to handle all the listeners call.
- AnimatorSet mAnimatorSet = new AnimatorSet();
- // When parsing the XML, we build individual animator and store in this array. At the end,
- // we add this array into the mAnimatorSet.
- private ArrayList<Animator> mTempAnimators;
+
+ /** 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.setBounds(copy.mVectorDrawable.getBounds());
mVectorDrawable.setAllowCaching(false);
}
- if (copy.mAnimatorSet != null) {
- final int numAnimators = copy.mTargetNameMap.size();
- // Deep copy a animator set, and then setup the target map again.
- mAnimatorSet = copy.mAnimatorSet.clone();
- mTargetNameMap = new ArrayMap<Animator, String>(numAnimators);
- // Since the new AnimatorSet is cloned from the old one, the order must be the
- // same inside the array.
- ArrayList<Animator> oldAnim = copy.mAnimatorSet.getChildAnimations();
- ArrayList<Animator> newAnim = mAnimatorSet.getChildAnimations();
-
- for (int i = 0; i < numAnimators; ++i) {
- // Target name must be the same for new and old
- String targetName = copy.mTargetNameMap.get(oldAnim.get(i));
-
- Object newTargetObject = mVectorDrawable.getTargetByName(targetName);
- newAnim.get(i).setTarget(newTargetObject);
- mTargetNameMap.put(newAnim.get(i), targetName);
- }
+
+ 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())
- || super.canApplyTheme();
+ || mPendingAnims != null || super.canApplyTheme();
}
@Override
public int getChangingConfigurations() {
return mChangingConfigurations;
}
- }
- private void setupAnimatorsForTarget(String name, Animator animator) {
- Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
- animator.setTarget(target);
- if (mAnimatedVectorState.mTempAnimators == null) {
- mAnimatedVectorState.mTempAnimators = new ArrayList<Animator>();
- mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>();
+ 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);
+ }
}
- mAnimatedVectorState.mTempAnimators.add(animator);
- mAnimatedVectorState.mTargetNameMap.put(animator, name);
- if (DBG_ANIMATION_VECTOR_DRAWABLE) {
- Log.v(LOGTAG, "add animator for target " + name + " " + 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 mAnimatedVectorState.mAnimatorSet.isRunning();
+ return mAnimatorSet.isRunning();
}
private boolean isStarted() {
- return mAnimatedVectorState.mAnimatorSet.isStarted();
+ return mAnimatorSet.isStarted();
}
@Override
public void start() {
+ ensureAnimatorSet();
+
// If any one of the animator has not ended, do nothing.
if (isStarted()) {
return;
}
- mAnimatedVectorState.mAnimatorSet.start();
+
+ mAnimatorSet.start();
invalidateSelf();
}
+ @NonNull
+ private void ensureAnimatorSet() {
+ if (!mHasAnimatorSet) {
+ mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet, mRes);
+ mHasAnimatorSet = true;
+ mRes = null;
+ }
+ }
+
@Override
public void stop() {
- mAnimatedVectorState.mAnimatorSet.end();
+ mAnimatorSet.end();
}
/**
* @hide
*/
public void reverse() {
- // Only reverse when all the animators can be reverse. Otherwise, partially
- // reverse is confusing.
+ ensureAnimatorSet();
+
+ // Only reverse when all the animators can be reversed.
if (!canReverse()) {
Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
return;
}
- mAnimatedVectorState.mAnimatorSet.reverse();
+
+ mAnimatorSet.reverse();
+ invalidateSelf();
}
/**
* @hide
*/
public boolean canReverse() {
- return mAnimatedVectorState.mAnimatorSet.canReverse();
+ return mAnimatorSet.canReverse();
}
private final Callback mCallback = new Callback() {