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