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