From d8d9a75f6a111b962bf5260a4dca87398405d95b Mon Sep 17 00:00:00 2001 From: Alan Viverette Date: Mon, 18 May 2015 18:53:46 -0700 Subject: [PATCH] Postpone AnimatedVectorDrawable animator inflation until applyTheme() This CL works around Animator's lack of support for applying a theme after inflation by postponing Animator inflation until a theme is available, either in inflate() or applyTheme(). Includes a workaround for AVDs that don't reference any theme attrs in their animators and this don't require a call to applyTheme(). Partial implementation of removing non-constant data from the AVD's constant state. Moves the mutable AnimatorSet to a local variable and treats the Animators as constant data. We'll follow up with real support for applyTheme() in Animator or AnimatorSet, at which point we can remove this workaround. Bug: 20817800 Change-Id: I555c53c955219990ee376bee568bcc038532f9ed --- .../graphics/drawable/AnimatedVectorDrawable.java | 273 ++++++++++++++++----- 1 file changed, 208 insertions(+), 65 deletions(-) diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java index 28c26ff38683..4b610cd700a5 100644 --- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java @@ -19,6 +19,7 @@ import android.animation.AnimatorInflater; 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; @@ -127,15 +128,27 @@ import java.util.List; * @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() { @@ -144,6 +157,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) { mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res); + mRes = res; } @Override @@ -161,7 +175,9 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { */ public void clearMutated() { super.clearMutated(); - mAnimatedVectorState.mVectorDrawable.clearMutated(); + if (mAnimatedVectorState.mVectorDrawable != null) { + mAnimatedVectorState.mVectorDrawable.clearMutated(); + } mMutated = false; } @@ -273,6 +289,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { @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; @@ -290,10 +307,10 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { 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)) { @@ -301,13 +318,21 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { 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(); } @@ -315,15 +340,10 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { 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 @@ -340,6 +360,16 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { 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; + } } /** @@ -349,7 +379,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { * @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); } /** @@ -359,7 +389,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { * animation. */ public void removeListener(AnimatorListener listener) { - mAnimatedVectorState.mAnimatorSet.removeListener(listener); + mAnimatorSet.removeListener(listener); } /** @@ -369,23 +399,27 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { * @return List The set of listeners. */ public List 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 mTempAnimators; + + /** Animators that require a theme before inflation. */ + ArrayList mPendingAnims; + + /** Fully inflated animators awaiting cloning into an AnimatorSet. */ + ArrayList mAnimators; + + /** Map of animators to their target object names */ ArrayMap mTargetNameMap; public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy, Callback owner, Resources res) { if (copy != null) { mChangingConfigurations = copy.mChangingConfigurations; + if (copy.mVectorDrawable != null) { final ConstantState cs = copy.mVectorDrawable.getConstantState(); if (res != null) { @@ -399,24 +433,17 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { 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(numAnimators); - // Since the new AnimatorSet is cloned from the old one, the order must be the - // same inside the array. - ArrayList oldAnim = copy.mAnimatorSet.getChildAnimations(); - ArrayList 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(); @@ -426,7 +453,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { @Override public boolean canApplyTheme() { return (mVectorDrawable != null && mVectorDrawable.canApplyTheme()) - || super.canApplyTheme(); + || mPendingAnims != null || super.canApplyTheme(); } @Override @@ -443,44 +470,157 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { 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(); - mAnimatedVectorState.mTargetNameMap = new ArrayMap(); + 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. + *

+ * If there are any pending uninflated animators, attempts to inflate + * them immediately against the provided resources object. + * + * @param animatorSet the animator set to which the animators should + * be added + * @param res the resources against which to inflate any pending + * animators, or {@code null} if not available + */ + public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet, + @Nullable Resources res) { + // Check for uninflated animators. We can remove this after we add + // support for Animator.applyTheme(). See comments in inflate(). + if (mPendingAnims != null) { + // Attempt to load animators without applying a theme. + if (res != null) { + inflatePendingAnimators(res, null); + } else { + Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable" + + " must be created using a Resources object or applyTheme() must be" + + " called with a non-null Theme object."); + } + + mPendingAnims = null; + } + + // Perform a deep copy of the constant state's animators. + final int count = mAnimators == null ? 0 : mAnimators.size(); + if (count > 0) { + final Animator firstAnim = prepareLocalAnimator(0); + final AnimatorSet.Builder builder = animatorSet.play(firstAnim); + for (int i = 1; i < count; ++i) { + final Animator nextAnim = prepareLocalAnimator(i); + builder.with(nextAnim); + } + } + } + + /** + * Prepares a local animator for the given index within the constant + * state's list of animators. + * + * @param index the index of the animator within the constant state + */ + private Animator prepareLocalAnimator(int index) { + final Animator animator = mAnimators.get(index); + final Animator localAnimator = animator.clone(); + final String targetName = mTargetNameMap.get(animator); + final Object target = mVectorDrawable.getTargetByName(targetName); + localAnimator.setTarget(target); + return localAnimator; + } + + /** + * Inflates pending animators, if any, against a theme. Clears the list of + * pending animators. + * + * @param t the theme against which to inflate the animators + */ + public void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) { + final ArrayList pendingAnims = mPendingAnims; + if (pendingAnims != null) { + mPendingAnims = null; + + for (int i = 0, count = pendingAnims.size(); i < count; i++) { + final PendingAnimator pendingAnimator = pendingAnims.get(i); + final Animator objectAnimator = pendingAnimator.newInstance(res, t); + addTargetAnimator(pendingAnimator.target, objectAnimator); + } + } + } + + /** + * Basically a constant state for Animators until we actually implement + * constant states for Animators. + */ + private static class PendingAnimator { + public final int animResId; + public final float pathErrorScale; + public final String target; + + public PendingAnimator(int animResId, float pathErrorScale, String target) { + this.animResId = animResId; + this.pathErrorScale = pathErrorScale; + this.target = target; + } + + public Animator newInstance(Resources res, Theme theme) { + return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale); + } } } @Override public boolean isRunning() { - return 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(); } /** @@ -491,20 +631,23 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable { * @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() { -- 2.11.0