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