AnimatedVectorDrawable.java revision d8d9a75f6a111b962bf5260a4dca87398405d95b
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package android.graphics.drawable;
16
17import android.animation.Animator;
18import android.animation.AnimatorInflater;
19import android.animation.AnimatorSet;
20import android.animation.Animator.AnimatorListener;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.res.ColorStateList;
24import android.content.res.Resources;
25import android.content.res.Resources.Theme;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
28import android.graphics.ColorFilter;
29import android.graphics.Outline;
30import android.graphics.PorterDuff;
31import android.graphics.Rect;
32import android.util.ArrayMap;
33import android.util.AttributeSet;
34import android.util.Log;
35
36import com.android.internal.R;
37
38import org.xmlpull.v1.XmlPullParser;
39import org.xmlpull.v1.XmlPullParserException;
40
41import java.io.IOException;
42import java.util.ArrayList;
43import java.util.List;
44
45/**
46 * This class uses {@link android.animation.ObjectAnimator} and
47 * {@link android.animation.AnimatorSet} to animate the properties of a
48 * {@link android.graphics.drawable.VectorDrawable} to create an animated drawable.
49 * <p>
50 * AnimatedVectorDrawable are normally defined as 3 separate XML files.
51 * </p>
52 * <p>
53 * First is the XML file for {@link android.graphics.drawable.VectorDrawable}.
54 * Note that we allow the animation to happen on the group's attributes and path's
55 * attributes, which requires they are uniquely named in this XML file. Groups
56 * and paths without animations do not need names.
57 * </p>
58 * <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
59 * <pre>
60 * &lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
61 *     android:height=&quot;64dp&quot;
62 *     android:width=&quot;64dp&quot;
63 *     android:viewportHeight=&quot;600&quot;
64 *     android:viewportWidth=&quot;600&quot; &gt;
65 *     &lt;group
66 *         android:name=&quot;rotationGroup&quot;
67 *         android:pivotX=&quot;300.0&quot;
68 *         android:pivotY=&quot;300.0&quot;
69 *         android:rotation=&quot;45.0&quot; &gt;
70 *         &lt;path
71 *             android:name=&quot;v&quot;
72 *             android:fillColor=&quot;#000000&quot;
73 *             android:pathData=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; /&gt;
74 *     &lt;/group&gt;
75 * &lt;/vector&gt;
76 * </pre></li>
77 * <p>
78 * Second is the AnimatedVectorDrawable's XML file, which defines the target
79 * VectorDrawable, the target paths and groups to animate, the properties of the
80 * path and group to animate and the animations defined as the ObjectAnimators
81 * or AnimatorSets.
82 * </p>
83 * <li>Here is a simple AnimatedVectorDrawable defined in this avd.xml file.
84 * Note how we use the names to refer to the groups and paths in the vectordrawable.xml.
85 * <pre>
86 * &lt;animated-vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
87 *   android:drawable=&quot;@drawable/vectordrawable&quot; &gt;
88 *     &lt;target
89 *         android:name=&quot;rotationGroup&quot;
90 *         android:animation=&quot;@anim/rotation&quot; /&gt;
91 *     &lt;target
92 *         android:name=&quot;v&quot;
93 *         android:animation=&quot;@anim/path_morph&quot; /&gt;
94 * &lt;/animated-vector&gt;
95 * </pre></li>
96 * <p>
97 * Last is the Animator XML file, which is the same as a normal ObjectAnimator
98 * or AnimatorSet.
99 * To complete this example, here are the 2 animator files used in avd.xml:
100 * rotation.xml and path_morph.xml.
101 * </p>
102 * <li>Here is the rotation.xml, which will rotate the target group for 360 degrees.
103 * <pre>
104 * &lt;objectAnimator
105 *     android:duration=&quot;6000&quot;
106 *     android:propertyName=&quot;rotation&quot;
107 *     android:valueFrom=&quot;0&quot;
108 *     android:valueTo=&quot;360&quot; /&gt;
109 * </pre></li>
110 * <li>Here is the path_morph.xml, which will morph the path from one shape to
111 * the other. Note that the paths must be compatible for morphing.
112 * In more details, the paths should have exact same length of commands , and
113 * exact same length of parameters for each commands.
114 * Note that the path strings are better stored in strings.xml for reusing.
115 * <pre>
116 * &lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android">;
117 *     &lt;objectAnimator
118 *         android:duration=&quot;3000&quot;
119 *         android:propertyName=&quot;pathData&quot;
120 *         android:valueFrom=&quot;M300,70 l 0,-70 70,70 0,0   -70,70z&quot;
121 *         android:valueTo=&quot;M300,70 l 0,-70 70,0  0,140 -70,0 z&quot;
122 *         android:valueType=&quot;pathType&quot;/&gt;
123 * &lt;/set&gt;
124 * </pre></li>
125 *
126 * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable
127 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name
128 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation
129 */
130public class AnimatedVectorDrawable extends Drawable implements Animatable {
131    private static final String LOGTAG = "AnimatedVectorDrawable";
132
133    private static final String ANIMATED_VECTOR = "animated-vector";
134    private static final String TARGET = "target";
135
136    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
137
138    /** Local, mutable animator set. */
139    private final AnimatorSet mAnimatorSet = new AnimatorSet();
140
141    /**
142     * The resources against which this drawable was created. Used to attempt
143     * to inflate animators if applyTheme() doesn't get called.
144     */
145    private Resources mRes;
146
147    private AnimatedVectorDrawableState mAnimatedVectorState;
148
149    /** Whether the animator set has been prepared. */
150    private boolean mHasAnimatorSet;
151
152    private boolean mMutated;
153
154    public AnimatedVectorDrawable() {
155        this(null, null);
156    }
157
158    private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
159        mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
160        mRes = res;
161    }
162
163    @Override
164    public Drawable mutate() {
165        if (!mMutated && super.mutate() == this) {
166            mAnimatedVectorState = new AnimatedVectorDrawableState(
167                    mAnimatedVectorState, mCallback, null);
168            mMutated = true;
169        }
170        return this;
171    }
172
173    /**
174     * @hide
175     */
176    public void clearMutated() {
177        super.clearMutated();
178        if (mAnimatedVectorState.mVectorDrawable != null) {
179            mAnimatedVectorState.mVectorDrawable.clearMutated();
180        }
181        mMutated = false;
182    }
183
184    @Override
185    public ConstantState getConstantState() {
186        mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
187        return mAnimatedVectorState;
188    }
189
190    @Override
191    public int getChangingConfigurations() {
192        return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations();
193    }
194
195    @Override
196    public void draw(Canvas canvas) {
197        mAnimatedVectorState.mVectorDrawable.draw(canvas);
198        if (isStarted()) {
199            invalidateSelf();
200        }
201    }
202
203    @Override
204    protected void onBoundsChange(Rect bounds) {
205        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
206    }
207
208    @Override
209    protected boolean onStateChange(int[] state) {
210        return mAnimatedVectorState.mVectorDrawable.setState(state);
211    }
212
213    @Override
214    protected boolean onLevelChange(int level) {
215        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
216    }
217
218    @Override
219    public boolean onLayoutDirectionChange(int layoutDirection) {
220        return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection);
221    }
222
223    @Override
224    public int getAlpha() {
225        return mAnimatedVectorState.mVectorDrawable.getAlpha();
226    }
227
228    @Override
229    public void setAlpha(int alpha) {
230        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
231    }
232
233    @Override
234    public void setColorFilter(ColorFilter colorFilter) {
235        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
236    }
237
238    @Override
239    public void setTintList(ColorStateList tint) {
240        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
241    }
242
243    @Override
244    public void setHotspot(float x, float y) {
245        mAnimatedVectorState.mVectorDrawable.setHotspot(x, y);
246    }
247
248    @Override
249    public void setHotspotBounds(int left, int top, int right, int bottom) {
250        mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom);
251    }
252
253    @Override
254    public void setTintMode(PorterDuff.Mode tintMode) {
255        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
256    }
257
258    @Override
259    public boolean setVisible(boolean visible, boolean restart) {
260        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
261        return super.setVisible(visible, restart);
262    }
263
264    @Override
265    public boolean isStateful() {
266        return mAnimatedVectorState.mVectorDrawable.isStateful();
267    }
268
269    @Override
270    public int getOpacity() {
271        return mAnimatedVectorState.mVectorDrawable.getOpacity();
272    }
273
274    @Override
275    public int getIntrinsicWidth() {
276        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
277    }
278
279    @Override
280    public int getIntrinsicHeight() {
281        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
282    }
283
284    @Override
285    public void getOutline(@NonNull Outline outline) {
286        mAnimatedVectorState.mVectorDrawable.getOutline(outline);
287    }
288
289    @Override
290    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
291            throws XmlPullParserException, IOException {
292        final AnimatedVectorDrawableState state = mAnimatedVectorState;
293
294        int eventType = parser.getEventType();
295        float pathErrorScale = 1;
296        while (eventType != XmlPullParser.END_DOCUMENT) {
297            if (eventType == XmlPullParser.START_TAG) {
298                final String tagName = parser.getName();
299                if (ANIMATED_VECTOR.equals(tagName)) {
300                    final TypedArray a = obtainAttributes(res, theme, attrs,
301                            R.styleable.AnimatedVectorDrawable);
302                    int drawableRes = a.getResourceId(
303                            R.styleable.AnimatedVectorDrawable_drawable, 0);
304                    if (drawableRes != 0) {
305                        VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
306                                drawableRes, theme).mutate();
307                        vectorDrawable.setAllowCaching(false);
308                        vectorDrawable.setCallback(mCallback);
309                        pathErrorScale = vectorDrawable.getPixelSize();
310                        if (state.mVectorDrawable != null) {
311                            state.mVectorDrawable.setCallback(null);
312                        }
313                        state.mVectorDrawable = vectorDrawable;
314                    }
315                    a.recycle();
316                } else if (TARGET.equals(tagName)) {
317                    final TypedArray a = obtainAttributes(res, theme, attrs,
318                            R.styleable.AnimatedVectorDrawableTarget);
319                    final String target = a.getString(
320                            R.styleable.AnimatedVectorDrawableTarget_name);
321                    final int animResId = a.getResourceId(
322                            R.styleable.AnimatedVectorDrawableTarget_animation, 0);
323                    if (animResId != 0) {
324                        if (theme != null) {
325                            final Animator objectAnimator = AnimatorInflater.loadAnimator(
326                                    res, theme, animResId, pathErrorScale);
327                            state.addTargetAnimator(target, objectAnimator);
328                        } else {
329                            // The animation may be theme-dependent. As a
330                            // workaround until Animator has full support for
331                            // applyTheme(), postpone loading the animator
332                            // until we have a theme in applyTheme().
333                            state.addPendingAnimator(animResId, pathErrorScale, target);
334
335                        }
336                    }
337                    a.recycle();
338                }
339            }
340
341            eventType = parser.next();
342        }
343
344        // If we don't have any pending animations, we don't need to hold a
345        // reference to the resources.
346        mRes = state.mPendingAnims == null ? null : res;
347    }
348
349    @Override
350    public boolean canApplyTheme() {
351        return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme())
352                || super.canApplyTheme();
353    }
354
355    @Override
356    public void applyTheme(Theme t) {
357        super.applyTheme(t);
358
359        final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable;
360        if (vectorDrawable != null && vectorDrawable.canApplyTheme()) {
361            vectorDrawable.applyTheme(t);
362        }
363
364        if (t != null) {
365            mAnimatedVectorState.inflatePendingAnimators(t.getResources(), t);
366        }
367
368        // If we don't have any pending animations, we don't need to hold a
369        // reference to the resources.
370        if (mAnimatedVectorState.mPendingAnims == null) {
371            mRes = null;
372        }
373    }
374
375    /**
376     * Adds a listener to the set of listeners that are sent events through the life of an
377     * animation.
378     *
379     * @param listener the listener to be added to the current set of listeners for this animation.
380     */
381    public void addListener(AnimatorListener listener) {
382        mAnimatorSet.addListener(listener);
383    }
384
385    /**
386     * Removes a listener from the set listening to this animation.
387     *
388     * @param listener the listener to be removed from the current set of listeners for this
389     *                 animation.
390     */
391    public void removeListener(AnimatorListener listener) {
392        mAnimatorSet.removeListener(listener);
393    }
394
395    /**
396     * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently
397     * listening for events on this <code>AnimatedVectorDrawable</code> object.
398     *
399     * @return List<AnimatorListener> The set of listeners.
400     */
401    public List<AnimatorListener> getListeners() {
402        return mAnimatorSet.getListeners();
403    }
404
405    private static class AnimatedVectorDrawableState extends ConstantState {
406        int mChangingConfigurations;
407        VectorDrawable mVectorDrawable;
408
409        /** Animators that require a theme before inflation. */
410        ArrayList<PendingAnimator> mPendingAnims;
411
412        /** Fully inflated animators awaiting cloning into an AnimatorSet. */
413        ArrayList<Animator> mAnimators;
414
415        /** Map of animators to their target object names */
416        ArrayMap<Animator, String> mTargetNameMap;
417
418        public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
419                Callback owner, Resources res) {
420            if (copy != null) {
421                mChangingConfigurations = copy.mChangingConfigurations;
422
423                if (copy.mVectorDrawable != null) {
424                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
425                    if (res != null) {
426                        mVectorDrawable = (VectorDrawable) cs.newDrawable(res);
427                    } else {
428                        mVectorDrawable = (VectorDrawable) cs.newDrawable();
429                    }
430                    mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate();
431                    mVectorDrawable.setCallback(owner);
432                    mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection());
433                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
434                    mVectorDrawable.setAllowCaching(false);
435                }
436
437                if (copy.mAnimators != null) {
438                    mAnimators = new ArrayList<>(copy.mAnimators);
439                }
440
441                if (copy.mTargetNameMap != null) {
442                    mTargetNameMap = new ArrayMap<>(copy.mTargetNameMap);
443                }
444
445                if (copy.mPendingAnims != null) {
446                    mPendingAnims = new ArrayList<>(copy.mPendingAnims);
447                }
448            } else {
449                mVectorDrawable = new VectorDrawable();
450            }
451        }
452
453        @Override
454        public boolean canApplyTheme() {
455            return (mVectorDrawable != null && mVectorDrawable.canApplyTheme())
456                    || mPendingAnims != null || super.canApplyTheme();
457        }
458
459        @Override
460        public Drawable newDrawable() {
461            return new AnimatedVectorDrawable(this, null);
462        }
463
464        @Override
465        public Drawable newDrawable(Resources res) {
466            return new AnimatedVectorDrawable(this, res);
467        }
468
469        @Override
470        public int getChangingConfigurations() {
471            return mChangingConfigurations;
472        }
473
474        public void addPendingAnimator(int resId, float pathErrorScale, String target) {
475            if (mPendingAnims == null) {
476                mPendingAnims = new ArrayList<>(1);
477            }
478            mPendingAnims.add(new PendingAnimator(resId, pathErrorScale, target));
479        }
480
481        public void addTargetAnimator(String targetName, Animator animator) {
482            if (mAnimators == null) {
483                mAnimators = new ArrayList<>(1);
484                mTargetNameMap = new ArrayMap<>(1);
485            }
486            mAnimators.add(animator);
487            mTargetNameMap.put(animator, targetName);
488
489            if (DBG_ANIMATION_VECTOR_DRAWABLE) {
490                Log.v(LOGTAG, "add animator  for target " + targetName + " " + animator);
491            }
492        }
493
494        /**
495         * Prepares a local set of mutable animators based on the constant
496         * state.
497         * <p>
498         * If there are any pending uninflated animators, attempts to inflate
499         * them immediately against the provided resources object.
500         *
501         * @param animatorSet the animator set to which the animators should
502         *                    be added
503         * @param res the resources against which to inflate any pending
504         *            animators, or {@code null} if not available
505         */
506        public void prepareLocalAnimators(@NonNull AnimatorSet animatorSet,
507                @Nullable Resources res) {
508            // Check for uninflated animators. We can remove this after we add
509            // support for Animator.applyTheme(). See comments in inflate().
510            if (mPendingAnims != null) {
511                // Attempt to load animators without applying a theme.
512                if (res != null) {
513                    inflatePendingAnimators(res, null);
514                } else {
515                    Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable"
516                            + " must be created using a Resources object or applyTheme() must be"
517                            + " called with a non-null Theme object.");
518                }
519
520                mPendingAnims = null;
521            }
522
523            // Perform a deep copy of the constant state's animators.
524            final int count = mAnimators == null ? 0 : mAnimators.size();
525            if (count > 0) {
526                final Animator firstAnim = prepareLocalAnimator(0);
527                final AnimatorSet.Builder builder = animatorSet.play(firstAnim);
528                for (int i = 1; i < count; ++i) {
529                    final Animator nextAnim = prepareLocalAnimator(i);
530                    builder.with(nextAnim);
531                }
532            }
533        }
534
535        /**
536         * Prepares a local animator for the given index within the constant
537         * state's list of animators.
538         *
539         * @param index the index of the animator within the constant state
540         */
541        private Animator prepareLocalAnimator(int index) {
542            final Animator animator = mAnimators.get(index);
543            final Animator localAnimator = animator.clone();
544            final String targetName = mTargetNameMap.get(animator);
545            final Object target = mVectorDrawable.getTargetByName(targetName);
546            localAnimator.setTarget(target);
547            return localAnimator;
548        }
549
550        /**
551         * Inflates pending animators, if any, against a theme. Clears the list of
552         * pending animators.
553         *
554         * @param t the theme against which to inflate the animators
555         */
556        public void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) {
557            final ArrayList<PendingAnimator> pendingAnims = mPendingAnims;
558            if (pendingAnims != null) {
559                mPendingAnims = null;
560
561                for (int i = 0, count = pendingAnims.size(); i < count; i++) {
562                    final PendingAnimator pendingAnimator = pendingAnims.get(i);
563                    final Animator objectAnimator = pendingAnimator.newInstance(res, t);
564                    addTargetAnimator(pendingAnimator.target, objectAnimator);
565                }
566            }
567        }
568
569        /**
570         * Basically a constant state for Animators until we actually implement
571         * constant states for Animators.
572         */
573        private static class PendingAnimator {
574            public final int animResId;
575            public final float pathErrorScale;
576            public final String target;
577
578            public PendingAnimator(int animResId, float pathErrorScale, String target) {
579                this.animResId = animResId;
580                this.pathErrorScale = pathErrorScale;
581                this.target = target;
582            }
583
584            public Animator newInstance(Resources res, Theme theme) {
585                return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale);
586            }
587        }
588    }
589
590    @Override
591    public boolean isRunning() {
592        return mAnimatorSet.isRunning();
593    }
594
595    private boolean isStarted() {
596        return mAnimatorSet.isStarted();
597    }
598
599    @Override
600    public void start() {
601        ensureAnimatorSet();
602
603        // If any one of the animator has not ended, do nothing.
604        if (isStarted()) {
605            return;
606        }
607
608        mAnimatorSet.start();
609        invalidateSelf();
610    }
611
612    @NonNull
613    private void ensureAnimatorSet() {
614        if (!mHasAnimatorSet) {
615            mAnimatedVectorState.prepareLocalAnimators(mAnimatorSet, mRes);
616            mHasAnimatorSet = true;
617            mRes = null;
618        }
619    }
620
621    @Override
622    public void stop() {
623        mAnimatorSet.end();
624    }
625
626    /**
627     * Reverses ongoing animations or starts pending animations in reverse.
628     * <p>
629     * NOTE: Only works if all animations support reverse. Otherwise, this will
630     * do nothing.
631     * @hide
632     */
633    public void reverse() {
634        ensureAnimatorSet();
635
636        // Only reverse when all the animators can be reversed.
637        if (!canReverse()) {
638            Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
639            return;
640        }
641
642        mAnimatorSet.reverse();
643        invalidateSelf();
644    }
645
646    /**
647     * @hide
648     */
649    public boolean canReverse() {
650        return mAnimatorSet.canReverse();
651    }
652
653    private final Callback mCallback = new Callback() {
654        @Override
655        public void invalidateDrawable(Drawable who) {
656            invalidateSelf();
657        }
658
659        @Override
660        public void scheduleDrawable(Drawable who, Runnable what, long when) {
661            scheduleSelf(what, when);
662        }
663
664        @Override
665        public void unscheduleDrawable(Drawable who, Runnable what) {
666            unscheduleSelf(what);
667        }
668    };
669}
670