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