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