TintManager.java revision 57c6de90985a63358129b99b9f0cd4d6afe887d6
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.support.v4.content.ContextCompat;
27import android.support.v4.util.LruCache;
28import android.support.v7.appcompat.R;
29import android.util.Log;
30import android.util.TypedValue;
31
32/**
33 * @hide
34 */
35public class TintManager {
36
37    private static final String TAG = TintManager.class.getSimpleName();
38    private static final boolean DEBUG = false;
39    private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
40
41    private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
42
43    /**
44     * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
45     * using the default mode.
46     */
47    private static final int[] TINT_COLOR_CONTROL_NORMAL = {
48            R.drawable.abc_ic_ab_back_mtrl_am_alpha,
49            R.drawable.abc_ic_go_search_api_mtrl_alpha,
50            R.drawable.abc_ic_search_api_mtrl_alpha,
51            R.drawable.abc_ic_commit_search_api_mtrl_alpha,
52            R.drawable.abc_ic_clear_mtrl_alpha,
53            R.drawable.abc_ic_menu_share_mtrl_alpha,
54            R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
55            R.drawable.abc_ic_menu_cut_mtrl_alpha,
56            R.drawable.abc_ic_menu_selectall_mtrl_alpha,
57            R.drawable.abc_ic_menu_paste_mtrl_am_alpha,
58            R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha,
59            R.drawable.abc_ic_voice_search_api_mtrl_alpha,
60            R.drawable.abc_textfield_search_default_mtrl_alpha,
61            R.drawable.abc_textfield_default_mtrl_alpha
62    };
63
64    /**
65     * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
66     * using the default mode.
67     */
68    private static final int[] TINT_COLOR_CONTROL_ACTIVATED = {
69            R.drawable.abc_textfield_activated_mtrl_alpha,
70            R.drawable.abc_textfield_search_activated_mtrl_alpha,
71            R.drawable.abc_cab_background_top_mtrl_alpha
72    };
73
74    /**
75     * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
76     * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode.
77     */
78    private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = {
79            R.drawable.abc_popup_background_mtrl_mult,
80            R.drawable.abc_cab_background_internal_bg,
81            R.drawable.abc_menu_hardkey_panel_mtrl_mult
82    };
83
84    /**
85     * Drawables which should be tinted using a state list containing values of
86     * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
87     */
88    private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
89            R.drawable.abc_edit_text_material,
90            R.drawable.abc_tab_indicator_material,
91            R.drawable.abc_textfield_search_material,
92            R.drawable.abc_spinner_mtrl_am_alpha
93    };
94
95    /**
96     * Drawables which contain other drawables which should be tinted. The child drawable IDs
97     * should be defined in one of the arrays above.
98     */
99    private static final int[] CONTAINERS_WITH_TINT_CHILDREN = {
100            R.drawable.abc_cab_background_top_material
101    };
102
103    private final Context mContext;
104    private final Resources mResources;
105    private final TypedValue mTypedValue;
106
107    private ColorStateList mDefaultColorStateList;
108
109    /**
110     * A helper method to instantiate a {@link TintManager} and then call {@link #getDrawable(int)}.
111     * This method should not be used routinely.
112     */
113    public static Drawable getDrawable(Context context, int resId) {
114        if (isInTintList(resId)) {
115            return new TintManager(context).getDrawable(resId);
116        } else {
117            return ContextCompat.getDrawable(context, resId);
118        }
119    }
120
121    public TintManager(Context context) {
122        mContext = context;
123        mResources = new TintResources(context.getResources(), this);
124        mTypedValue = new TypedValue();
125    }
126
127    public Drawable getDrawable(int resId) {
128        Drawable drawable = ContextCompat.getDrawable(mContext, resId);
129
130        if (drawable != null) {
131            if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
132                drawable = new TintDrawableWrapper(drawable, getDefaultColorStateList());
133            } else if (arrayContains(CONTAINERS_WITH_TINT_CHILDREN, resId)) {
134                drawable = mResources.getDrawable(resId);
135            } else {
136                tintDrawable(resId, drawable);
137            }
138        }
139        return drawable;
140    }
141
142    void tintDrawable(final int resId, final Drawable drawable) {
143        PorterDuff.Mode tintMode = null;
144        boolean colorAttrSet = false;
145        int colorAttr = 0;
146        int alpha = -1;
147
148        if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
149            colorAttr = R.attr.colorControlNormal;
150            colorAttrSet = true;
151        } else if (arrayContains(TINT_COLOR_CONTROL_ACTIVATED, resId)) {
152            colorAttr = R.attr.colorControlActivated;
153            colorAttrSet = true;
154        } else if (arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, resId)) {
155            colorAttr = android.R.attr.colorBackground;
156            colorAttrSet = true;
157            tintMode = PorterDuff.Mode.MULTIPLY;
158        } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
159            colorAttr = android.R.attr.colorForeground;
160            colorAttrSet = true;
161            alpha = Math.round(0.16f * 255);
162        }
163
164        if (colorAttrSet) {
165            if (tintMode == null) {
166                tintMode = DEFAULT_MODE;
167            }
168            final int color = getThemeAttrColor(colorAttr);
169
170            // First, lets see if the cache already contains the color filter
171            PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, tintMode);
172
173            if (filter == null) {
174                // Cache miss, so create a color filter and add it to the cache
175                filter = new PorterDuffColorFilter(color, tintMode);
176                COLOR_FILTER_CACHE.put(color, tintMode, filter);
177            }
178
179            // Finally set the color filter
180            drawable.setColorFilter(filter);
181
182            if (alpha != -1) {
183                drawable.setAlpha(alpha);
184            }
185
186            if (DEBUG) {
187                Log.d(TAG, "Tinted Drawable ID: " + mResources.getResourceName(resId) +
188                        " with color: #" + Integer.toHexString(color));
189            }
190        }
191    }
192
193    private static boolean arrayContains(int[] array, int value) {
194        for (int id : array) {
195            if (id == value) {
196                return true;
197            }
198        }
199        return false;
200    }
201
202    private static boolean isInTintList(int drawableId) {
203        return arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, drawableId) ||
204                arrayContains(TINT_COLOR_CONTROL_NORMAL, drawableId) ||
205                arrayContains(TINT_COLOR_CONTROL_ACTIVATED, drawableId) ||
206                arrayContains(TINT_COLOR_CONTROL_STATE_LIST, drawableId) ||
207                arrayContains(CONTAINERS_WITH_TINT_CHILDREN, drawableId);
208    }
209
210    private ColorStateList getDefaultColorStateList() {
211        if (mDefaultColorStateList == null) {
212            /**
213             * Generate the default color state list which uses the colorControl attributes.
214             * Order is important here. The default enabled state needs to go at the bottom.
215             */
216
217            final int colorControlNormal = getThemeAttrColor(R.attr.colorControlNormal);
218            final int colorControlActivated = getThemeAttrColor(R.attr.colorControlActivated);
219
220            final int[][] states = new int[7][];
221            final int[] colors = new int[7];
222            int i = 0;
223
224            // Disabled state
225            states[i] = new int[] { -android.R.attr.state_enabled };
226            colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal);
227            i++;
228
229            states[i] = new int[] { android.R.attr.state_focused };
230            colors[i] = colorControlActivated;
231            i++;
232
233            states[i] = new int[] { android.R.attr.state_activated };
234            colors[i] = colorControlActivated;
235            i++;
236
237            states[i] = new int[] { android.R.attr.state_pressed };
238            colors[i] = colorControlActivated;
239            i++;
240
241            states[i] = new int[] { android.R.attr.state_checked };
242            colors[i] = colorControlActivated;
243            i++;
244
245            states[i] = new int[] { android.R.attr.state_selected };
246            colors[i] = colorControlActivated;
247            i++;
248
249            // Default enabled state
250            states[i] = new int[0];
251            colors[i] = colorControlNormal;
252            i++;
253
254            mDefaultColorStateList = new ColorStateList(states, colors);
255        }
256        return mDefaultColorStateList;
257    }
258
259    int getThemeAttrColor(int attr) {
260        if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) {
261            if (mTypedValue.type >= TypedValue.TYPE_FIRST_INT
262                    && mTypedValue.type <= TypedValue.TYPE_LAST_INT) {
263                return mTypedValue.data;
264            } else if (mTypedValue.type == TypedValue.TYPE_STRING) {
265                return mResources.getColor(mTypedValue.resourceId);
266            }
267        }
268        return 0;
269    }
270
271    int getDisabledThemeAttrColor(int attr) {
272        final int color = getThemeAttrColor(attr);
273        final int originalAlpha = Color.alpha(color);
274
275        // Now retrieve the disabledAlpha value from the theme
276        mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, mTypedValue, true);
277        final float disabledAlpha = mTypedValue.getFloat();
278
279        // Return the color, multiplying the original alpha by the disabled value
280        return (color & 0x00ffffff) | (Math.round(originalAlpha * disabledAlpha) << 24);
281    }
282
283    private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
284
285        public ColorFilterLruCache(int maxSize) {
286            super(maxSize);
287        }
288
289        PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
290            return get(generateCacheKey(color, mode));
291        }
292
293        PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
294            return put(generateCacheKey(color, mode), filter);
295        }
296
297        private static int generateCacheKey(int color, PorterDuff.Mode mode) {
298            int hashCode = 1;
299            hashCode = 31 * hashCode + color;
300            hashCode = 31 * hashCode + mode.hashCode();
301            return hashCode;
302        }
303    }
304}
305