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