AnimatedVectorDrawable.java revision 9b115a9870a184e32bdbd07f792f9b8c956c75ca
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    /**
139     * The resources against which this drawable was created. Used to attempt
140     * to inflate animators if applyTheme() doesn't get called.
141     */
142    private Resources mRes;
143
144    private AnimatedVectorDrawableState mAnimatedVectorState;
145
146    private boolean mMutated;
147
148    public AnimatedVectorDrawable() {
149        this(null, null);
150    }
151
152    private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
153        mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
154        mRes = res;
155    }
156
157    @Override
158    public Drawable mutate() {
159        if (!mMutated && super.mutate() == this) {
160            mAnimatedVectorState = new AnimatedVectorDrawableState(
161                    mAnimatedVectorState, mCallback, null);
162            mMutated = true;
163        }
164        return this;
165    }
166
167    /**
168     * @hide
169     */
170    public void clearMutated() {
171        super.clearMutated();
172        mAnimatedVectorState.mVectorDrawable.clearMutated();
173        mMutated = false;
174    }
175
176    @Override
177    public ConstantState getConstantState() {
178        mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
179        return mAnimatedVectorState;
180    }
181
182    @Override
183    public int getChangingConfigurations() {
184        return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations();
185    }
186
187    @Override
188    public void draw(Canvas canvas) {
189        mAnimatedVectorState.mVectorDrawable.draw(canvas);
190        if (isStarted()) {
191            invalidateSelf();
192        }
193    }
194
195    @Override
196    protected void onBoundsChange(Rect bounds) {
197        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
198    }
199
200    @Override
201    protected boolean onStateChange(int[] state) {
202        return mAnimatedVectorState.mVectorDrawable.setState(state);
203    }
204
205    @Override
206    protected boolean onLevelChange(int level) {
207        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
208    }
209
210    @Override
211    public boolean onLayoutDirectionChange(int layoutDirection) {
212        return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection);
213    }
214
215    @Override
216    public int getAlpha() {
217        return mAnimatedVectorState.mVectorDrawable.getAlpha();
218    }
219
220    @Override
221    public void setAlpha(int alpha) {
222        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
223    }
224
225    @Override
226    public void setColorFilter(ColorFilter colorFilter) {
227        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
228    }
229
230    @Override
231    public void setTintList(ColorStateList tint) {
232        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
233    }
234
235    @Override
236    public void setHotspot(float x, float y) {
237        mAnimatedVectorState.mVectorDrawable.setHotspot(x, y);
238    }
239
240    @Override
241    public void setHotspotBounds(int left, int top, int right, int bottom) {
242        mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom);
243    }
244
245    @Override
246    public void setTintMode(PorterDuff.Mode tintMode) {
247        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
248    }
249
250    @Override
251    public boolean setVisible(boolean visible, boolean restart) {
252        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
253        return super.setVisible(visible, restart);
254    }
255
256    @Override
257    public boolean isStateful() {
258        return mAnimatedVectorState.mVectorDrawable.isStateful();
259    }
260
261    @Override
262    public int getOpacity() {
263        return mAnimatedVectorState.mVectorDrawable.getOpacity();
264    }
265
266    @Override
267    public int getIntrinsicWidth() {
268        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
269    }
270
271    @Override
272    public int getIntrinsicHeight() {
273        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
274    }
275
276    @Override
277    public void getOutline(@NonNull Outline outline) {
278        mAnimatedVectorState.mVectorDrawable.getOutline(outline);
279    }
280
281    @Override
282    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
283            throws XmlPullParserException, IOException {
284        final AnimatedVectorDrawableState state = mAnimatedVectorState;
285
286        int eventType = parser.getEventType();
287        float pathErrorScale = 1;
288        while (eventType != XmlPullParser.END_DOCUMENT) {
289            if (eventType == XmlPullParser.START_TAG) {
290                final String tagName = parser.getName();
291                if (ANIMATED_VECTOR.equals(tagName)) {
292                    final TypedArray a = obtainAttributes(res, theme, attrs,
293                            R.styleable.AnimatedVectorDrawable);
294                    int drawableRes = a.getResourceId(
295                            R.styleable.AnimatedVectorDrawable_drawable, 0);
296                    if (drawableRes != 0) {
297                        VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
298                                drawableRes, theme).mutate();
299                        vectorDrawable.setAllowCaching(false);
300                        vectorDrawable.setCallback(mCallback);
301                        pathErrorScale = vectorDrawable.getPixelSize();
302                        if (state.mVectorDrawable != null) {
303                            state.mVectorDrawable.setCallback(null);
304                        }
305                        state.mVectorDrawable = vectorDrawable;
306                    }
307                    a.recycle();
308                } else if (TARGET.equals(tagName)) {
309                    final TypedArray a = obtainAttributes(res, theme, attrs,
310                            R.styleable.AnimatedVectorDrawableTarget);
311                    final String target = a.getString(
312                            R.styleable.AnimatedVectorDrawableTarget_name);
313
314                    final int animResId = a.getResourceId(
315                            R.styleable.AnimatedVectorDrawableTarget_animation, 0);
316                    if (animResId != 0) {
317                        if (theme != null) {
318                            final Animator objectAnimator = AnimatorInflater.loadAnimator(
319                                    res, theme, animResId, pathErrorScale);
320                            setupAnimatorsForTarget(target, objectAnimator);
321                        } else {
322                            // The animation may be theme-dependent. As a
323                            // workaround until Animator has full support for
324                            // applyTheme(), postpone loading the animator
325                            // until we have a theme in applyTheme().
326                            if (state.mPendingAnims == null) {
327                                state.mPendingAnims = new ArrayList<>(1);
328                            }
329                            state.mPendingAnims.add(
330                                    new PendingAnimator(animResId, pathErrorScale, target));
331
332                        }
333                    }
334                    a.recycle();
335                }
336            }
337
338            eventType = parser.next();
339        }
340
341        // If we don't have any pending animations, we don't need to hold a
342        // reference to the resources.
343        if (state.mPendingAnims == null) {
344            mRes = null;
345        }
346
347        setupAnimatorSet();
348    }
349
350    private void setupAnimatorSet() {
351        if (mAnimatedVectorState.mTempAnimators != null) {
352            mAnimatedVectorState.mAnimatorSet.playTogether(mAnimatedVectorState.mTempAnimators);
353            mAnimatedVectorState.mTempAnimators.clear();
354            mAnimatedVectorState.mTempAnimators = null;
355        }
356    }
357
358    @Override
359    public boolean canApplyTheme() {
360        return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme())
361                || super.canApplyTheme();
362    }
363
364    @Override
365    public void applyTheme(Theme t) {
366        super.applyTheme(t);
367
368        final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable;
369        if (vectorDrawable != null && vectorDrawable.canApplyTheme()) {
370            vectorDrawable.applyTheme(t);
371        }
372
373        if (t != null) {
374            inflatePendingAnimators(t.getResources(), t);
375        }
376    }
377
378    /**
379     * Inflates pending animators, if any, against a theme. Clears the list of
380     * pending animators.
381     *
382     * @param t the theme against which to inflate the animators
383     */
384    private void inflatePendingAnimators(@NonNull Resources res, @Nullable Theme t) {
385        final ArrayList<PendingAnimator> pendingAnims = mAnimatedVectorState.mPendingAnims;
386        if (pendingAnims != null) {
387            mAnimatedVectorState.mPendingAnims = null;
388
389            for (int i = 0, count = pendingAnims.size(); i < count; i++) {
390                final PendingAnimator pendingAnimator = pendingAnims.get(i);
391                final Animator objectAnimator = pendingAnimator.newInstance(res, t);
392                setupAnimatorsForTarget(pendingAnimator.target, objectAnimator);
393            }
394        }
395    }
396
397    /**
398     * Adds a listener to the set of listeners that are sent events through the life of an
399     * animation.
400     *
401     * @param listener the listener to be added to the current set of listeners for this animation.
402     */
403    public void addListener(AnimatorListener listener) {
404        mAnimatedVectorState.mAnimatorSet.addListener(listener);
405    }
406
407    /**
408     * Removes a listener from the set listening to this animation.
409     *
410     * @param listener the listener to be removed from the current set of listeners for this
411     *                 animation.
412     */
413    public void removeListener(AnimatorListener listener) {
414        mAnimatedVectorState.mAnimatorSet.removeListener(listener);
415    }
416
417    /**
418     * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently
419     * listening for events on this <code>AnimatedVectorDrawable</code> object.
420     *
421     * @return List<AnimatorListener> The set of listeners.
422     */
423    public List<AnimatorListener> getListeners() {
424        return mAnimatedVectorState.mAnimatorSet.getListeners();
425    }
426
427    private static class AnimatedVectorDrawableState extends ConstantState {
428        int mChangingConfigurations;
429        VectorDrawable mVectorDrawable;
430        // Always have a valid animatorSet to handle all the listeners call.
431        AnimatorSet mAnimatorSet = new AnimatorSet();
432        // When parsing the XML, we build individual animator and store in this array. At the end,
433        // we add this array into the mAnimatorSet.
434        private ArrayList<Animator> mTempAnimators;
435        ArrayMap<Animator, String> mTargetNameMap;
436        ArrayList<PendingAnimator> mPendingAnims;
437
438        public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
439                Callback owner, Resources res) {
440            if (copy != null) {
441                mChangingConfigurations = copy.mChangingConfigurations;
442                if (copy.mVectorDrawable != null) {
443                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
444                    if (res != null) {
445                        mVectorDrawable = (VectorDrawable) cs.newDrawable(res);
446                    } else {
447                        mVectorDrawable = (VectorDrawable) cs.newDrawable();
448                    }
449                    mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate();
450                    mVectorDrawable.setCallback(owner);
451                    mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection());
452                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
453                    mVectorDrawable.setAllowCaching(false);
454                }
455                if (copy.mAnimatorSet != null) {
456                    final int numAnimators = copy.mTargetNameMap.size();
457                    // Deep copy a animator set, and then setup the target map again.
458                    mAnimatorSet = copy.mAnimatorSet.clone();
459                    mTargetNameMap = new ArrayMap<>(numAnimators);
460                    // Since the new AnimatorSet is cloned from the old one, the order must be the
461                    // same inside the array.
462                    ArrayList<Animator> oldAnim = copy.mAnimatorSet.getChildAnimations();
463                    ArrayList<Animator> newAnim = mAnimatorSet.getChildAnimations();
464
465                    for (int i = 0; i < numAnimators; ++i) {
466                        // Target name must be the same for new and old
467                        String targetName = copy.mTargetNameMap.get(oldAnim.get(i));
468
469                        Object newTargetObject = mVectorDrawable.getTargetByName(targetName);
470                        newAnim.get(i).setTarget(newTargetObject);
471                        mTargetNameMap.put(newAnim.get(i), targetName);
472                    }
473                }
474
475                // Shallow copy since the array is immutable after inflate().
476                mPendingAnims = copy.mPendingAnims;
477            } else {
478                mVectorDrawable = new VectorDrawable();
479            }
480        }
481
482        @Override
483        public boolean canApplyTheme() {
484            return (mVectorDrawable != null && mVectorDrawable.canApplyTheme())
485                    || mPendingAnims != null || super.canApplyTheme();
486        }
487
488        @Override
489        public Drawable newDrawable() {
490            return new AnimatedVectorDrawable(this, null);
491        }
492
493        @Override
494        public Drawable newDrawable(Resources res) {
495            return new AnimatedVectorDrawable(this, res);
496        }
497
498        @Override
499        public int getChangingConfigurations() {
500            return mChangingConfigurations;
501        }
502    }
503
504    /**
505     * Basically a constant state for Animators until we actually implement
506     * constant states for Animators.
507     */
508    private static class PendingAnimator {
509        public final int animResId;
510        public final float pathErrorScale;
511        public final String target;
512
513        public PendingAnimator(int animResId, float pathErrorScale, String target) {
514            this.animResId = animResId;
515            this.pathErrorScale = pathErrorScale;
516            this.target = target;
517        }
518
519        public Animator newInstance(Resources res, Theme theme) {
520            return AnimatorInflater.loadAnimator(res, theme, animResId, pathErrorScale);
521        }
522    }
523
524    private void setupAnimatorsForTarget(String name, Animator animator) {
525        Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
526        animator.setTarget(target);
527        if (mAnimatedVectorState.mTempAnimators == null) {
528            mAnimatedVectorState.mTempAnimators = new ArrayList<>();
529            mAnimatedVectorState.mTargetNameMap = new ArrayMap<>();
530        }
531        mAnimatedVectorState.mTempAnimators.add(animator);
532        mAnimatedVectorState.mTargetNameMap.put(animator, name);
533        if (DBG_ANIMATION_VECTOR_DRAWABLE) {
534            Log.v(LOGTAG, "add animator  for target " + name + " " + animator);
535        }
536    }
537
538    @Override
539    public boolean isRunning() {
540        return mAnimatedVectorState.mAnimatorSet.isRunning();
541    }
542
543    private boolean isStarted() {
544        return mAnimatedVectorState.mAnimatorSet.isStarted();
545    }
546
547    @Override
548    public void start() {
549        // If any one of the animator has not ended, do nothing.
550        if (isStarted()) {
551            return;
552        }
553
554        // Check for uninflated animators. We can remove this after we add
555        // support for Animator.applyTheme(). See comments in inflate().
556        if (mAnimatedVectorState.mPendingAnims != null) {
557            // Attempt to load animators without applying a theme.
558            if (mRes != null) {
559                inflatePendingAnimators(mRes, null);
560                mRes = null;
561            } else {
562                Log.e(LOGTAG, "Failed to load animators. Either the AnimatedVectorDrawable must be"
563                        + " created using a Resources object or applyTheme() must be called with"
564                        + " a non-null Theme object.");
565            }
566
567            mAnimatedVectorState.mPendingAnims = null;
568        }
569
570        mAnimatedVectorState.mAnimatorSet.start();
571        invalidateSelf();
572    }
573
574    @Override
575    public void stop() {
576        mAnimatedVectorState.mAnimatorSet.end();
577    }
578
579    /**
580     * Reverses ongoing animations or starts pending animations in reverse.
581     * <p>
582     * NOTE: Only works if all animations support reverse. Otherwise, this will
583     * do nothing.
584     * @hide
585     */
586    public void reverse() {
587        // Only reverse when all the animators can be reverse. Otherwise, partially
588        // reverse is confusing.
589        if (!canReverse()) {
590            Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
591            return;
592        }
593        mAnimatedVectorState.mAnimatorSet.reverse();
594    }
595
596    /**
597     * @hide
598     */
599    public boolean canReverse() {
600        return mAnimatedVectorState.mAnimatorSet.canReverse();
601    }
602
603    private final Callback mCallback = new Callback() {
604        @Override
605        public void invalidateDrawable(Drawable who) {
606            invalidateSelf();
607        }
608
609        @Override
610        public void scheduleDrawable(Drawable who, Runnable what, long when) {
611            scheduleSelf(what, when);
612        }
613
614        @Override
615        public void unscheduleDrawable(Drawable who, Runnable what) {
616            unscheduleSelf(what);
617        }
618    };
619}
620