1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v7.internal.widget;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.Resources;
22 import android.graphics.Color;
23 import android.graphics.PorterDuff;
24 import android.graphics.PorterDuffColorFilter;
25 import android.graphics.drawable.Drawable;
26 import android.support.v4.content.ContextCompat;
27 import android.support.v4.util.LruCache;
28 import android.support.v7.appcompat.R;
29 import android.util.Log;
30 import android.util.TypedValue;
31 
32 /**
33  * @hide
34  */
35 public class TintManager {
36 
37     private static final String TAG = TintManager.class.getSimpleName();
38     private static final boolean DEBUG = false;
39 
40     static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
41 
42     private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
43 
44     /**
45      * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
46      * using the default mode.
47      */
48     private static final int[] TINT_COLOR_CONTROL_NORMAL = {
49             R.drawable.abc_ic_ab_back_mtrl_am_alpha,
50             R.drawable.abc_ic_go_search_api_mtrl_alpha,
51             R.drawable.abc_ic_search_api_mtrl_alpha,
52             R.drawable.abc_ic_commit_search_api_mtrl_alpha,
53             R.drawable.abc_ic_clear_mtrl_alpha,
54             R.drawable.abc_ic_menu_share_mtrl_alpha,
55             R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
56             R.drawable.abc_ic_menu_cut_mtrl_alpha,
57             R.drawable.abc_ic_menu_selectall_mtrl_alpha,
58             R.drawable.abc_ic_menu_paste_mtrl_am_alpha,
59             R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha,
60             R.drawable.abc_ic_voice_search_api_mtrl_alpha,
61             R.drawable.abc_textfield_search_default_mtrl_alpha,
62             R.drawable.abc_textfield_default_mtrl_alpha,
63             R.drawable.abc_ab_share_pack_mtrl_alpha
64     };
65 
66     /**
67      * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
68      * using the default mode.
69      */
70     private static final int[] TINT_COLOR_CONTROL_ACTIVATED = {
71             R.drawable.abc_textfield_activated_mtrl_alpha,
72             R.drawable.abc_textfield_search_activated_mtrl_alpha,
73             R.drawable.abc_cab_background_top_mtrl_alpha
74     };
75 
76     /**
77      * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
78      * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode.
79      */
80     private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = {
81             R.drawable.abc_popup_background_mtrl_mult,
82             R.drawable.abc_cab_background_internal_bg,
83             R.drawable.abc_menu_hardkey_panel_mtrl_mult
84     };
85 
86     /**
87      * Drawables which should be tinted using a state list containing values of
88      * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
89      */
90     private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
91             R.drawable.abc_edit_text_material,
92             R.drawable.abc_tab_indicator_material,
93             R.drawable.abc_textfield_search_material,
94             R.drawable.abc_spinner_mtrl_am_alpha,
95             R.drawable.abc_btn_check_material,
96             R.drawable.abc_btn_radio_material,
97             R.drawable.abc_spinner_textfield_background_material,
98             R.drawable.abc_ratingbar_full_material
99     };
100 
101     /**
102      * Drawables which contain other drawables which should be tinted. The child drawable IDs
103      * should be defined in one of the arrays above.
104      */
105     private static final int[] CONTAINERS_WITH_TINT_CHILDREN = {
106             R.drawable.abc_cab_background_top_material
107     };
108 
109     private final Context mContext;
110     private final Resources mResources;
111     private final TypedValue mTypedValue;
112 
113     private ColorStateList mDefaultColorStateList;
114     private ColorStateList mSwitchThumbStateList;
115     private ColorStateList mSwitchTrackStateList;
116     private ColorStateList mButtonStateList;
117 
118     /**
119      * A helper method to instantiate a {@link TintManager} and then call {@link #getDrawable(int)}.
120      * This method should not be used routinely.
121      */
getDrawable(Context context, int resId)122     public static Drawable getDrawable(Context context, int resId) {
123         if (isInTintList(resId)) {
124             return new TintManager(context).getDrawable(resId);
125         } else {
126             return ContextCompat.getDrawable(context, resId);
127         }
128     }
129 
TintManager(Context context)130     public TintManager(Context context) {
131         mContext = context;
132         mResources = new TintResources(context.getResources(), this);
133         mTypedValue = new TypedValue();
134     }
135 
getDrawable(int resId)136     public Drawable getDrawable(int resId) {
137         Drawable drawable = ContextCompat.getDrawable(mContext, resId);
138 
139         if (drawable != null) {
140             drawable = drawable.mutate();
141 
142             if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
143                 drawable = new TintDrawableWrapper(drawable, getDefaultColorStateList());
144             } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
145                 drawable = new TintDrawableWrapper(drawable, getSwitchTrackColorStateList());
146             } else if (resId == R.drawable.abc_switch_thumb_material) {
147                 drawable = new TintDrawableWrapper(drawable, getSwitchThumbColorStateList(),
148                         PorterDuff.Mode.MULTIPLY);
149             } else if (resId == R.drawable.abc_btn_default_mtrl_shape) {
150                 drawable = new TintDrawableWrapper(drawable, getButtonColorStateList());
151             } else if (arrayContains(CONTAINERS_WITH_TINT_CHILDREN, resId)) {
152                 drawable = mResources.getDrawable(resId);
153             } else {
154                 tintDrawable(resId, drawable);
155             }
156         }
157         return drawable;
158     }
159 
tintDrawable(final int resId, final Drawable drawable)160     void tintDrawable(final int resId, final Drawable drawable) {
161         PorterDuff.Mode tintMode = null;
162         boolean colorAttrSet = false;
163         int colorAttr = 0;
164         int alpha = -1;
165 
166         if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
167             colorAttr = R.attr.colorControlNormal;
168             colorAttrSet = true;
169         } else if (arrayContains(TINT_COLOR_CONTROL_ACTIVATED, resId)) {
170             colorAttr = R.attr.colorControlActivated;
171             colorAttrSet = true;
172         } else if (arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, resId)) {
173             colorAttr = android.R.attr.colorBackground;
174             colorAttrSet = true;
175             tintMode = PorterDuff.Mode.MULTIPLY;
176         } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
177             colorAttr = android.R.attr.colorForeground;
178             colorAttrSet = true;
179             alpha = Math.round(0.16f * 255);
180         }
181 
182         if (colorAttrSet) {
183             if (tintMode == null) {
184                 tintMode = DEFAULT_MODE;
185             }
186             final int color = getThemeAttrColor(colorAttr);
187 
188             // First, lets see if the cache already contains the color filter
189             PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, tintMode);
190 
191             if (filter == null) {
192                 // Cache miss, so create a color filter and add it to the cache
193                 filter = new PorterDuffColorFilter(color, tintMode);
194                 COLOR_FILTER_CACHE.put(color, tintMode, filter);
195             }
196 
197             // Finally set the color filter
198             drawable.setColorFilter(filter);
199 
200             if (alpha != -1) {
201                 drawable.setAlpha(alpha);
202             }
203 
204             if (DEBUG) {
205                 Log.d(TAG, "Tinted Drawable ID: " + mResources.getResourceName(resId) +
206                         " with color: #" + Integer.toHexString(color));
207             }
208         }
209     }
210 
arrayContains(int[] array, int value)211     private static boolean arrayContains(int[] array, int value) {
212         for (int id : array) {
213             if (id == value) {
214                 return true;
215             }
216         }
217         return false;
218     }
219 
isInTintList(int drawableId)220     private static boolean isInTintList(int drawableId) {
221         return arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, drawableId) ||
222                 arrayContains(TINT_COLOR_CONTROL_NORMAL, drawableId) ||
223                 arrayContains(TINT_COLOR_CONTROL_ACTIVATED, drawableId) ||
224                 arrayContains(TINT_COLOR_CONTROL_STATE_LIST, drawableId) ||
225                 arrayContains(CONTAINERS_WITH_TINT_CHILDREN, drawableId);
226     }
227 
getDefaultColorStateList()228     private ColorStateList getDefaultColorStateList() {
229         if (mDefaultColorStateList == null) {
230             /**
231              * Generate the default color state list which uses the colorControl attributes.
232              * Order is important here. The default enabled state needs to go at the bottom.
233              */
234 
235             final int colorControlNormal = getThemeAttrColor(R.attr.colorControlNormal);
236             final int colorControlActivated = getThemeAttrColor(R.attr.colorControlActivated);
237 
238             final int[][] states = new int[7][];
239             final int[] colors = new int[7];
240             int i = 0;
241 
242             // Disabled state
243             states[i] = new int[] { -android.R.attr.state_enabled };
244             colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal);
245             i++;
246 
247             states[i] = new int[] { android.R.attr.state_focused };
248             colors[i] = colorControlActivated;
249             i++;
250 
251             states[i] = new int[] { android.R.attr.state_activated };
252             colors[i] = colorControlActivated;
253             i++;
254 
255             states[i] = new int[] { android.R.attr.state_pressed };
256             colors[i] = colorControlActivated;
257             i++;
258 
259             states[i] = new int[] { android.R.attr.state_checked };
260             colors[i] = colorControlActivated;
261             i++;
262 
263             states[i] = new int[] { android.R.attr.state_selected };
264             colors[i] = colorControlActivated;
265             i++;
266 
267             // Default enabled state
268             states[i] = new int[0];
269             colors[i] = colorControlNormal;
270             i++;
271 
272             mDefaultColorStateList = new ColorStateList(states, colors);
273         }
274         return mDefaultColorStateList;
275     }
276 
getSwitchTrackColorStateList()277     private ColorStateList getSwitchTrackColorStateList() {
278         if (mSwitchTrackStateList == null) {
279             final int[][] states = new int[3][];
280             final int[] colors = new int[3];
281             int i = 0;
282 
283             // Disabled state
284             states[i] = new int[] { -android.R.attr.state_enabled };
285             colors[i] = getThemeAttrColor(android.R.attr.colorForeground, 0.1f);
286             i++;
287 
288             states[i] = new int[] { android.R.attr.state_checked };
289             colors[i] = getThemeAttrColor(R.attr.colorControlActivated, 0.3f);
290             i++;
291 
292             // Default enabled state
293             states[i] = new int[0];
294             colors[i] = getThemeAttrColor(android.R.attr.colorForeground, 0.3f);
295             i++;
296 
297             mSwitchTrackStateList = new ColorStateList(states, colors);
298         }
299         return mSwitchTrackStateList;
300     }
301 
getSwitchThumbColorStateList()302     private ColorStateList getSwitchThumbColorStateList() {
303         if (mSwitchThumbStateList == null) {
304             final int[][] states = new int[3][];
305             final int[] colors = new int[3];
306             int i = 0;
307 
308             // Disabled state
309             states[i] = new int[] { -android.R.attr.state_enabled };
310             colors[i] = getDisabledThemeAttrColor(R.attr.colorSwitchThumbNormal);
311             i++;
312 
313             states[i] = new int[] { android.R.attr.state_checked };
314             colors[i] = getThemeAttrColor(R.attr.colorControlActivated);
315             i++;
316 
317             // Default enabled state
318             states[i] = new int[0];
319             colors[i] = getThemeAttrColor(R.attr.colorSwitchThumbNormal);
320             i++;
321 
322             mSwitchThumbStateList = new ColorStateList(states, colors);
323         }
324         return mSwitchThumbStateList;
325     }
326 
getButtonColorStateList()327     private ColorStateList getButtonColorStateList() {
328         if (mButtonStateList == null) {
329             final int[][] states = new int[4][];
330             final int[] colors = new int[4];
331             int i = 0;
332 
333             // Disabled state
334             states[i] = new int[] { -android.R.attr.state_enabled };
335             colors[i] = getDisabledThemeAttrColor(R.attr.colorButtonNormal);
336             i++;
337 
338             states[i] = new int[] { android.R.attr.state_pressed };
339             colors[i] = getThemeAttrColor(R.attr.colorControlHighlight);
340             i++;
341 
342             states[i] = new int[] { android.R.attr.state_focused };
343             colors[i] = getThemeAttrColor(R.attr.colorControlHighlight);
344             i++;
345 
346             // Default enabled state
347             states[i] = new int[0];
348             colors[i] = getThemeAttrColor(R.attr.colorButtonNormal);
349             i++;
350 
351             mButtonStateList = new ColorStateList(states, colors);
352         }
353         return mButtonStateList;
354     }
355 
getThemeAttrColor(int attr)356     int getThemeAttrColor(int attr) {
357         if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) {
358             if (mTypedValue.type >= TypedValue.TYPE_FIRST_INT
359                     && mTypedValue.type <= TypedValue.TYPE_LAST_INT) {
360                 return mTypedValue.data;
361             } else if (mTypedValue.type == TypedValue.TYPE_STRING) {
362                 return mResources.getColor(mTypedValue.resourceId);
363             }
364         }
365         return 0;
366     }
367 
getThemeAttrColor(int attr, float alpha)368     int getThemeAttrColor(int attr, float alpha) {
369         final int color = getThemeAttrColor(attr);
370         final int originalAlpha = Color.alpha(color);
371 
372         // Return the color, multiplying the original alpha by the disabled value
373         return (color & 0x00ffffff) | (Math.round(originalAlpha * alpha) << 24);
374     }
375 
getDisabledThemeAttrColor(int attr)376     int getDisabledThemeAttrColor(int attr) {
377         // Now retrieve the disabledAlpha value from the theme
378         mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, mTypedValue, true);
379         final float disabledAlpha = mTypedValue.getFloat();
380 
381         return getThemeAttrColor(attr, disabledAlpha);
382     }
383 
384     private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
385 
ColorFilterLruCache(int maxSize)386         public ColorFilterLruCache(int maxSize) {
387             super(maxSize);
388         }
389 
get(int color, PorterDuff.Mode mode)390         PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
391             return get(generateCacheKey(color, mode));
392         }
393 
put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter)394         PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
395             return put(generateCacheKey(color, mode), filter);
396         }
397 
generateCacheKey(int color, PorterDuff.Mode mode)398         private static int generateCacheKey(int color, PorterDuff.Mode mode) {
399             int hashCode = 1;
400             hashCode = 31 * hashCode + color;
401             hashCode = 31 * hashCode + mode.hashCode();
402             return hashCode;
403         }
404     }
405 }
406