AppCompatDrawableManager.java revision e6c800a4b82cad69b8a6f6aaae0be47823eddf8f
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.content.res.Resources;
25import android.graphics.Color;
26import android.graphics.PorterDuff;
27import android.graphics.PorterDuffColorFilter;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.Drawable.ConstantState;
30import android.graphics.drawable.DrawableContainer;
31import android.graphics.drawable.GradientDrawable;
32import android.graphics.drawable.InsetDrawable;
33import android.graphics.drawable.LayerDrawable;
34import android.graphics.drawable.StateListDrawable;
35import android.os.Build;
36import android.support.annotation.DrawableRes;
37import android.support.annotation.NonNull;
38import android.support.annotation.Nullable;
39import android.support.graphics.drawable.AnimatedVectorDrawableCompat;
40import android.support.graphics.drawable.VectorDrawableCompat;
41import android.support.v4.content.ContextCompat;
42import android.support.v4.graphics.ColorUtils;
43import android.support.v4.graphics.drawable.DrawableCompat;
44import android.support.v4.util.ArrayMap;
45import android.support.v4.util.LongSparseArray;
46import android.support.v4.util.LruCache;
47import android.support.v7.appcompat.R;
48import android.util.AttributeSet;
49import android.util.Log;
50import android.util.SparseArray;
51import android.util.TypedValue;
52import android.util.Xml;
53
54import java.lang.ref.WeakReference;
55import java.util.WeakHashMap;
56
57import static android.support.v7.widget.ColorStateListUtils.getColorStateList;
58import static android.support.v7.widget.ThemeUtils.getDisabledThemeAttrColor;
59import static android.support.v7.widget.ThemeUtils.getThemeAttrColor;
60import static android.support.v7.widget.ThemeUtils.getThemeAttrColorStateList;
61
62/**
63 * @hide
64 */
65public final class AppCompatDrawableManager {
66
67    private interface InflateDelegate {
68        Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
69                @NonNull AttributeSet attrs, @Nullable Resources.Theme theme);
70    }
71
72    private static final String TAG = "AppCompatDrawableManager";
73    private static final boolean DEBUG = false;
74    private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
75    private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip";
76
77    private static AppCompatDrawableManager INSTANCE;
78
79    public static AppCompatDrawableManager get() {
80        if (INSTANCE == null) {
81            INSTANCE = new AppCompatDrawableManager();
82            installDefaultInflateDelegates(INSTANCE);
83        }
84        return INSTANCE;
85    }
86
87    private static void installDefaultInflateDelegates(@NonNull AppCompatDrawableManager manager) {
88        final int sdk = Build.VERSION.SDK_INT;
89        if (sdk < 21) {
90            // We only want to use the automatic VectorDrawableCompat handling where it's
91            // needed: on devices running before Lollipop
92            manager.addDelegate("vector", new VdcInflateDelegate());
93
94            if (sdk >= 11) {
95                // AnimatedVectorDrawableCompat only works on API v11+
96                manager.addDelegate("animated-vector", new AvdcInflateDelegate());
97            }
98        }
99    }
100
101    private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
102
103    /**
104     * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
105     * using the default mode using a raw color filter.
106     */
107    private static final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = {
108            R.drawable.abc_textfield_search_default_mtrl_alpha,
109            R.drawable.abc_textfield_default_mtrl_alpha,
110            R.drawable.abc_ab_share_pack_mtrl_alpha
111    };
112
113    /**
114     * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, using
115     * {@link DrawableCompat}'s tinting functionality.
116     */
117    private static final int[] TINT_COLOR_CONTROL_NORMAL = {
118            R.drawable.abc_ic_search_api_mtrl_alpha,
119            R.drawable.abc_ic_commit_search_api_mtrl_alpha,
120    };
121
122    /**
123     * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
124     * using a color filter.
125     */
126    private static final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = {
127            R.drawable.abc_textfield_activated_mtrl_alpha,
128            R.drawable.abc_textfield_search_activated_mtrl_alpha,
129            R.drawable.abc_cab_background_top_mtrl_alpha,
130            R.drawable.abc_text_cursor_material
131    };
132
133    /**
134     * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
135     * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter.
136     */
137    private static final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = {
138            R.drawable.abc_popup_background_mtrl_mult,
139            R.drawable.abc_cab_background_internal_bg,
140            R.drawable.abc_menu_hardkey_panel_mtrl_mult
141    };
142
143    /**
144     * Drawables which should be tinted using a state list containing values of
145     * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
146     */
147    private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
148            R.drawable.abc_tab_indicator_material,
149            R.drawable.abc_textfield_search_material,
150            R.drawable.abc_ratingbar_full_material
151    };
152
153    /**
154     * Drawables which should be tinted using a state list containing values of
155     * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} for the checked
156     * state.
157     */
158    private static final int[] TINT_CHECKABLE_BUTTON_LIST = {
159            R.drawable.abc_btn_check_material,
160            R.drawable.abc_btn_radio_material
161    };
162
163    private WeakHashMap<Context, SparseArray<ColorStateList>> mTintLists;
164    private ArrayMap<String, InflateDelegate> mDelegates;
165    private SparseArray<String> mKnownDrawableIdTags;
166
167    private final Object mDelegateDrawableCacheLock = new Object();
168    private final WeakHashMap<Context, LongSparseArray<WeakReference<Drawable.ConstantState>>>
169            mDelegateDrawableCaches = new WeakHashMap<>(0);
170
171    private TypedValue mTypedValue;
172
173    public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
174        return getDrawable(context, resId, false);
175    }
176
177    public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
178            boolean failIfNotKnown) {
179        Drawable drawable = loadDrawableFromDelegates(context, resId);
180        if (drawable == null) {
181            drawable = ContextCompat.getDrawable(context, resId);
182        }
183        if (drawable != null) {
184            // Tint it if needed
185            drawable = tintDrawable(context, resId, failIfNotKnown, drawable);
186        }
187        if (drawable != null) {
188            // See if we need to 'fix' the drawable
189            DrawableUtils.fixDrawable(drawable);
190        }
191        return drawable;
192    }
193
194    private Drawable tintDrawable(@NonNull Context context, @DrawableRes int resId,
195            boolean failIfNotKnown, @NonNull Drawable drawable) {
196        final ColorStateList tintList = getTintList(context, resId);
197        if (tintList != null) {
198            // First mutate the Drawable, then wrap it and set the tint list
199            if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
200                drawable = drawable.mutate();
201            }
202            drawable = DrawableCompat.wrap(drawable);
203            DrawableCompat.setTintList(drawable, tintList);
204
205            // If there is a blending mode specified for the drawable, use it
206            final PorterDuff.Mode tintMode = getTintMode(resId);
207            if (tintMode != null) {
208                DrawableCompat.setTintMode(drawable, tintMode);
209            }
210        } else if (resId == R.drawable.abc_cab_background_top_material) {
211            return new LayerDrawable(new Drawable[]{
212                    getDrawable(context, R.drawable.abc_cab_background_internal_bg),
213                    getDrawable(context, R.drawable.abc_cab_background_top_mtrl_alpha)
214            });
215        } else if (resId == R.drawable.abc_seekbar_track_material) {
216            LayerDrawable ld = (LayerDrawable) drawable;
217            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background),
218                    getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE);
219            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress),
220                    getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE);
221            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress),
222                    getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
223        } else if (resId == R.drawable.abc_ratingbar_indicator_material
224                || resId == R.drawable.abc_ratingbar_small_material) {
225            LayerDrawable ld = (LayerDrawable) drawable;
226            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background),
227                    getDisabledThemeAttrColor(context, R.attr.colorControlNormal),
228                    DEFAULT_MODE);
229            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress),
230                    getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
231            setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress),
232                    getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE);
233        } else {
234            final boolean tinted = tintDrawableUsingColorFilter(context, resId, drawable);
235            if (!tinted && failIfNotKnown) {
236                // If we didn't tint using a ColorFilter, and we're set to fail if we don't
237                // know the id, return null
238                drawable = null;
239            }
240        }
241        return drawable;
242    }
243
244    private Drawable loadDrawableFromDelegates(@NonNull Context context, @DrawableRes int resId) {
245        if (mDelegates != null && !mDelegates.isEmpty()) {
246            if (mKnownDrawableIdTags != null) {
247                final String cachedTagName = mKnownDrawableIdTags.get(resId);
248                if (SKIP_DRAWABLE_TAG.equals(cachedTagName)
249                        || (cachedTagName != null && mDelegates.get(cachedTagName) == null)) {
250                    // If we don't have a delegate for the drawable tag, or we've been set to
251                    // skip it, fail fast and return null
252                    if (DEBUG) {
253                        Log.d(TAG, "[loadDrawableFromDelegates] Skipping drawable: "
254                                + context.getResources().getResourceName(resId));
255                    }
256                    return null;
257                }
258            } else {
259                // Create an id cache as we'll need one later
260                mKnownDrawableIdTags = new SparseArray<>();
261            }
262
263            if (mTypedValue == null) {
264                mTypedValue = new TypedValue();
265            }
266
267            final TypedValue tv = mTypedValue;
268            final Resources res = context.getResources();
269            res.getValue(resId, tv, true);
270
271            final long key = (((long) tv.assetCookie) << 32) | tv.data;
272
273            Drawable dr = getCachedDelegateDrawable(context, key);
274            if (dr != null) {
275                if (DEBUG) {
276                    Log.i(TAG, "[loadDrawableFromDelegates] Returning cached drawable: " +
277                            context.getResources().getResourceName(resId));
278                }
279                // We have a cached drawable, return it!
280                return dr;
281            }
282
283            if (tv.string != null && tv.string.toString().endsWith(".xml")) {
284                // If the resource is an XML file, let's try and parse it
285                try {
286                    final XmlPullParser parser = res.getXml(resId);
287                    final AttributeSet attrs = Xml.asAttributeSet(parser);
288                    int type;
289                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
290                            type != XmlPullParser.END_DOCUMENT) {
291                        // Empty loop
292                    }
293                    if (type != XmlPullParser.START_TAG) {
294                        throw new XmlPullParserException("No start tag found");
295                    }
296
297                    final String tagName = parser.getName();
298                    // Add the tag name to the cache
299                    mKnownDrawableIdTags.append(resId, tagName);
300
301                    // Now try and find a delegate for the tag name and inflate if found
302                    final InflateDelegate delegate = mDelegates.get(tagName);
303                    if (delegate != null) {
304                        dr = delegate.createFromXmlInner(context, parser, attrs,
305                                context.getTheme());
306                    }
307                    if (dr != null) {
308                        // Add it to the drawable cache
309                        dr.setChangingConfigurations(tv.changingConfigurations);
310                        if (addCachedDelegateDrawable(context, key, dr) && DEBUG) {
311                            Log.i(TAG, "[loadDrawableFromDelegates] Saved drawable to cache: " +
312                                    context.getResources().getResourceName(resId));
313                        }
314                    }
315                } catch (Exception e) {
316                    Log.e(TAG, "Exception while inflating drawable", e);
317                }
318            }
319            if (dr == null) {
320                // If we reach here then the delegate inflation of the resource failed. Mark it as
321                // bad so we skip the id next time
322                mKnownDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG);
323            }
324            return dr;
325        }
326
327        return null;
328    }
329
330    private Drawable getCachedDelegateDrawable(@NonNull final Context context, final long key) {
331        synchronized (mDelegateDrawableCacheLock) {
332            final LongSparseArray<WeakReference<ConstantState>> cache
333                    = mDelegateDrawableCaches.get(context);
334            if (cache == null) {
335                return null;
336            }
337
338            final WeakReference<ConstantState> wr = cache.get(key);
339            if (wr != null) {
340                // We have the key, and the secret
341                ConstantState entry = wr.get();
342                if (entry != null) {
343                    return entry.newDrawable(context.getResources());
344                } else {
345                    // Our entry has been purged
346                    cache.delete(key);
347                }
348            }
349        }
350        return null;
351    }
352
353    private boolean addCachedDelegateDrawable(@NonNull final Context context, final long key,
354            @NonNull final Drawable drawable) {
355        final ConstantState cs = drawable.getConstantState();
356        if (cs != null) {
357            synchronized (mDelegateDrawableCacheLock) {
358                LongSparseArray<WeakReference<ConstantState>> cache
359                        = mDelegateDrawableCaches.get(context);
360                if (cache == null) {
361                    cache = new LongSparseArray<>();
362                    mDelegateDrawableCaches.put(context, cache);
363                }
364                cache.put(key, new WeakReference<ConstantState>(cs));
365            }
366            return true;
367        }
368        return false;
369    }
370
371    public final Drawable onDrawableLoadedFromResources(@NonNull Context context,
372            @NonNull TintResources resources, @DrawableRes final int resId) {
373        Drawable drawable = loadDrawableFromDelegates(context, resId);
374        if (drawable == null) {
375            drawable = resources.superGetDrawable(resId);
376        }
377        if (drawable != null) {
378            return tintDrawable(context, resId, false, drawable);
379        }
380        return null;
381    }
382
383    private static boolean tintDrawableUsingColorFilter(@NonNull Context context,
384            @DrawableRes final int resId, @NonNull Drawable drawable) {
385        PorterDuff.Mode tintMode = DEFAULT_MODE;
386        boolean colorAttrSet = false;
387        int colorAttr = 0;
388        int alpha = -1;
389
390        if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) {
391            colorAttr = R.attr.colorControlNormal;
392            colorAttrSet = true;
393        } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) {
394            colorAttr = R.attr.colorControlActivated;
395            colorAttrSet = true;
396        } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) {
397            colorAttr = android.R.attr.colorBackground;
398            colorAttrSet = true;
399            tintMode = PorterDuff.Mode.MULTIPLY;
400        } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
401            colorAttr = android.R.attr.colorForeground;
402            colorAttrSet = true;
403            alpha = Math.round(0.16f * 255);
404        }
405
406        if (colorAttrSet) {
407            if (DrawableUtils.canSafelyMutateDrawable(drawable)) {
408                drawable = drawable.mutate();
409            }
410
411            final int color = getThemeAttrColor(context, colorAttr);
412            drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode));
413
414            if (alpha != -1) {
415                drawable.setAlpha(alpha);
416            }
417
418            if (DEBUG) {
419                Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted "
420                        + context.getResources().getResourceName(resId) +
421                        " with color: #" + Integer.toHexString(color));
422            }
423            return true;
424        }
425        return false;
426    }
427
428    private void addDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) {
429        if (mDelegates == null) {
430            mDelegates = new ArrayMap<>();
431        }
432        mDelegates.put(tagName, delegate);
433    }
434
435    private void removeDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) {
436        if (mDelegates != null && mDelegates.get(tagName) == delegate) {
437            mDelegates.remove(tagName);
438        }
439    }
440
441    private static boolean arrayContains(int[] array, int value) {
442        for (int id : array) {
443            if (id == value) {
444                return true;
445            }
446        }
447        return false;
448    }
449
450    final PorterDuff.Mode getTintMode(final int resId) {
451        PorterDuff.Mode mode = null;
452
453        if (resId == R.drawable.abc_switch_thumb_material) {
454            mode = PorterDuff.Mode.MULTIPLY;
455        }
456
457        return mode;
458    }
459
460    public final ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) {
461        // Try the cache first (if it exists)
462        ColorStateList tint = getTintListFromCache(context, resId);
463
464        if (tint == null) {
465            // ...if the cache did not contain a color state list, try and create one
466            if (resId == R.drawable.abc_edit_text_material) {
467                tint = getColorStateList(context, R.color.abc_tint_edittext);
468            } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
469                tint = getColorStateList(context, R.color.abc_tint_switch_track);
470            } else if (resId == R.drawable.abc_switch_thumb_material) {
471                tint = getColorStateList(context, R.color.abc_tint_switch_thumb);
472            } else if (resId == R.drawable.abc_btn_default_mtrl_shape
473                    || resId == R.drawable.abc_btn_borderless_material) {
474                tint = createDefaultButtonColorStateList(context);
475            } else if (resId == R.drawable.abc_btn_colored_material) {
476                tint = createColoredButtonColorStateList(context);
477            } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha
478                    || resId == R.drawable.abc_spinner_textfield_background_material) {
479                tint = getColorStateList(context, R.color.abc_tint_spinner);
480            } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
481                tint = getThemeAttrColorStateList(context, R.attr.colorControlNormal);
482            } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
483                tint = getColorStateList(context, R.color.abc_tint_default);
484            } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) {
485                tint = getColorStateList(context, R.color.abc_tint_btn_checkable);
486            } else if (resId == R.drawable.abc_seekbar_thumb_material) {
487                tint = getColorStateList(context, R.color.abc_tint_seek_thumb);
488            }
489
490            if (tint != null) {
491                addTintListToCache(context, resId, tint);
492            }
493        }
494        return tint;
495    }
496
497    private ColorStateList getTintListFromCache(@NonNull Context context, @DrawableRes int resId) {
498        if (mTintLists != null) {
499            final SparseArray<ColorStateList> tints = mTintLists.get(context);
500            return tints != null ? tints.get(resId) : null;
501        }
502        return null;
503    }
504
505    private void addTintListToCache(@NonNull Context context, @DrawableRes int resId,
506            @NonNull ColorStateList tintList) {
507        if (mTintLists == null) {
508            mTintLists = new WeakHashMap<>();
509        }
510        SparseArray<ColorStateList> themeTints = mTintLists.get(context);
511        if (themeTints == null) {
512            themeTints = new SparseArray<>();
513            mTintLists.put(context, themeTints);
514        }
515        themeTints.append(resId, tintList);
516    }
517
518    private ColorStateList createDefaultButtonColorStateList(Context context) {
519        return createButtonColorStateList(context, R.attr.colorButtonNormal);
520    }
521
522    private ColorStateList createColoredButtonColorStateList(Context context) {
523        return createButtonColorStateList(context, R.attr.colorAccent);
524    }
525
526    private ColorStateList createButtonColorStateList(Context context, int baseColorAttr) {
527        final int[][] states = new int[4][];
528        final int[] colors = new int[4];
529        int i = 0;
530
531        final int baseColor = getThemeAttrColor(context, baseColorAttr);
532        final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight);
533
534        // Disabled state
535        states[i] = ThemeUtils.DISABLED_STATE_SET;
536        colors[i] = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal);
537        i++;
538
539        states[i] = ThemeUtils.PRESSED_STATE_SET;
540        colors[i] = ColorUtils.compositeColors(colorControlHighlight, baseColor);
541        i++;
542
543        states[i] = ThemeUtils.FOCUSED_STATE_SET;
544        colors[i] = ColorUtils.compositeColors(colorControlHighlight, baseColor);
545        i++;
546
547        // Default enabled state
548        states[i] = ThemeUtils.EMPTY_STATE_SET;
549        colors[i] = baseColor;
550        i++;
551
552        return new ColorStateList(states, colors);
553    }
554
555    private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
556
557        public ColorFilterLruCache(int maxSize) {
558            super(maxSize);
559        }
560
561        PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
562            return get(generateCacheKey(color, mode));
563        }
564
565        PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
566            return put(generateCacheKey(color, mode), filter);
567        }
568
569        private static int generateCacheKey(int color, PorterDuff.Mode mode) {
570            int hashCode = 1;
571            hashCode = 31 * hashCode + color;
572            hashCode = 31 * hashCode + mode.hashCode();
573            return hashCode;
574        }
575    }
576
577    public static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {
578        if (DrawableUtils.canSafelyMutateDrawable(drawable)
579                && drawable.mutate() != drawable) {
580            Log.d(TAG, "Mutated drawable is not the same instance as the input.");
581            return;
582        }
583
584        if (tint.mHasTintList || tint.mHasTintMode) {
585            drawable.setColorFilter(createTintFilter(
586                    tint.mHasTintList ? tint.mTintList : null,
587                    tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
588                    state));
589        } else {
590            drawable.clearColorFilter();
591        }
592
593        if (Build.VERSION.SDK_INT <= 23) {
594            // Pre-v23 there is no guarantee that a state change will invoke an invalidation,
595            // so we force it ourselves
596            drawable.invalidateSelf();
597        }
598    }
599
600    private static PorterDuffColorFilter createTintFilter(ColorStateList tint,
601            PorterDuff.Mode tintMode, final int[] state) {
602        if (tint == null || tintMode == null) {
603            return null;
604        }
605        final int color = tint.getColorForState(state, Color.TRANSPARENT);
606        return getPorterDuffColorFilter(color, tintMode);
607    }
608
609    public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
610        // First, lets see if the cache already contains the color filter
611        PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
612
613        if (filter == null) {
614            // Cache miss, so create a color filter and add it to the cache
615            filter = new PorterDuffColorFilter(color, mode);
616            COLOR_FILTER_CACHE.put(color, mode, filter);
617        }
618
619        return filter;
620    }
621
622    private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
623        if (DrawableUtils.canSafelyMutateDrawable(d)) {
624            d = d.mutate();
625        }
626        d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE : mode));
627    }
628
629    private static class VdcInflateDelegate implements InflateDelegate {
630        @Override
631        public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
632                @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) {
633            try {
634                return VectorDrawableCompat
635                        .createFromXmlInner(context.getResources(), parser, attrs, theme);
636            } catch (Exception e) {
637                Log.e("VdcInflateDelegate", "Exception while inflating <vector>", e);
638                return null;
639            }
640        }
641    }
642
643    private static class AvdcInflateDelegate implements InflateDelegate {
644        @Override
645        public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser,
646                @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) {
647            try {
648                return AnimatedVectorDrawableCompat
649                        .createFromXmlInner(context, context.getResources(), parser, attrs, theme);
650            } catch (Exception e) {
651                Log.e("AvdcInflateDelegate", "Exception while inflating <animated-vector>", e);
652                return null;
653            }
654        }
655    }
656}
657