TintManager.java revision 7e82b99953680915596eaf0eb35927388e574ca8
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.internal.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.Resources;
22import android.graphics.Color;
23import android.graphics.PorterDuff;
24import android.graphics.PorterDuffColorFilter;
25import android.graphics.drawable.Drawable;
26import android.os.Build;
27import android.support.v4.content.ContextCompat;
28import android.support.v4.graphics.drawable.DrawableCompat;
29import android.support.v4.util.LruCache;
30import android.support.v7.appcompat.R;
31import android.util.Log;
32import android.util.SparseArray;
33import android.util.TypedValue;
34import android.view.View;
35
36/**
37 * @hide
38 */
39public final class TintManager {
40
41    static final boolean SHOULD_BE_USED = Build.VERSION.SDK_INT < 21;
42
43    private static final String TAG = TintManager.class.getSimpleName();
44    private static final boolean DEBUG = false;
45
46    static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
47
48    private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
49
50    /**
51     * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
52     * using the default mode.
53     */
54    private static final int[] TINT_COLOR_CONTROL_NORMAL = {
55            R.drawable.abc_ic_ab_back_mtrl_am_alpha,
56            R.drawable.abc_ic_go_search_api_mtrl_alpha,
57            R.drawable.abc_ic_search_api_mtrl_alpha,
58            R.drawable.abc_ic_commit_search_api_mtrl_alpha,
59            R.drawable.abc_ic_clear_mtrl_alpha,
60            R.drawable.abc_ic_menu_share_mtrl_alpha,
61            R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
62            R.drawable.abc_ic_menu_cut_mtrl_alpha,
63            R.drawable.abc_ic_menu_selectall_mtrl_alpha,
64            R.drawable.abc_ic_menu_paste_mtrl_am_alpha,
65            R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha,
66            R.drawable.abc_ic_voice_search_api_mtrl_alpha,
67            R.drawable.abc_textfield_search_default_mtrl_alpha,
68            R.drawable.abc_textfield_default_mtrl_alpha,
69            R.drawable.abc_ab_share_pack_mtrl_alpha
70    };
71
72    /**
73     * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
74     * using the default mode.
75     */
76    private static final int[] TINT_COLOR_CONTROL_ACTIVATED = {
77            R.drawable.abc_textfield_activated_mtrl_alpha,
78            R.drawable.abc_textfield_search_activated_mtrl_alpha,
79            R.drawable.abc_cab_background_top_mtrl_alpha,
80            R.drawable.abc_text_cursor_mtrl_alpha
81    };
82
83    /**
84     * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
85     * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode.
86     */
87    private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = {
88            R.drawable.abc_popup_background_mtrl_mult,
89            R.drawable.abc_cab_background_internal_bg,
90            R.drawable.abc_menu_hardkey_panel_mtrl_mult
91    };
92
93    /**
94     * Drawables which should be tinted using a state list containing values of
95     * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
96     */
97    private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
98            R.drawable.abc_edit_text_material,
99            R.drawable.abc_tab_indicator_material,
100            R.drawable.abc_textfield_search_material,
101            R.drawable.abc_spinner_mtrl_am_alpha,
102            R.drawable.abc_btn_check_material,
103            R.drawable.abc_btn_radio_material,
104            R.drawable.abc_spinner_textfield_background_material,
105            R.drawable.abc_ratingbar_full_material,
106            R.drawable.abc_switch_track_mtrl_alpha,
107            R.drawable.abc_switch_thumb_material,
108            R.drawable.abc_btn_default_mtrl_shape,
109            R.drawable.abc_btn_borderless_material
110    };
111
112    /**
113     * Drawables which contain other drawables which should be tinted. The child drawable IDs
114     * should be defined in one of the arrays above.
115     */
116    private static final int[] CONTAINERS_WITH_TINT_CHILDREN = {
117            R.drawable.abc_cab_background_top_material
118    };
119
120    private final Context mContext;
121    private final Resources mResources;
122    private final TypedValue mTypedValue;
123
124    private final SparseArray<ColorStateList> mColorStateLists;
125    private ColorStateList mDefaultColorStateList;
126
127    /**
128     * A helper method to instantiate a {@link TintManager} and then call {@link #getDrawable(int)}.
129     * This method should not be used routinely.
130     */
131    public static Drawable getDrawable(Context context, int resId) {
132        if (isInTintList(resId)) {
133            final TintManager tm = (context instanceof TintContextWrapper)
134                    ? ((TintContextWrapper) context).getTintManager()
135                    : new TintManager(context);
136            return tm.getDrawable(resId);
137        } else {
138            return ContextCompat.getDrawable(context, resId);
139        }
140    }
141
142    public TintManager(Context context) {
143        mColorStateLists = new SparseArray<>();
144        mContext = context;
145        mTypedValue = new TypedValue();
146        mResources = new TintResources(context.getResources(), this);
147    }
148
149    Resources getResources() {
150        return mResources;
151    }
152
153    public Drawable getDrawable(int resId) {
154        Drawable drawable = ContextCompat.getDrawable(mContext, resId);
155
156        if (drawable != null) {
157            drawable = drawable.mutate();
158
159            if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
160                ColorStateList colorStateList = getColorStateListForKnownDrawableId(resId);
161                PorterDuff.Mode tintMode = DEFAULT_MODE;
162                if (resId == R.drawable.abc_switch_thumb_material) {
163                    tintMode = PorterDuff.Mode.MULTIPLY;
164                }
165
166                if (colorStateList != null) {
167                    drawable = DrawableCompat.wrap(drawable);
168                    DrawableCompat.setTintList(drawable, colorStateList);
169                    DrawableCompat.setTintMode(drawable, tintMode);
170                }
171            } else if (arrayContains(CONTAINERS_WITH_TINT_CHILDREN, resId)) {
172                drawable = mResources.getDrawable(resId);
173            } else {
174                tintDrawable(resId, drawable);
175            }
176        }
177        return drawable;
178    }
179
180    void tintDrawable(final int resId, final Drawable drawable) {
181        PorterDuff.Mode tintMode = null;
182        boolean colorAttrSet = false;
183        int colorAttr = 0;
184        int alpha = -1;
185
186        if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
187            colorAttr = R.attr.colorControlNormal;
188            colorAttrSet = true;
189        } else if (arrayContains(TINT_COLOR_CONTROL_ACTIVATED, resId)) {
190            colorAttr = R.attr.colorControlActivated;
191            colorAttrSet = true;
192        } else if (arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, resId)) {
193            colorAttr = android.R.attr.colorBackground;
194            colorAttrSet = true;
195            tintMode = PorterDuff.Mode.MULTIPLY;
196        } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
197            colorAttr = android.R.attr.colorForeground;
198            colorAttrSet = true;
199            alpha = Math.round(0.16f * 255);
200        }
201
202        if (colorAttrSet) {
203            if (tintMode == null) {
204                tintMode = DEFAULT_MODE;
205            }
206            final int color = getThemeAttrColor(colorAttr);
207
208            tintDrawableUsingColorFilter(drawable, color, tintMode);
209
210            if (alpha != -1) {
211                drawable.setAlpha(alpha);
212            }
213
214            if (DEBUG) {
215                Log.d(TAG, "Tinted Drawable ID: " + mResources.getResourceName(resId) +
216                        " with color: #" + Integer.toHexString(color));
217            }
218        }
219    }
220
221    private static boolean arrayContains(int[] array, int value) {
222        for (int id : array) {
223            if (id == value) {
224                return true;
225            }
226        }
227        return false;
228    }
229
230    private static boolean isInTintList(int drawableId) {
231        return arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, drawableId) ||
232                arrayContains(TINT_COLOR_CONTROL_NORMAL, drawableId) ||
233                arrayContains(TINT_COLOR_CONTROL_ACTIVATED, drawableId) ||
234                arrayContains(TINT_COLOR_CONTROL_STATE_LIST, drawableId) ||
235                arrayContains(CONTAINERS_WITH_TINT_CHILDREN, drawableId);
236    }
237
238    ColorStateList getColorStateList(int resId) {
239        return arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)
240                ? getColorStateListForKnownDrawableId(resId)
241                : null;
242    }
243
244    private ColorStateList getColorStateListForKnownDrawableId(int resId) {
245        // Try the cache first
246        ColorStateList colorStateList = mColorStateLists.get(resId);
247
248        if (colorStateList == null) {
249            // ...if the cache did not contain a color state list, try and create
250            if (resId == R.drawable.abc_edit_text_material) {
251                colorStateList = createEditTextColorStateList();
252            } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
253                colorStateList = createSwitchTrackColorStateList();
254            } else if (resId == R.drawable.abc_switch_thumb_material) {
255                colorStateList = createSwitchThumbColorStateList();
256            } else if (resId == R.drawable.abc_btn_default_mtrl_shape
257                    || resId == R.drawable.abc_btn_borderless_material) {
258                colorStateList = createButtonColorStateList();
259            } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha
260                    || resId == R.drawable.abc_spinner_textfield_background_material) {
261                colorStateList = createSpinnerColorStateList();
262            } else {
263                // If we don't have an explicit color state list for this Drawable, use the default
264                colorStateList = getDefaultColorStateList();
265            }
266
267            // ..and add it to the cache
268            mColorStateLists.append(resId, colorStateList);
269        }
270        return colorStateList;
271    }
272
273    private ColorStateList getDefaultColorStateList() {
274        if (mDefaultColorStateList == null) {
275            /**
276             * Generate the default color state list which uses the colorControl attributes.
277             * Order is important here. The default enabled state needs to go at the bottom.
278             */
279
280            final int colorControlNormal = getThemeAttrColor(R.attr.colorControlNormal);
281            final int colorControlActivated = getThemeAttrColor(R.attr.colorControlActivated);
282
283            final int[][] states = new int[7][];
284            final int[] colors = new int[7];
285            int i = 0;
286
287            // Disabled state
288            states[i] = new int[] { -android.R.attr.state_enabled };
289            colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal);
290            i++;
291
292            states[i] = new int[] { android.R.attr.state_focused };
293            colors[i] = colorControlActivated;
294            i++;
295
296            states[i] = new int[] { android.R.attr.state_activated };
297            colors[i] = colorControlActivated;
298            i++;
299
300            states[i] = new int[] { android.R.attr.state_pressed };
301            colors[i] = colorControlActivated;
302            i++;
303
304            states[i] = new int[] { android.R.attr.state_checked };
305            colors[i] = colorControlActivated;
306            i++;
307
308            states[i] = new int[] { android.R.attr.state_selected };
309            colors[i] = colorControlActivated;
310            i++;
311
312            // Default enabled state
313            states[i] = new int[0];
314            colors[i] = colorControlNormal;
315            i++;
316
317            mDefaultColorStateList = new ColorStateList(states, colors);
318        }
319        return mDefaultColorStateList;
320    }
321
322    private ColorStateList createSwitchTrackColorStateList() {
323        final int[][] states = new int[3][];
324        final int[] colors = new int[3];
325        int i = 0;
326
327        // Disabled state
328        states[i] = new int[]{-android.R.attr.state_enabled};
329        colors[i] = getThemeAttrColor(android.R.attr.colorForeground, 0.1f);
330        i++;
331
332        states[i] = new int[]{android.R.attr.state_checked};
333        colors[i] = getThemeAttrColor(R.attr.colorControlActivated, 0.3f);
334        i++;
335
336        // Default enabled state
337        states[i] = new int[0];
338        colors[i] = getThemeAttrColor(android.R.attr.colorForeground, 0.3f);
339        i++;
340
341        return new ColorStateList(states, colors);
342    }
343
344    private ColorStateList createSwitchThumbColorStateList() {
345        final int[][] states = new int[3][];
346        final int[] colors = new int[3];
347        int i = 0;
348
349        final ColorStateList thumbColor = getThemeAttrColorStateList(R.attr.colorSwitchThumbNormal);
350
351        if (thumbColor != null && thumbColor.isStateful()) {
352            // If colorSwitchThumbNormal is a valid ColorStateList, extract the default and
353            // disabled colors from it
354
355            // Disabled state
356            states[i] = new int[]{-android.R.attr.state_enabled};
357            colors[i] = thumbColor.getColorForState(states[i], 0);
358            i++;
359
360            states[i] = new int[]{android.R.attr.state_checked};
361            colors[i] = getThemeAttrColor(R.attr.colorControlActivated);
362            i++;
363
364            // Default enabled state
365            states[i] = new int[0];
366            colors[i] = thumbColor.getDefaultColor();
367            i++;
368        } else {
369            // Else we'll use an approximation using the default disabled alpha
370
371            // Disabled state
372            states[i] = new int[]{-android.R.attr.state_enabled};
373            colors[i] = getDisabledThemeAttrColor(R.attr.colorSwitchThumbNormal);
374            i++;
375
376            states[i] = new int[]{android.R.attr.state_checked};
377            colors[i] = getThemeAttrColor(R.attr.colorControlActivated);
378            i++;
379
380            // Default enabled state
381            states[i] = new int[0];
382            colors[i] = getThemeAttrColor(R.attr.colorSwitchThumbNormal);
383            i++;
384        }
385
386        return new ColorStateList(states, colors);
387    }
388
389    private ColorStateList createEditTextColorStateList() {
390        final int[][] states = new int[3][];
391        final int[] colors = new int[3];
392        int i = 0;
393
394        // Disabled state
395        states[i] = new int[]{-android.R.attr.state_enabled};
396        colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal);
397        i++;
398
399        states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused};
400        colors[i] = getThemeAttrColor(R.attr.colorControlNormal);
401        i++;
402
403        // Default enabled state
404        states[i] = new int[0];
405        colors[i] = getThemeAttrColor(R.attr.colorControlActivated);
406        i++;
407
408        return new ColorStateList(states, colors);
409    }
410
411    private ColorStateList createButtonColorStateList() {
412        final int[][] states = new int[4][];
413        final int[] colors = new int[4];
414        int i = 0;
415
416        // Disabled state
417        states[i] = new int[]{-android.R.attr.state_enabled};
418        colors[i] = getDisabledThemeAttrColor(R.attr.colorButtonNormal);
419        i++;
420
421        states[i] = new int[]{android.R.attr.state_pressed};
422        colors[i] = getThemeAttrColor(R.attr.colorControlHighlight);
423        i++;
424
425        states[i] = new int[]{android.R.attr.state_focused};
426        colors[i] = getThemeAttrColor(R.attr.colorControlHighlight);
427        i++;
428
429        // Default enabled state
430        states[i] = new int[0];
431        colors[i] = getThemeAttrColor(R.attr.colorButtonNormal);
432        i++;
433
434        return new ColorStateList(states, colors);
435    }
436
437    private ColorStateList createSpinnerColorStateList() {
438        final int[][] states = new int[3][];
439        final int[] colors = new int[3];
440        int i = 0;
441
442        // Disabled state
443        states[i] = new int[]{-android.R.attr.state_enabled};
444        colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal);
445        i++;
446
447        states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused};
448        colors[i] = getThemeAttrColor(R.attr.colorControlNormal);
449        i++;
450
451        states[i] = new int[0];
452        colors[i] = getThemeAttrColor(R.attr.colorControlActivated);
453        i++;
454
455        return new ColorStateList(states, colors);
456    }
457
458    private int getThemeAttrColor(int attr) {
459        if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) {
460            if (mTypedValue.type >= TypedValue.TYPE_FIRST_INT
461                    && mTypedValue.type <= TypedValue.TYPE_LAST_INT) {
462                return mTypedValue.data;
463            } else if (mTypedValue.type == TypedValue.TYPE_STRING) {
464                return mResources.getColor(mTypedValue.resourceId);
465            }
466        }
467        return 0;
468    }
469
470    private ColorStateList getThemeAttrColorStateList(int attr) {
471        if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) {
472            if (mTypedValue.type == TypedValue.TYPE_STRING) {
473                return mResources.getColorStateList(mTypedValue.resourceId);
474            }
475        }
476        return null;
477    }
478
479    private int getThemeAttrColor(int attr, float alpha) {
480        final int color = getThemeAttrColor(attr);
481        final int originalAlpha = Color.alpha(color);
482
483        // Return the color, multiplying the original alpha by the disabled value
484        return (color & 0x00ffffff) | (Math.round(originalAlpha * alpha) << 24);
485    }
486
487    private int getDisabledThemeAttrColor(int attr) {
488        // Now retrieve the disabledAlpha value from the theme
489        mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, mTypedValue, true);
490        final float disabledAlpha = mTypedValue.getFloat();
491
492        return getThemeAttrColor(attr, disabledAlpha);
493    }
494
495    private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
496
497        public ColorFilterLruCache(int maxSize) {
498            super(maxSize);
499        }
500
501        PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
502            return get(generateCacheKey(color, mode));
503        }
504
505        PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
506            return put(generateCacheKey(color, mode), filter);
507        }
508
509        private static int generateCacheKey(int color, PorterDuff.Mode mode) {
510            int hashCode = 1;
511            hashCode = 31 * hashCode + color;
512            hashCode = 31 * hashCode + mode.hashCode();
513            return hashCode;
514        }
515    }
516
517    public static void tintViewBackground(View view, TintInfo tint) {
518        final Drawable background = view.getBackground();
519        if (tint.mTintList != null) {
520            tintDrawableUsingColorFilter(
521                    background,
522                    tint.mTintList.getColorForState(view.getDrawableState(),
523                            tint.mTintList.getDefaultColor()),
524                    tint.mTintMode != null ? tint.mTintMode : DEFAULT_MODE);
525        } else {
526            background.clearColorFilter();
527        }
528    }
529
530    private static void tintDrawableUsingColorFilter(Drawable drawable, int color,
531            PorterDuff.Mode mode) {
532        // First, lets see if the cache already contains the color filter
533        PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
534
535        if (filter == null) {
536            // Cache miss, so create a color filter and add it to the cache
537            filter = new PorterDuffColorFilter(color, mode);
538            COLOR_FILTER_CACHE.put(color, mode, filter);
539        }
540
541        drawable.setColorFilter(filter);
542    }
543}
544