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