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.graphics.Color;
22 import android.graphics.PorterDuff;
23 import android.graphics.PorterDuffColorFilter;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.LayerDrawable;
26 import android.os.Build;
27 import android.support.v4.content.ContextCompat;
28 import android.support.v4.graphics.ColorUtils;
29 import android.support.v4.graphics.drawable.DrawableCompat;
30 import android.support.v4.util.LruCache;
31 import android.support.v7.appcompat.R;
32 import android.util.Log;
33 import android.util.SparseArray;
34 import android.view.View;
35 
36 import java.lang.ref.WeakReference;
37 import java.util.WeakHashMap;
38 
39 import static android.support.v7.internal.widget.ThemeUtils.getDisabledThemeAttrColor;
40 import static android.support.v7.internal.widget.ThemeUtils.getThemeAttrColor;
41 import static android.support.v7.internal.widget.ThemeUtils.getThemeAttrColorStateList;
42 
43 /**
44  * @hide
45  */
46 public final class TintManager {
47 
48     public static final boolean SHOULD_BE_USED = Build.VERSION.SDK_INT < 21;
49 
50     private static final String TAG = "TintManager";
51     private static final boolean DEBUG = false;
52     private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
53 
54     private static final WeakHashMap<Context, TintManager> INSTANCE_CACHE = new WeakHashMap<>();
55     private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
56 
57     /**
58      * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
59      * using the default mode using a raw color filter.
60      */
61     private static final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = {
62             R.drawable.abc_textfield_search_default_mtrl_alpha,
63             R.drawable.abc_textfield_default_mtrl_alpha,
64             R.drawable.abc_ab_share_pack_mtrl_alpha
65     };
66 
67     /**
68      * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, using
69      * {@link DrawableCompat}'s tinting functionality.
70      */
71     private static final int[] TINT_COLOR_CONTROL_NORMAL = {
72             R.drawable.abc_ic_ab_back_mtrl_am_alpha,
73             R.drawable.abc_ic_go_search_api_mtrl_alpha,
74             R.drawable.abc_ic_search_api_mtrl_alpha,
75             R.drawable.abc_ic_commit_search_api_mtrl_alpha,
76             R.drawable.abc_ic_clear_mtrl_alpha,
77             R.drawable.abc_ic_menu_share_mtrl_alpha,
78             R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
79             R.drawable.abc_ic_menu_cut_mtrl_alpha,
80             R.drawable.abc_ic_menu_selectall_mtrl_alpha,
81             R.drawable.abc_ic_menu_paste_mtrl_am_alpha,
82             R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha,
83             R.drawable.abc_ic_voice_search_api_mtrl_alpha
84     };
85 
86     /**
87      * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
88      * using a color filter.
89      */
90     private static final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = {
91             R.drawable.abc_textfield_activated_mtrl_alpha,
92             R.drawable.abc_textfield_search_activated_mtrl_alpha,
93             R.drawable.abc_cab_background_top_mtrl_alpha,
94             R.drawable.abc_text_cursor_material
95     };
96 
97     /**
98      * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
99      * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter.
100      */
101     private static final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = {
102             R.drawable.abc_popup_background_mtrl_mult,
103             R.drawable.abc_cab_background_internal_bg,
104             R.drawable.abc_menu_hardkey_panel_mtrl_mult
105     };
106 
107     /**
108      * Drawables which should be tinted using a state list containing values of
109      * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
110      */
111     private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
112             R.drawable.abc_edit_text_material,
113             R.drawable.abc_tab_indicator_material,
114             R.drawable.abc_textfield_search_material,
115             R.drawable.abc_spinner_mtrl_am_alpha,
116             R.drawable.abc_spinner_textfield_background_material,
117             R.drawable.abc_ratingbar_full_material,
118             R.drawable.abc_switch_track_mtrl_alpha,
119             R.drawable.abc_switch_thumb_material,
120             R.drawable.abc_btn_default_mtrl_shape,
121             R.drawable.abc_btn_borderless_material
122     };
123 
124     /**
125      * Drawables which should be tinted using a state list containing values of
126      * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} for the checked
127      * state.
128      */
129     private static final int[] TINT_CHECKABLE_BUTTON_LIST = {
130             R.drawable.abc_btn_check_material,
131             R.drawable.abc_btn_radio_material
132     };
133 
134     private final WeakReference<Context> mContextRef;
135     private SparseArray<ColorStateList> mTintLists;
136     private ColorStateList mDefaultColorStateList;
137 
138     /**
139      * A helper method to get a {@link TintManager} and then call {@link #getDrawable(int)}.
140      * This method should not be used routinely.
141      */
142     public static Drawable getDrawable(Context context, int resId) {
143         if (isInTintList(resId)) {
144             return TintManager.get(context).getDrawable(resId);
145         } else {
146             return ContextCompat.getDrawable(context, resId);
147         }
148     }
149 
150     /**
151      * Get a {@link android.support.v7.internal.widget.TintManager} instance.
152      */
153     public static TintManager get(Context context) {
154         TintManager tm = INSTANCE_CACHE.get(context);
155         if (tm == null) {
156             tm = new TintManager(context);
157             INSTANCE_CACHE.put(context, tm);
158         }
159         return tm;
160     }
161 
162     private TintManager(Context context) {
163         mContextRef = new WeakReference<>(context);
164     }
165 
166     public Drawable getDrawable(int resId) {
167         return getDrawable(resId, false);
168     }
169 
170     public Drawable getDrawable(int resId, boolean failIfNotKnown) {
171         final Context context = mContextRef.get();
172         if (context == null) return null;
173 
174         Drawable drawable = ContextCompat.getDrawable(context, resId);
175 
176         if (drawable != null) {
177             if (Build.VERSION.SDK_INT >= 8) {
178                 // Mutate can cause NPEs on 2.1
179                 drawable = drawable.mutate();
180             }
181 
182             final ColorStateList tintList = getTintList(resId);
183             if (tintList != null) {
184                 // First wrap the Drawable and set the tint list
185                 drawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTintList(drawable, tintList)186                 DrawableCompat.setTintList(drawable, tintList);
187 
188                 // If there is a blending mode specified for the drawable, use it
189                 final PorterDuff.Mode tintMode = getTintMode(resId);
190                 if (tintMode != null) {
DrawableCompat.setTintMode(drawable, tintMode)191                     DrawableCompat.setTintMode(drawable, tintMode);
192                 }
193             } else if (resId == R.drawable.abc_cab_background_top_material) {
194                 return new LayerDrawable(new Drawable[] {
195                         getDrawable(R.drawable.abc_cab_background_internal_bg),
196                         getDrawable(R.drawable.abc_cab_background_top_mtrl_alpha)
197                 });
198             } else {
199                 final boolean usedColorFilter = tintDrawableUsingColorFilter(resId, drawable);
200                 if (!usedColorFilter && failIfNotKnown) {
201                     // If we didn't tint using a ColorFilter, and we're set to fail if we don't
202                     // know the id, return null
203                     drawable = null;
204                 }
205             }
206         }
207         return drawable;
208     }
209 
210     public final boolean tintDrawableUsingColorFilter(final int resId, Drawable drawable) {
211         final Context context = mContextRef.get();
212         if (context == null) return false;
213 
214         PorterDuff.Mode tintMode = DEFAULT_MODE;
215         boolean colorAttrSet = false;
216         int colorAttr = 0;
217         int alpha = -1;
218 
219         if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) {
220             colorAttr = R.attr.colorControlNormal;
221             colorAttrSet = true;
222         } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) {
223             colorAttr = R.attr.colorControlActivated;
224             colorAttrSet = true;
225         } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) {
226             colorAttr = android.R.attr.colorBackground;
227             colorAttrSet = true;
228             tintMode = PorterDuff.Mode.MULTIPLY;
229         } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) {
230             colorAttr = android.R.attr.colorForeground;
231             colorAttrSet = true;
232             alpha = Math.round(0.16f * 255);
233         }
234 
235         if (colorAttrSet) {
236             final int color = getThemeAttrColor(context, colorAttr);
237             drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode));
238 
239             if (alpha != -1) {
240                 drawable.setAlpha(alpha);
241             }
242 
243             if (DEBUG) {
244                 Log.d(TAG, "Tinted Drawable: " + context.getResources().getResourceName(resId) +
245                         " with color: #" + Integer.toHexString(color));
246             }
247             return true;
248         }
249         return false;
250     }
251 
252     private static boolean arrayContains(int[] array, int value) {
253         for (int id : array) {
254             if (id == value) {
255                 return true;
256             }
257         }
258         return false;
259     }
260 
261     private static boolean isInTintList(int drawableId) {
262         return arrayContains(TINT_COLOR_CONTROL_NORMAL, drawableId) ||
263                 arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, drawableId) ||
264                 arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, drawableId) ||
265                 arrayContains(TINT_COLOR_CONTROL_STATE_LIST, drawableId) ||
266                 arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, drawableId) ||
267                 arrayContains(TINT_CHECKABLE_BUTTON_LIST, drawableId) ||
268                 drawableId == R.drawable.abc_cab_background_top_material;
269     }
270 
271     final PorterDuff.Mode getTintMode(final int resId) {
272         PorterDuff.Mode mode = null;
273 
274         if (resId == R.drawable.abc_switch_thumb_material) {
275             mode = PorterDuff.Mode.MULTIPLY;
276         }
277 
278         return mode;
279     }
280 
281     public final ColorStateList getTintList(int resId) {
282         final Context context = mContextRef.get();
283         if (context == null) return null;
284 
285         // Try the cache first (if it exists)
286         ColorStateList tint = mTintLists != null ? mTintLists.get(resId) : null;
287 
288         if (tint == null) {
289             // ...if the cache did not contain a color state list, try and create one
290             if (resId == R.drawable.abc_edit_text_material) {
291                 tint = createEditTextColorStateList(context);
292             } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) {
293                 tint = createSwitchTrackColorStateList(context);
294             } else if (resId == R.drawable.abc_switch_thumb_material) {
295                 tint = createSwitchThumbColorStateList(context);
296             } else if (resId == R.drawable.abc_btn_default_mtrl_shape
297                     || resId == R.drawable.abc_btn_borderless_material) {
298                 tint = createDefaultButtonColorStateList(context);
299             } else if (resId == R.drawable.abc_btn_colored_material) {
300                 tint = createColoredButtonColorStateList(context);
301             } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha
302                     || resId == R.drawable.abc_spinner_textfield_background_material) {
303                 tint = createSpinnerColorStateList(context);
304             } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) {
305                 tint = getThemeAttrColorStateList(context, R.attr.colorControlNormal);
306             } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) {
307                 tint = getDefaultColorStateList(context);
308             } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) {
309                 tint = createCheckableButtonColorStateList(context);
310             }
311 
312             if (tint != null) {
313                 if (mTintLists == null) {
314                     // If our tint list cache hasn't been set up yet, create it
315                     mTintLists = new SparseArray<>();
316                 }
317                 // Add any newly created ColorStateList to the cache
318                 mTintLists.append(resId, tint);
319             }
320         }
321         return tint;
322     }
323 
324     private ColorStateList getDefaultColorStateList(Context context) {
325         if (mDefaultColorStateList == null) {
326             /**
327              * Generate the default color state list which uses the colorControl attributes.
328              * Order is important here. The default enabled state needs to go at the bottom.
329              */
330 
331             final int colorControlNormal = getThemeAttrColor(context, R.attr.colorControlNormal);
332             final int colorControlActivated = getThemeAttrColor(context,
333                     R.attr.colorControlActivated);
334 
335             final int[][] states = new int[7][];
336             final int[] colors = new int[7];
337             int i = 0;
338 
339             // Disabled state
340             states[i] = ThemeUtils.DISABLED_STATE_SET;
341             colors[i] = getDisabledThemeAttrColor(context, R.attr.colorControlNormal);
342             i++;
343 
344             states[i] = ThemeUtils.FOCUSED_STATE_SET;
345             colors[i] = colorControlActivated;
346             i++;
347 
348             states[i] = ThemeUtils.ACTIVATED_STATE_SET;
349             colors[i] = colorControlActivated;
350             i++;
351 
352             states[i] = ThemeUtils.PRESSED_STATE_SET;
353             colors[i] = colorControlActivated;
354             i++;
355 
356             states[i] = ThemeUtils.CHECKED_STATE_SET;
357             colors[i] = colorControlActivated;
358             i++;
359 
360             states[i] = ThemeUtils.SELECTED_STATE_SET;
361             colors[i] = colorControlActivated;
362             i++;
363 
364             // Default enabled state
365             states[i] = ThemeUtils.EMPTY_STATE_SET;
366             colors[i] = colorControlNormal;
367             i++;
368 
369             mDefaultColorStateList = new ColorStateList(states, colors);
370         }
371         return mDefaultColorStateList;
372     }
373 
374     private ColorStateList createCheckableButtonColorStateList(Context context) {
375         final int[][] states = new int[3][];
376         final int[] colors = new int[3];
377         int i = 0;
378 
379         // Disabled state
380         states[i] = ThemeUtils.DISABLED_STATE_SET;
381         colors[i] = getDisabledThemeAttrColor(context, R.attr.colorControlNormal);
382         i++;
383 
384         states[i] = ThemeUtils.CHECKED_STATE_SET;
385         colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
386         i++;
387 
388         // Default enabled state
389         states[i] = ThemeUtils.EMPTY_STATE_SET;
390         colors[i] = getThemeAttrColor(context, R.attr.colorControlNormal);
391         i++;
392 
393         return new ColorStateList(states, colors);
394     }
395 
396     private ColorStateList createSwitchTrackColorStateList(Context context) {
397         final int[][] states = new int[3][];
398         final int[] colors = new int[3];
399         int i = 0;
400 
401         // Disabled state
402         states[i] = ThemeUtils.DISABLED_STATE_SET;
403         colors[i] = getThemeAttrColor(context, android.R.attr.colorForeground, 0.1f);
404         i++;
405 
406         states[i] = ThemeUtils.CHECKED_STATE_SET;
407         colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated, 0.3f);
408         i++;
409 
410         // Default enabled state
411         states[i] = ThemeUtils.EMPTY_STATE_SET;
412         colors[i] = getThemeAttrColor(context, android.R.attr.colorForeground, 0.3f);
413         i++;
414 
415         return new ColorStateList(states, colors);
416     }
417 
418     private ColorStateList createSwitchThumbColorStateList(Context context) {
419         final int[][] states = new int[3][];
420         final int[] colors = new int[3];
421         int i = 0;
422 
423         final ColorStateList thumbColor = getThemeAttrColorStateList(context,
424                 R.attr.colorSwitchThumbNormal);
425 
426         if (thumbColor != null && thumbColor.isStateful()) {
427             // If colorSwitchThumbNormal is a valid ColorStateList, extract the default and
428             // disabled colors from it
429 
430             // Disabled state
431             states[i] = ThemeUtils.DISABLED_STATE_SET;
432             colors[i] = thumbColor.getColorForState(states[i], 0);
433             i++;
434 
435             states[i] = ThemeUtils.CHECKED_STATE_SET;
436             colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
437             i++;
438 
439             // Default enabled state
440             states[i] = ThemeUtils.EMPTY_STATE_SET;
441             colors[i] = thumbColor.getDefaultColor();
442             i++;
443         } else {
444             // Else we'll use an approximation using the default disabled alpha
445 
446             // Disabled state
447             states[i] = ThemeUtils.DISABLED_STATE_SET;
448             colors[i] = getDisabledThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
449             i++;
450 
451             states[i] = ThemeUtils.CHECKED_STATE_SET;
452             colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
453             i++;
454 
455             // Default enabled state
456             states[i] = ThemeUtils.EMPTY_STATE_SET;
457             colors[i] = getThemeAttrColor(context, R.attr.colorSwitchThumbNormal);
458             i++;
459         }
460 
461         return new ColorStateList(states, colors);
462     }
463 
464     private ColorStateList createEditTextColorStateList(Context context) {
465         final int[][] states = new int[3][];
466         final int[] colors = new int[3];
467         int i = 0;
468 
469         // Disabled state
470         states[i] = ThemeUtils.DISABLED_STATE_SET;
471         colors[i] = getDisabledThemeAttrColor(context, R.attr.colorControlNormal);
472         i++;
473 
474         states[i] = ThemeUtils.NOT_PRESSED_OR_FOCUSED_STATE_SET;
475         colors[i] = getThemeAttrColor(context, R.attr.colorControlNormal);
476         i++;
477 
478         // Default enabled state
479         states[i] = ThemeUtils.EMPTY_STATE_SET;
480         colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
481         i++;
482 
483         return new ColorStateList(states, colors);
484     }
485 
486     private ColorStateList createDefaultButtonColorStateList(Context context) {
487         return createButtonColorStateList(context, R.attr.colorButtonNormal);
488     }
489 
490     private ColorStateList createColoredButtonColorStateList(Context context) {
491         return createButtonColorStateList(context, R.attr.colorAccent);
492     }
493 
494     private ColorStateList createButtonColorStateList(Context context, int baseColorAttr) {
495         final int[][] states = new int[4][];
496         final int[] colors = new int[4];
497         int i = 0;
498 
499         final int baseColor = getThemeAttrColor(context, baseColorAttr);
500         final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight);
501 
502         // Disabled state
503         states[i] = ThemeUtils.DISABLED_STATE_SET;
504         colors[i] = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal);
505         i++;
506 
507         states[i] = ThemeUtils.PRESSED_STATE_SET;
508         colors[i] = ColorUtils.compositeColors(colorControlHighlight, baseColor);
509         i++;
510 
511         states[i] = ThemeUtils.FOCUSED_STATE_SET;
512         colors[i] = ColorUtils.compositeColors(colorControlHighlight, baseColor);
513         i++;
514 
515         // Default enabled state
516         states[i] = ThemeUtils.EMPTY_STATE_SET;
517         colors[i] = baseColor;
518         i++;
519 
520         return new ColorStateList(states, colors);
521     }
522 
523     private ColorStateList createSpinnerColorStateList(Context context) {
524         final int[][] states = new int[3][];
525         final int[] colors = new int[3];
526         int i = 0;
527 
528         // Disabled state
529         states[i] = ThemeUtils.DISABLED_STATE_SET;
530         colors[i] = getDisabledThemeAttrColor(context, R.attr.colorControlNormal);
531         i++;
532 
533         states[i] = ThemeUtils.NOT_PRESSED_OR_FOCUSED_STATE_SET;
534         colors[i] = getThemeAttrColor(context, R.attr.colorControlNormal);
535         i++;
536 
537         states[i] = ThemeUtils.EMPTY_STATE_SET;
538         colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated);
539         i++;
540 
541         return new ColorStateList(states, colors);
542     }
543 
544     private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
545 
546         public ColorFilterLruCache(int maxSize) {
547             super(maxSize);
548         }
549 
550         PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
551             return get(generateCacheKey(color, mode));
552         }
553 
554         PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
555             return put(generateCacheKey(color, mode), filter);
556         }
557 
558         private static int generateCacheKey(int color, PorterDuff.Mode mode) {
559             int hashCode = 1;
560             hashCode = 31 * hashCode + color;
561             hashCode = 31 * hashCode + mode.hashCode();
562             return hashCode;
563         }
564     }
565 
566     public static void tintViewBackground(View view, TintInfo tint) {
567         final Drawable background = view.getBackground();
568         if (tint.mHasTintList || tint.mHasTintMode) {
569             background.setColorFilter(createTintFilter(
570                     tint.mHasTintList ? tint.mTintList : null,
571                     tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
572                     view.getDrawableState()));
573         } else {
574             background.clearColorFilter();
575         }
576 
577         if (Build.VERSION.SDK_INT <= 10) {
578             // On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
579             // has changed, so we need to force an invalidation
580             view.invalidate();
581         }
582     }
583 
584     private static PorterDuffColorFilter createTintFilter(ColorStateList tint,
585             PorterDuff.Mode tintMode, final int[] state) {
586         if (tint == null || tintMode == null) {
587             return null;
588         }
589         final int color = tint.getColorForState(state, Color.TRANSPARENT);
590         return getPorterDuffColorFilter(color, tintMode);
591     }
592 
593     private static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
594         // First, lets see if the cache already contains the color filter
595         PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
596 
597         if (filter == null) {
598             // Cache miss, so create a color filter and add it to the cache
599             filter = new PorterDuffColorFilter(color, mode);
600             COLOR_FILTER_CACHE.put(color, mode, filter);
601         }
602 
603         return filter;
604     }
605 }
606