AnimatedVectorDrawable.java revision 87e1938be8e6c2ac7e6163a63df5dd69633cc836
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.content.res.ColorStateList;
23import android.content.res.Resources;
24import android.content.res.Resources.Theme;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
27import android.graphics.ColorFilter;
28import android.graphics.Insets;
29import android.graphics.Outline;
30import android.graphics.PorterDuff;
31import android.graphics.Rect;
32import android.util.ArrayMap;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.View;
36
37import com.android.internal.R;
38
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlPullParserException;
41
42import java.io.IOException;
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * This class uses {@link android.animation.ObjectAnimator} and
48 * {@link android.animation.AnimatorSet} to animate the properties of a
49 * {@link android.graphics.drawable.VectorDrawable} to create an animated drawable.
50 * <p>
51 * AnimatedVectorDrawable are normally defined as 3 separate XML files.
52 * </p>
53 * <p>
54 * First is the XML file for {@link android.graphics.drawable.VectorDrawable}.
55 * Note that we allow the animation to happen on the group's attributes and path's
56 * attributes, which requires they are uniquely named in this XML file. Groups
57 * and paths without animations do not need names.
58 * </p>
59 * <li>Here is a simple VectorDrawable in this vectordrawable.xml file.
60 * <pre>
61 * &lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
62 *     android:height=&quot;64dp&quot;
63 *     android:width=&quot;64dp&quot;
64 *     android:viewportHeight=&quot;600&quot;
65 *     android:viewportWidth=&quot;600&quot; &gt;
66 *     &lt;group
67 *         android:name=&quot;rotationGroup&quot;
68 *         android:pivotX=&quot;300.0&quot;
69 *         android:pivotY=&quot;300.0&quot;
70 *         android:rotation=&quot;45.0&quot; &gt;
71 *         &lt;path
72 *             android:name=&quot;v&quot;
73 *             android:fillColor=&quot;#000000&quot;
74 *             android:pathData=&quot;M300,70 l 0,-70 70,70 0,0 -70,70z&quot; /&gt;
75 *     &lt;/group&gt;
76 * &lt;/vector&gt;
77 * </pre></li>
78 * <p>
79 * Second is the AnimatedVectorDrawable's XML file, which defines the target
80 * VectorDrawable, the target paths and groups to animate, the properties of the
81 * path and group to animate and the animations defined as the ObjectAnimators
82 * or AnimatorSets.
83 * </p>
84 * <li>Here is a simple AnimatedVectorDrawable defined in this avd.xml file.
85 * Note how we use the names to refer to the groups and paths in the vectordrawable.xml.
86 * <pre>
87 * &lt;animated-vector xmlns:android=&quot;http://schemas.android.com/apk/res/android";
88 *   android:drawable=&quot;@drawable/vectordrawable&quot; &gt;
89 *     &lt;target
90 *         android:name=&quot;rotationGroup&quot;
91 *         android:animation=&quot;@anim/rotation&quot; /&gt;
92 *     &lt;target
93 *         android:name=&quot;v&quot;
94 *         android:animation=&quot;@anim/path_morph&quot; /&gt;
95 * &lt;/animated-vector&gt;
96 * </pre></li>
97 * <p>
98 * Last is the Animator XML file, which is the same as a normal ObjectAnimator
99 * or AnimatorSet.
100 * To complete this example, here are the 2 animator files used in avd.xml:
101 * rotation.xml and path_morph.xml.
102 * </p>
103 * <li>Here is the rotation.xml, which will rotate the target group for 360 degrees.
104 * <pre>
105 * &lt;objectAnimator
106 *     android:duration=&quot;6000&quot;
107 *     android:propertyName=&quot;rotation&quot;
108 *     android:valueFrom=&quot;0&quot;
109 *     android:valueTo=&quot;360&quot; /&gt;
110 * </pre></li>
111 * <li>Here is the path_morph.xml, which will morph the path from one shape to
112 * the other. Note that the paths must be compatible for morphing.
113 * In more details, the paths should have exact same length of commands , and
114 * exact same length of parameters for each commands.
115 * Note that the path strings are better stored in strings.xml for reusing.
116 * <pre>
117 * &lt;set xmlns:android=&quot;http://schemas.android.com/apk/res/android">;
118 *     &lt;objectAnimator
119 *         android:duration=&quot;3000&quot;
120 *         android:propertyName=&quot;pathData&quot;
121 *         android:valueFrom=&quot;M300,70 l 0,-70 70,70 0,0   -70,70z&quot;
122 *         android:valueTo=&quot;M300,70 l 0,-70 70,0  0,140 -70,0 z&quot;
123 *         android:valueType=&quot;pathType&quot;/&gt;
124 * &lt;/set&gt;
125 * </pre></li>
126 *
127 * @attr ref android.R.styleable#AnimatedVectorDrawable_drawable
128 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_name
129 * @attr ref android.R.styleable#AnimatedVectorDrawableTarget_animation
130 */
131public class AnimatedVectorDrawable extends Drawable implements Animatable {
132    private static final String LOGTAG = AnimatedVectorDrawable.class.getSimpleName();
133
134    private static final String ANIMATED_VECTOR = "animated-vector";
135    private static final String TARGET = "target";
136
137    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
138
139    private AnimatedVectorDrawableState mAnimatedVectorState;
140
141    private boolean mMutated;
142
143    public AnimatedVectorDrawable() {
144        this(null, null);
145    }
146
147    private AnimatedVectorDrawable(AnimatedVectorDrawableState state, Resources res) {
148        mAnimatedVectorState = new AnimatedVectorDrawableState(state, mCallback, res);
149    }
150
151    @Override
152    public Drawable mutate() {
153        if (!mMutated && super.mutate() == this) {
154            mAnimatedVectorState = new AnimatedVectorDrawableState(
155                    mAnimatedVectorState, mCallback, null);
156            mMutated = true;
157        }
158        return this;
159    }
160
161    /**
162     * @hide
163     */
164    public void clearMutated() {
165        super.clearMutated();
166        mAnimatedVectorState.mVectorDrawable.clearMutated();
167        mMutated = false;
168    }
169
170    @Override
171    public ConstantState getConstantState() {
172        mAnimatedVectorState.mChangingConfigurations = getChangingConfigurations();
173        return mAnimatedVectorState;
174    }
175
176    @Override
177    public int getChangingConfigurations() {
178        return super.getChangingConfigurations() | mAnimatedVectorState.getChangingConfigurations();
179    }
180
181    @Override
182    public void draw(Canvas canvas) {
183        mAnimatedVectorState.mVectorDrawable.draw(canvas);
184        if (isStarted()) {
185            invalidateSelf();
186        }
187    }
188
189    @Override
190    protected void onBoundsChange(Rect bounds) {
191        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
192    }
193
194    @Override
195    protected boolean onStateChange(int[] state) {
196        return mAnimatedVectorState.mVectorDrawable.setState(state);
197    }
198
199    @Override
200    protected boolean onLevelChange(int level) {
201        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
202    }
203
204    @Override
205    public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
206        return mAnimatedVectorState.mVectorDrawable.setLayoutDirection(layoutDirection);
207    }
208
209    @Override
210    public int getAlpha() {
211        return mAnimatedVectorState.mVectorDrawable.getAlpha();
212    }
213
214    @Override
215    public void setAlpha(int alpha) {
216        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
217    }
218
219    @Override
220    public void setColorFilter(ColorFilter colorFilter) {
221        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
222    }
223
224    @Override
225    public void setTintList(ColorStateList tint) {
226        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
227    }
228
229    @Override
230    public void setHotspot(float x, float y) {
231        mAnimatedVectorState.mVectorDrawable.setHotspot(x, y);
232    }
233
234    @Override
235    public void setHotspotBounds(int left, int top, int right, int bottom) {
236        mAnimatedVectorState.mVectorDrawable.setHotspotBounds(left, top, right, bottom);
237    }
238
239    @Override
240    public void setTintMode(PorterDuff.Mode tintMode) {
241        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
242    }
243
244    @Override
245    public boolean setVisible(boolean visible, boolean restart) {
246        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
247        return super.setVisible(visible, restart);
248    }
249
250    @Override
251    public boolean isStateful() {
252        return mAnimatedVectorState.mVectorDrawable.isStateful();
253    }
254
255    @Override
256    public int getOpacity() {
257        return mAnimatedVectorState.mVectorDrawable.getOpacity();
258    }
259
260    @Override
261    public int getIntrinsicWidth() {
262        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
263    }
264
265    @Override
266    public int getIntrinsicHeight() {
267        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
268    }
269
270    @Override
271    public void getOutline(@NonNull Outline outline) {
272        mAnimatedVectorState.mVectorDrawable.getOutline(outline);
273    }
274
275    /** @hide */
276    @Override
277    public Insets getOpticalInsets() {
278        return mAnimatedVectorState.mVectorDrawable.getOpticalInsets();
279    }
280
281    @Override
282    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
283            throws XmlPullParserException, IOException {
284
285        int eventType = parser.getEventType();
286        float pathErrorScale = 1;
287        while (eventType != XmlPullParser.END_DOCUMENT) {
288            if (eventType == XmlPullParser.START_TAG) {
289                final String tagName = parser.getName();
290                if (ANIMATED_VECTOR.equals(tagName)) {
291                    final TypedArray a = obtainAttributes(res, theme, attrs,
292                            R.styleable.AnimatedVectorDrawable);
293                    int drawableRes = a.getResourceId(
294                            R.styleable.AnimatedVectorDrawable_drawable, 0);
295                    if (drawableRes != 0) {
296                        VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
297                                drawableRes, theme).mutate();
298                        vectorDrawable.setAllowCaching(false);
299                        vectorDrawable.setCallback(mCallback);
300                        pathErrorScale = vectorDrawable.getPixelSize();
301                        if (mAnimatedVectorState.mVectorDrawable != null) {
302                            mAnimatedVectorState.mVectorDrawable.setCallback(null);
303                        }
304                        mAnimatedVectorState.mVectorDrawable = vectorDrawable;
305                    }
306                    a.recycle();
307                } else if (TARGET.equals(tagName)) {
308                    final TypedArray a = obtainAttributes(res, theme, attrs,
309                            R.styleable.AnimatedVectorDrawableTarget);
310                    final String target = a.getString(
311                            R.styleable.AnimatedVectorDrawableTarget_name);
312
313                    int id = a.getResourceId(
314                            R.styleable.AnimatedVectorDrawableTarget_animation, 0);
315                    if (id != 0) {
316                        Animator objectAnimator = AnimatorInflater.loadAnimator(res, theme, id,
317                                pathErrorScale);
318                        setupAnimatorsForTarget(target, objectAnimator);
319                    }
320                    a.recycle();
321                }
322            }
323
324            eventType = parser.next();
325        }
326        setupAnimatorSet();
327    }
328
329    private void setupAnimatorSet() {
330        if (mAnimatedVectorState.mTempAnimators != null) {
331            mAnimatedVectorState.mAnimatorSet.playTogether(mAnimatedVectorState.mTempAnimators);
332            mAnimatedVectorState.mTempAnimators.clear();
333            mAnimatedVectorState.mTempAnimators = null;
334        }
335    }
336
337    @Override
338    public boolean canApplyTheme() {
339        return (mAnimatedVectorState != null && mAnimatedVectorState.canApplyTheme())
340                || super.canApplyTheme();
341    }
342
343    @Override
344    public void applyTheme(Theme t) {
345        super.applyTheme(t);
346
347        final VectorDrawable vectorDrawable = mAnimatedVectorState.mVectorDrawable;
348        if (vectorDrawable != null && vectorDrawable.canApplyTheme()) {
349            vectorDrawable.applyTheme(t);
350        }
351    }
352
353    /**
354     * Adds a listener to the set of listeners that are sent events through the life of an
355     * animation.
356     *
357     * @param listener the listener to be added to the current set of listeners for this animation.
358     */
359    public void addListener(AnimatorListener listener) {
360        mAnimatedVectorState.mAnimatorSet.addListener(listener);
361    }
362
363    /**
364     * Removes a listener from the set listening to this animation.
365     *
366     * @param listener the listener to be removed from the current set of listeners for this
367     *                 animation.
368     */
369    public void removeListener(AnimatorListener listener) {
370        mAnimatedVectorState.mAnimatorSet.removeListener(listener);
371    }
372
373    /**
374     * Gets the set of {@link android.animation.Animator.AnimatorListener} objects that are currently
375     * listening for events on this <code>AnimatedVectorDrawable</code> object.
376     *
377     * @return List<AnimatorListener> The set of listeners.
378     */
379    public List<AnimatorListener> getListeners() {
380        return mAnimatedVectorState.mAnimatorSet.getListeners();
381    }
382
383    private static class AnimatedVectorDrawableState extends ConstantState {
384        int mChangingConfigurations;
385        VectorDrawable mVectorDrawable;
386        // Always have a valid animatorSet to handle all the listeners call.
387        AnimatorSet mAnimatorSet = new AnimatorSet();
388        // When parsing the XML, we build individual animator and store in this array. At the end,
389        // we add this array into the mAnimatorSet.
390        private ArrayList<Animator> mTempAnimators;
391        ArrayMap<Animator, String> mTargetNameMap;
392
393        public AnimatedVectorDrawableState(AnimatedVectorDrawableState copy,
394                Callback owner, Resources res) {
395            if (copy != null) {
396                mChangingConfigurations = copy.mChangingConfigurations;
397                if (copy.mVectorDrawable != null) {
398                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
399                    if (res != null) {
400                        mVectorDrawable = (VectorDrawable) cs.newDrawable(res);
401                    } else {
402                        mVectorDrawable = (VectorDrawable) cs.newDrawable();
403                    }
404                    mVectorDrawable = (VectorDrawable) mVectorDrawable.mutate();
405                    mVectorDrawable.setCallback(owner);
406                    mVectorDrawable.setLayoutDirection(copy.mVectorDrawable.getLayoutDirection());
407                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
408                    mVectorDrawable.setAllowCaching(false);
409                }
410                if (copy.mAnimatorSet != null) {
411                    final int numAnimators = copy.mTargetNameMap.size();
412                    // Deep copy a animator set, and then setup the target map again.
413                    mAnimatorSet = copy.mAnimatorSet.clone();
414                    mTargetNameMap = new ArrayMap<Animator, String>(numAnimators);
415                    // Since the new AnimatorSet is cloned from the old one, the order must be the
416                    // same inside the array.
417                    ArrayList<Animator> oldAnim = copy.mAnimatorSet.getChildAnimations();
418                    ArrayList<Animator> newAnim = mAnimatorSet.getChildAnimations();
419
420                    for (int i = 0; i < numAnimators; ++i) {
421                        // Target name must be the same for new and old
422                        String targetName = copy.mTargetNameMap.get(oldAnim.get(i));
423
424                        Object newTargetObject = mVectorDrawable.getTargetByName(targetName);
425                        newAnim.get(i).setTarget(newTargetObject);
426                        mTargetNameMap.put(newAnim.get(i), targetName);
427                    }
428                }
429            } else {
430                mVectorDrawable = new VectorDrawable();
431            }
432        }
433
434        @Override
435        public boolean canApplyTheme() {
436            return (mVectorDrawable != null && mVectorDrawable.canApplyTheme())
437                    || super.canApplyTheme();
438        }
439
440        @Override
441        public Drawable newDrawable() {
442            return new AnimatedVectorDrawable(this, null);
443        }
444
445        @Override
446        public Drawable newDrawable(Resources res) {
447            return new AnimatedVectorDrawable(this, res);
448        }
449
450        @Override
451        public int getChangingConfigurations() {
452            return mChangingConfigurations;
453        }
454    }
455
456    private void setupAnimatorsForTarget(String name, Animator animator) {
457        Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
458        animator.setTarget(target);
459        if (mAnimatedVectorState.mTempAnimators == null) {
460            mAnimatedVectorState.mTempAnimators = new ArrayList<Animator>();
461            mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>();
462        }
463        mAnimatedVectorState.mTempAnimators.add(animator);
464        mAnimatedVectorState.mTargetNameMap.put(animator, name);
465        if (DBG_ANIMATION_VECTOR_DRAWABLE) {
466            Log.v(LOGTAG, "add animator  for target " + name + " " + animator);
467        }
468    }
469
470    @Override
471    public boolean isRunning() {
472        return mAnimatedVectorState.mAnimatorSet.isRunning();
473    }
474
475    private boolean isStarted() {
476        return mAnimatedVectorState.mAnimatorSet.isStarted();
477    }
478
479    @Override
480    public void start() {
481        // If any one of the animator has not ended, do nothing.
482        if (isStarted()) {
483            return;
484        }
485        mAnimatedVectorState.mAnimatorSet.start();
486        invalidateSelf();
487    }
488
489    @Override
490    public void stop() {
491        mAnimatedVectorState.mAnimatorSet.end();
492    }
493
494    /**
495     * Reverses ongoing animations or starts pending animations in reverse.
496     * <p>
497     * NOTE: Only works if all animations support reverse. Otherwise, this will
498     * do nothing.
499     * @hide
500     */
501    public void reverse() {
502        // Only reverse when all the animators can be reverse. Otherwise, partially
503        // reverse is confusing.
504        if (!canReverse()) {
505            Log.w(LOGTAG, "AnimatedVectorDrawable can't reverse()");
506            return;
507        }
508        mAnimatedVectorState.mAnimatorSet.reverse();
509    }
510
511    /**
512     * @hide
513     */
514    public boolean canReverse() {
515        return mAnimatedVectorState.mAnimatorSet.canReverse();
516    }
517
518    private final Callback mCallback = new Callback() {
519        @Override
520        public void invalidateDrawable(Drawable who) {
521            invalidateSelf();
522        }
523
524        @Override
525        public void scheduleDrawable(Drawable who, Runnable what, long when) {
526            scheduleSelf(what, when);
527        }
528
529        @Override
530        public void unscheduleDrawable(Drawable who, Runnable what) {
531            unscheduleSelf(what);
532        }
533    };
534}
535