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