1/*
2 * Copyright (C) 2015 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.support.graphics.drawable;
16
17import android.animation.Animator;
18import android.animation.AnimatorInflater;
19import android.animation.AnimatorSet;
20import android.animation.ArgbEvaluator;
21import android.animation.ObjectAnimator;
22import android.annotation.TargetApi;
23import android.content.Context;
24import android.content.res.ColorStateList;
25import android.content.res.Resources;
26import android.content.res.Resources.Theme;
27import android.content.res.TypedArray;
28import android.graphics.Canvas;
29import android.graphics.ColorFilter;
30import android.graphics.PorterDuff;
31import android.graphics.Rect;
32import android.graphics.drawable.Animatable;
33import android.graphics.drawable.AnimatedVectorDrawable;
34import android.graphics.drawable.Drawable;
35import android.os.Build;
36import android.support.annotation.DrawableRes;
37import android.support.annotation.NonNull;
38import android.support.annotation.Nullable;
39import android.support.v4.content.res.ResourcesCompat;
40import android.support.v4.graphics.drawable.DrawableCompat;
41import android.support.v4.util.ArrayMap;
42import android.util.AttributeSet;
43import android.util.Log;
44import android.util.Xml;
45
46import org.xmlpull.v1.XmlPullParser;
47import org.xmlpull.v1.XmlPullParserException;
48
49import java.io.IOException;
50import java.util.ArrayList;
51import java.util.List;
52
53/**
54 * For API 24 and above, this class is delegating to the framework's {@link AnimatedVectorDrawable}.
55 * For older API version, this class uses {@link android.animation.ObjectAnimator} and
56 * {@link android.animation.AnimatorSet} to animate the properties of a
57 * {@link VectorDrawableCompat} to create an animated drawable.
58 * <p>
59 * AnimatedVectorDrawableCompat are defined in the same XML format as {@link AnimatedVectorDrawable}.
60 * </p>
61 * You can always create a AnimatedVectorDrawableCompat object and use it as a Drawable by the Java
62 * API. In order to refer to AnimatedVectorDrawableCompat inside a XML file, you can use
63 * app:srcCompat attribute in AppCompat library's ImageButton or ImageView.
64 */
65@TargetApi(Build.VERSION_CODES.LOLLIPOP)
66public class AnimatedVectorDrawableCompat extends VectorDrawableCommon implements Animatable {
67    private static final String LOGTAG = "AnimatedVDCompat";
68
69    private static final String ANIMATED_VECTOR = "animated-vector";
70    private static final String TARGET = "target";
71
72    private static final boolean DBG_ANIMATION_VECTOR_DRAWABLE = false;
73
74    private AnimatedVectorDrawableCompatState mAnimatedVectorState;
75
76    private Context mContext;
77
78    private ArgbEvaluator mArgbEvaluator = null;
79
80    AnimatedVectorDrawableDelegateState mCachedConstantStateDelegate;
81
82    AnimatedVectorDrawableCompat() {
83        this(null, null, null);
84    }
85
86    private AnimatedVectorDrawableCompat(@Nullable Context context) {
87        this(context, null, null);
88    }
89
90    private AnimatedVectorDrawableCompat(@Nullable Context context,
91                                         @Nullable AnimatedVectorDrawableCompatState state,
92                                         @Nullable Resources res) {
93        mContext = context;
94        if (state != null) {
95            mAnimatedVectorState = state;
96        } else {
97            mAnimatedVectorState = new AnimatedVectorDrawableCompatState(context, state, mCallback,
98                    res);
99        }
100    }
101
102    @Override
103    public Drawable mutate() {
104        if (mDelegateDrawable != null) {
105            mDelegateDrawable.mutate();
106            return this;
107        }
108        throw new IllegalStateException("Mutate() is not supported for older platform");
109    }
110
111
112    /**
113     * Create a AnimatedVectorDrawableCompat object.
114     *
115     * @param context the context for creating the animators.
116     * @param resId   the resource ID for AnimatedVectorDrawableCompat object.
117     * @return a new AnimatedVectorDrawableCompat or null if parsing error is found.
118     */
119    @Nullable
120    public static AnimatedVectorDrawableCompat create(@NonNull Context context,
121                                                      @DrawableRes int resId) {
122        if (Build.VERSION.SDK_INT >= 24) {
123            final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context);
124            drawable.mDelegateDrawable = ResourcesCompat.getDrawable(context.getResources(), resId,
125                    context.getTheme());
126            drawable.mDelegateDrawable.setCallback(drawable.mCallback);
127            drawable.mCachedConstantStateDelegate = new AnimatedVectorDrawableDelegateState(
128                    drawable.mDelegateDrawable.getConstantState());
129            return drawable;
130        }
131        Resources resources = context.getResources();
132        try {
133            final XmlPullParser parser = resources.getXml(resId);
134            final AttributeSet attrs = Xml.asAttributeSet(parser);
135            int type;
136            while ((type = parser.next()) != XmlPullParser.START_TAG
137                    && type != XmlPullParser.END_DOCUMENT) {
138                // Empty loop
139            }
140            if (type != XmlPullParser.START_TAG) {
141                throw new XmlPullParserException("No start tag found");
142            }
143            return createFromXmlInner(context, context.getResources(), parser, attrs,
144                    context.getTheme());
145        } catch (XmlPullParserException e) {
146            Log.e(LOGTAG, "parser error", e);
147        } catch (IOException e) {
148            Log.e(LOGTAG, "parser error", e);
149        }
150        return null;
151    }
152
153    /**
154     * Create a AnimatedVectorDrawableCompat from inside an XML document using an optional
155     * {@link Theme}. Called on a parser positioned at a tag in an XML
156     * document, tries to create a Drawable from that tag. Returns {@code null}
157     * if the tag is not a valid drawable.
158     */
159    public static AnimatedVectorDrawableCompat createFromXmlInner(Context context, Resources r,
160            XmlPullParser parser, AttributeSet attrs, Theme theme)
161            throws XmlPullParserException, IOException {
162        final AnimatedVectorDrawableCompat drawable = new AnimatedVectorDrawableCompat(context);
163        drawable.inflate(r, parser, attrs, theme);
164        return drawable;
165    }
166
167    /**
168     * {@inheritDoc}
169     * <strong>Note</strong> that we don't support constant state when SDK < 24.
170     * Make sure you check the return value before using it.
171     */
172    @Override
173    public ConstantState getConstantState() {
174        if (mDelegateDrawable != null) {
175            return new AnimatedVectorDrawableDelegateState(mDelegateDrawable.getConstantState());
176        }
177        // We can't support constant state in older platform.
178        // We need Context to create the animator, and we can't save the context in the constant
179        // state.
180        return null;
181    }
182
183    @Override
184    public int getChangingConfigurations() {
185        if (mDelegateDrawable != null) {
186            return mDelegateDrawable.getChangingConfigurations();
187        }
188        return super.getChangingConfigurations() | mAnimatedVectorState.mChangingConfigurations;
189    }
190
191    @Override
192    public void draw(Canvas canvas) {
193        if (mDelegateDrawable != null) {
194            mDelegateDrawable.draw(canvas);
195            return;
196        }
197        mAnimatedVectorState.mVectorDrawable.draw(canvas);
198        if (isStarted()) {
199            invalidateSelf();
200        }
201    }
202
203    @Override
204    protected void onBoundsChange(Rect bounds) {
205        if (mDelegateDrawable != null) {
206            mDelegateDrawable.setBounds(bounds);
207            return;
208        }
209        mAnimatedVectorState.mVectorDrawable.setBounds(bounds);
210    }
211
212    @Override
213    protected boolean onStateChange(int[] state) {
214        if (mDelegateDrawable != null) {
215            return mDelegateDrawable.setState(state);
216        }
217        return mAnimatedVectorState.mVectorDrawable.setState(state);
218    }
219
220    @Override
221    protected boolean onLevelChange(int level) {
222        if (mDelegateDrawable != null) {
223            return mDelegateDrawable.setLevel(level);
224        }
225        return mAnimatedVectorState.mVectorDrawable.setLevel(level);
226    }
227
228    @Override
229    public int getAlpha() {
230        if (mDelegateDrawable != null) {
231            return DrawableCompat.getAlpha(mDelegateDrawable);
232        }
233        return mAnimatedVectorState.mVectorDrawable.getAlpha();
234    }
235
236    @Override
237    public void setAlpha(int alpha) {
238        if (mDelegateDrawable != null) {
239            mDelegateDrawable.setAlpha(alpha);
240            return;
241        }
242        mAnimatedVectorState.mVectorDrawable.setAlpha(alpha);
243    }
244
245    @Override
246    public void setColorFilter(ColorFilter colorFilter) {
247        if (mDelegateDrawable != null) {
248            mDelegateDrawable.setColorFilter(colorFilter);
249            return;
250        }
251        mAnimatedVectorState.mVectorDrawable.setColorFilter(colorFilter);
252    }
253
254    @Override
255    public void setTint(int tint) {
256        if (mDelegateDrawable != null) {
257            DrawableCompat.setTint(mDelegateDrawable, tint);
258            return;
259        }
260
261        mAnimatedVectorState.mVectorDrawable.setTint(tint);
262    }
263
264    @Override
265    public void setTintList(ColorStateList tint) {
266        if (mDelegateDrawable != null) {
267            DrawableCompat.setTintList(mDelegateDrawable, tint);
268            return;
269        }
270
271        mAnimatedVectorState.mVectorDrawable.setTintList(tint);
272    }
273
274    @Override
275    public void setTintMode(PorterDuff.Mode tintMode) {
276        if (mDelegateDrawable != null) {
277            DrawableCompat.setTintMode(mDelegateDrawable, tintMode);
278            return;
279        }
280
281        mAnimatedVectorState.mVectorDrawable.setTintMode(tintMode);
282    }
283
284    @Override
285    public boolean setVisible(boolean visible, boolean restart) {
286        if (mDelegateDrawable != null) {
287            return mDelegateDrawable.setVisible(visible, restart);
288        }
289        mAnimatedVectorState.mVectorDrawable.setVisible(visible, restart);
290        return super.setVisible(visible, restart);
291    }
292
293    @Override
294    public boolean isStateful() {
295        if (mDelegateDrawable != null) {
296            return mDelegateDrawable.isStateful();
297        }
298        return mAnimatedVectorState.mVectorDrawable.isStateful();
299    }
300
301    @Override
302    public int getOpacity() {
303        if (mDelegateDrawable != null) {
304            return mDelegateDrawable.getOpacity();
305        }
306        return mAnimatedVectorState.mVectorDrawable.getOpacity();
307    }
308
309    @Override
310    public int getIntrinsicWidth() {
311        if (mDelegateDrawable != null) {
312            return mDelegateDrawable.getIntrinsicWidth();
313        }
314        return mAnimatedVectorState.mVectorDrawable.getIntrinsicWidth();
315    }
316
317    @Override
318    public int getIntrinsicHeight() {
319        if (mDelegateDrawable != null) {
320            return mDelegateDrawable.getIntrinsicHeight();
321        }
322        return mAnimatedVectorState.mVectorDrawable.getIntrinsicHeight();
323    }
324
325    @Override
326    public boolean isAutoMirrored() {
327        if (mDelegateDrawable != null) {
328            return DrawableCompat.isAutoMirrored(mDelegateDrawable);
329        }
330        return mAnimatedVectorState.mVectorDrawable.isAutoMirrored();
331    }
332
333    @Override
334    public void setAutoMirrored(boolean mirrored) {
335        if (mDelegateDrawable != null) {
336            mDelegateDrawable.setAutoMirrored(mirrored);
337            return;
338        }
339        mAnimatedVectorState.mVectorDrawable.setAutoMirrored(mirrored);
340    }
341
342    /**
343     * Obtains styled attributes from the theme, if available, or unstyled
344     * resources if the theme is null.
345     */
346    static TypedArray obtainAttributes(
347            Resources res, Theme theme, AttributeSet set, int[] attrs) {
348        if (theme == null) {
349            return res.obtainAttributes(set, attrs);
350        }
351        return theme.obtainStyledAttributes(set, attrs, 0, 0);
352    }
353
354    @Override
355    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
356            throws XmlPullParserException, IOException {
357        if (mDelegateDrawable != null) {
358            DrawableCompat.inflate(mDelegateDrawable, res, parser, attrs, theme);
359            return;
360        }
361        int eventType = parser.getEventType();
362        final int innerDepth = parser.getDepth() + 1;
363
364        // Parse everything until the end of the animated-vector element.
365        while (eventType != XmlPullParser.END_DOCUMENT
366                && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
367            if (eventType == XmlPullParser.START_TAG) {
368                final String tagName = parser.getName();
369                if (DBG_ANIMATION_VECTOR_DRAWABLE) {
370                    Log.v(LOGTAG, "tagName is " + tagName);
371                }
372                if (ANIMATED_VECTOR.equals(tagName)) {
373                    final TypedArray a =
374                            obtainAttributes(res, theme, attrs,
375                                    AndroidResources.styleable_AnimatedVectorDrawable);
376
377                    int drawableRes = a.getResourceId(
378                            AndroidResources.styleable_AnimatedVectorDrawable_drawable, 0);
379                    if (DBG_ANIMATION_VECTOR_DRAWABLE) {
380                        Log.v(LOGTAG, "drawableRes is " + drawableRes);
381                    }
382                    if (drawableRes != 0) {
383                        VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(res,
384                                drawableRes, theme);
385                        vectorDrawable.setAllowCaching(false);
386                        vectorDrawable.setCallback(mCallback);
387                        if (mAnimatedVectorState.mVectorDrawable != null) {
388                            mAnimatedVectorState.mVectorDrawable.setCallback(null);
389                        }
390                        mAnimatedVectorState.mVectorDrawable = vectorDrawable;
391                    }
392                    a.recycle();
393                } else if (TARGET.equals(tagName)) {
394                    final TypedArray a =
395                            res.obtainAttributes(attrs,
396                                    AndroidResources.styleable_AnimatedVectorDrawableTarget);
397                    final String target = a.getString(
398                            AndroidResources.styleable_AnimatedVectorDrawableTarget_name);
399
400                    int id = a.getResourceId(
401                            AndroidResources.styleable_AnimatedVectorDrawableTarget_animation, 0);
402                    if (id != 0) {
403                        if (mContext != null) {
404                            Animator objectAnimator = AnimatorInflater.loadAnimator(mContext, id);
405                            setupAnimatorsForTarget(target, objectAnimator);
406                        } else {
407                            throw new IllegalStateException("Context can't be null when inflating" +
408                                    " animators");
409                        }
410                    }
411                    a.recycle();
412                }
413            }
414
415            eventType = parser.next();
416        }
417    }
418
419    @Override
420    public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs)
421            throws XmlPullParserException, IOException {
422        inflate(res, parser, attrs, null);
423    }
424
425    @Override
426    public void applyTheme(Theme t) {
427        if (mDelegateDrawable != null) {
428            DrawableCompat.applyTheme(mDelegateDrawable, t);
429            return;
430        }
431        // TODO: support theming in older platform.
432        return;
433    }
434
435    @Override
436    public boolean canApplyTheme() {
437        if (mDelegateDrawable != null) {
438            return DrawableCompat.canApplyTheme(mDelegateDrawable);
439        }
440        // TODO: support theming in older platform.
441        return false;
442    }
443
444    /**
445     * Constant state for delegating the creating drawable job.
446     * Instead of creating a VectorDrawable, create a VectorDrawableCompat instance which contains
447     * a delegated VectorDrawable instance.
448     */
449    private static class AnimatedVectorDrawableDelegateState extends ConstantState {
450        private final ConstantState mDelegateState;
451
452        public AnimatedVectorDrawableDelegateState(ConstantState state) {
453            mDelegateState = state;
454        }
455
456        @Override
457        public Drawable newDrawable() {
458            AnimatedVectorDrawableCompat drawableCompat =
459                    new AnimatedVectorDrawableCompat();
460            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable();
461            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
462            return drawableCompat;
463        }
464
465        @Override
466        public Drawable newDrawable(Resources res) {
467            AnimatedVectorDrawableCompat drawableCompat =
468                    new AnimatedVectorDrawableCompat();
469            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res);
470            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
471            return drawableCompat;
472        }
473
474        @Override
475        public Drawable newDrawable(Resources res, Theme theme) {
476            AnimatedVectorDrawableCompat drawableCompat =
477                    new AnimatedVectorDrawableCompat();
478            drawableCompat.mDelegateDrawable = mDelegateState.newDrawable(res, theme);
479            drawableCompat.mDelegateDrawable.setCallback(drawableCompat.mCallback);
480            return drawableCompat;
481        }
482
483        @Override
484        public boolean canApplyTheme() {
485            return mDelegateState.canApplyTheme();
486        }
487
488        @Override
489        public int getChangingConfigurations() {
490            return mDelegateState.getChangingConfigurations();
491        }
492    }
493
494    private static class AnimatedVectorDrawableCompatState extends ConstantState {
495        int mChangingConfigurations;
496        VectorDrawableCompat mVectorDrawable;
497        ArrayList<Animator> mAnimators;
498        ArrayMap<Animator, String> mTargetNameMap;
499
500        public AnimatedVectorDrawableCompatState(Context context,
501                AnimatedVectorDrawableCompatState copy, Callback owner, Resources res) {
502            if (copy != null) {
503                mChangingConfigurations = copy.mChangingConfigurations;
504                if (copy.mVectorDrawable != null) {
505                    final ConstantState cs = copy.mVectorDrawable.getConstantState();
506                    if (res != null) {
507                        mVectorDrawable = (VectorDrawableCompat) cs.newDrawable(res);
508                    } else {
509                        mVectorDrawable = (VectorDrawableCompat) cs.newDrawable();
510                    }
511                    mVectorDrawable = (VectorDrawableCompat) mVectorDrawable.mutate();
512                    mVectorDrawable.setCallback(owner);
513                    mVectorDrawable.setBounds(copy.mVectorDrawable.getBounds());
514                    mVectorDrawable.setAllowCaching(false);
515                }
516                if (copy.mAnimators != null) {
517                    final int numAnimators = copy.mAnimators.size();
518                    mAnimators = new ArrayList<Animator>(numAnimators);
519                    mTargetNameMap = new ArrayMap<Animator, String>(numAnimators);
520                    for (int i = 0; i < numAnimators; ++i) {
521                        Animator anim = copy.mAnimators.get(i);
522                        Animator animClone = anim.clone();
523                        String targetName = copy.mTargetNameMap.get(anim);
524                        Object targetObject = mVectorDrawable.getTargetByName(targetName);
525                        animClone.setTarget(targetObject);
526                        mAnimators.add(animClone);
527                        mTargetNameMap.put(animClone, targetName);
528                    }
529                }
530            }
531        }
532
533        @Override
534        public Drawable newDrawable() {
535            throw new IllegalStateException("No constant state support for SDK < 24.");
536        }
537
538        @Override
539        public Drawable newDrawable(Resources res) {
540            throw new IllegalStateException("No constant state support for SDK < 24.");
541        }
542
543        @Override
544        public int getChangingConfigurations() {
545            return mChangingConfigurations;
546        }
547    }
548
549    /**
550     * Utility function to fix color interpolation prior to Lollipop. Without this fix, colors
551     * are evaluated as raw integers instead of as colors, which leads to artifacts during
552     * fillColor animations.
553     */
554    private void setupColorAnimator(Animator animator) {
555        if (animator instanceof AnimatorSet) {
556            List<Animator> childAnimators = ((AnimatorSet) animator).getChildAnimations();
557            if (childAnimators != null) {
558                for (int i = 0; i < childAnimators.size(); ++i) {
559                    setupColorAnimator(childAnimators.get(i));
560                }
561            }
562        }
563        if (animator instanceof ObjectAnimator) {
564            ObjectAnimator objectAnim = (ObjectAnimator) animator;
565            final String propertyName = objectAnim.getPropertyName();
566            if ("fillColor".equals(propertyName) || "strokeColor".equals(propertyName)) {
567                if (mArgbEvaluator == null) {
568                    mArgbEvaluator = new ArgbEvaluator();
569                }
570                objectAnim.setEvaluator(mArgbEvaluator);
571            }
572        }
573    }
574
575    private void setupAnimatorsForTarget(String name, Animator animator) {
576        Object target = mAnimatedVectorState.mVectorDrawable.getTargetByName(name);
577        animator.setTarget(target);
578        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
579            setupColorAnimator(animator);
580        }
581        if (mAnimatedVectorState.mAnimators == null) {
582            mAnimatedVectorState.mAnimators = new ArrayList<Animator>();
583            mAnimatedVectorState.mTargetNameMap = new ArrayMap<Animator, String>();
584        }
585        mAnimatedVectorState.mAnimators.add(animator);
586        mAnimatedVectorState.mTargetNameMap.put(animator, name);
587        if (DBG_ANIMATION_VECTOR_DRAWABLE) {
588            Log.v(LOGTAG, "add animator  for target " + name + " " + animator);
589        }
590    }
591
592    @Override
593    public boolean isRunning() {
594        if (mDelegateDrawable != null) {
595            return ((AnimatedVectorDrawable) mDelegateDrawable).isRunning();
596        }
597        final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
598        final int size = animators.size();
599        for (int i = 0; i < size; i++) {
600            final Animator animator = animators.get(i);
601            if (animator.isRunning()) {
602                return true;
603            }
604        }
605        return false;
606    }
607
608    private boolean isStarted() {
609        final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
610        if (animators == null) {
611            return false;
612        }
613        final int size = animators.size();
614        for (int i = 0; i < size; i++) {
615            final Animator animator = animators.get(i);
616            if (animator.isRunning()) {
617                return true;
618            }
619        }
620        return false;
621    }
622
623    @Override
624    public void start() {
625        if (mDelegateDrawable != null) {
626            ((AnimatedVectorDrawable) mDelegateDrawable).start();
627            return;
628        }
629        // If any one of the animator has not ended, do nothing.
630        if (isStarted()) {
631            return;
632        }
633        // Otherwise, kick off every animator.
634        final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
635        final int size = animators.size();
636        for (int i = 0; i < size; i++) {
637            final Animator animator = animators.get(i);
638            animator.start();
639        }
640        invalidateSelf();
641    }
642
643    @Override
644    public void stop() {
645        if (mDelegateDrawable != null) {
646            ((AnimatedVectorDrawable) mDelegateDrawable).stop();
647            return;
648        }
649        final ArrayList<Animator> animators = mAnimatedVectorState.mAnimators;
650        final int size = animators.size();
651        for (int i = 0; i < size; i++) {
652            final Animator animator = animators.get(i);
653            animator.end();
654        }
655    }
656
657    final Callback mCallback = new Callback() {
658        @Override
659        public void invalidateDrawable(Drawable who) {
660            invalidateSelf();
661        }
662
663        @Override
664        public void scheduleDrawable(Drawable who, Runnable what, long when) {
665            scheduleSelf(what, when);
666        }
667
668        @Override
669        public void unscheduleDrawable(Drawable who, Runnable what) {
670            unscheduleSelf(what);
671        }
672    };
673}
674