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