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
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     */
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
130    public TintManager(Context context) {
131        mContext = context;
132        mResources = new TintResources(context.getResources(), this);
133        mTypedValue = new TypedValue();
134    }
135
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
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
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
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
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
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
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
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
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
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
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
386        public ColorFilterLruCache(int maxSize) {
387            super(maxSize);
388        }
389
390        PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
391            return get(generateCacheKey(color, mode));
392        }
393
394        PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
395            return put(generateCacheKey(color, mode), filter);
396        }
397
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