TintManager.java revision 7e82b99953680915596eaf0eb35927388e574ca8
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.os.Build; 27import android.support.v4.content.ContextCompat; 28import android.support.v4.graphics.drawable.DrawableCompat; 29import android.support.v4.util.LruCache; 30import android.support.v7.appcompat.R; 31import android.util.Log; 32import android.util.SparseArray; 33import android.util.TypedValue; 34import android.view.View; 35 36/** 37 * @hide 38 */ 39public final class TintManager { 40 41 static final boolean SHOULD_BE_USED = Build.VERSION.SDK_INT < 21; 42 43 private static final String TAG = TintManager.class.getSimpleName(); 44 private static final boolean DEBUG = false; 45 46 static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; 47 48 private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6); 49 50 /** 51 * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, 52 * using the default mode. 53 */ 54 private static final int[] TINT_COLOR_CONTROL_NORMAL = { 55 R.drawable.abc_ic_ab_back_mtrl_am_alpha, 56 R.drawable.abc_ic_go_search_api_mtrl_alpha, 57 R.drawable.abc_ic_search_api_mtrl_alpha, 58 R.drawable.abc_ic_commit_search_api_mtrl_alpha, 59 R.drawable.abc_ic_clear_mtrl_alpha, 60 R.drawable.abc_ic_menu_share_mtrl_alpha, 61 R.drawable.abc_ic_menu_copy_mtrl_am_alpha, 62 R.drawable.abc_ic_menu_cut_mtrl_alpha, 63 R.drawable.abc_ic_menu_selectall_mtrl_alpha, 64 R.drawable.abc_ic_menu_paste_mtrl_am_alpha, 65 R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha, 66 R.drawable.abc_ic_voice_search_api_mtrl_alpha, 67 R.drawable.abc_textfield_search_default_mtrl_alpha, 68 R.drawable.abc_textfield_default_mtrl_alpha, 69 R.drawable.abc_ab_share_pack_mtrl_alpha 70 }; 71 72 /** 73 * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated}, 74 * using the default mode. 75 */ 76 private static final int[] TINT_COLOR_CONTROL_ACTIVATED = { 77 R.drawable.abc_textfield_activated_mtrl_alpha, 78 R.drawable.abc_textfield_search_activated_mtrl_alpha, 79 R.drawable.abc_cab_background_top_mtrl_alpha, 80 R.drawable.abc_text_cursor_mtrl_alpha 81 }; 82 83 /** 84 * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground}, 85 * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode. 86 */ 87 private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = { 88 R.drawable.abc_popup_background_mtrl_mult, 89 R.drawable.abc_cab_background_internal_bg, 90 R.drawable.abc_menu_hardkey_panel_mtrl_mult 91 }; 92 93 /** 94 * Drawables which should be tinted using a state list containing values of 95 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} 96 */ 97 private static final int[] TINT_COLOR_CONTROL_STATE_LIST = { 98 R.drawable.abc_edit_text_material, 99 R.drawable.abc_tab_indicator_material, 100 R.drawable.abc_textfield_search_material, 101 R.drawable.abc_spinner_mtrl_am_alpha, 102 R.drawable.abc_btn_check_material, 103 R.drawable.abc_btn_radio_material, 104 R.drawable.abc_spinner_textfield_background_material, 105 R.drawable.abc_ratingbar_full_material, 106 R.drawable.abc_switch_track_mtrl_alpha, 107 R.drawable.abc_switch_thumb_material, 108 R.drawable.abc_btn_default_mtrl_shape, 109 R.drawable.abc_btn_borderless_material 110 }; 111 112 /** 113 * Drawables which contain other drawables which should be tinted. The child drawable IDs 114 * should be defined in one of the arrays above. 115 */ 116 private static final int[] CONTAINERS_WITH_TINT_CHILDREN = { 117 R.drawable.abc_cab_background_top_material 118 }; 119 120 private final Context mContext; 121 private final Resources mResources; 122 private final TypedValue mTypedValue; 123 124 private final SparseArray<ColorStateList> mColorStateLists; 125 private ColorStateList mDefaultColorStateList; 126 127 /** 128 * A helper method to instantiate a {@link TintManager} and then call {@link #getDrawable(int)}. 129 * This method should not be used routinely. 130 */ 131 public static Drawable getDrawable(Context context, int resId) { 132 if (isInTintList(resId)) { 133 final TintManager tm = (context instanceof TintContextWrapper) 134 ? ((TintContextWrapper) context).getTintManager() 135 : new TintManager(context); 136 return tm.getDrawable(resId); 137 } else { 138 return ContextCompat.getDrawable(context, resId); 139 } 140 } 141 142 public TintManager(Context context) { 143 mColorStateLists = new SparseArray<>(); 144 mContext = context; 145 mTypedValue = new TypedValue(); 146 mResources = new TintResources(context.getResources(), this); 147 } 148 149 Resources getResources() { 150 return mResources; 151 } 152 153 public Drawable getDrawable(int resId) { 154 Drawable drawable = ContextCompat.getDrawable(mContext, resId); 155 156 if (drawable != null) { 157 drawable = drawable.mutate(); 158 159 if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) { 160 ColorStateList colorStateList = getColorStateListForKnownDrawableId(resId); 161 PorterDuff.Mode tintMode = DEFAULT_MODE; 162 if (resId == R.drawable.abc_switch_thumb_material) { 163 tintMode = PorterDuff.Mode.MULTIPLY; 164 } 165 166 if (colorStateList != null) { 167 drawable = DrawableCompat.wrap(drawable); 168 DrawableCompat.setTintList(drawable, colorStateList); 169 DrawableCompat.setTintMode(drawable, tintMode); 170 } 171 } else if (arrayContains(CONTAINERS_WITH_TINT_CHILDREN, resId)) { 172 drawable = mResources.getDrawable(resId); 173 } else { 174 tintDrawable(resId, drawable); 175 } 176 } 177 return drawable; 178 } 179 180 void tintDrawable(final int resId, final Drawable drawable) { 181 PorterDuff.Mode tintMode = null; 182 boolean colorAttrSet = false; 183 int colorAttr = 0; 184 int alpha = -1; 185 186 if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) { 187 colorAttr = R.attr.colorControlNormal; 188 colorAttrSet = true; 189 } else if (arrayContains(TINT_COLOR_CONTROL_ACTIVATED, resId)) { 190 colorAttr = R.attr.colorControlActivated; 191 colorAttrSet = true; 192 } else if (arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, resId)) { 193 colorAttr = android.R.attr.colorBackground; 194 colorAttrSet = true; 195 tintMode = PorterDuff.Mode.MULTIPLY; 196 } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) { 197 colorAttr = android.R.attr.colorForeground; 198 colorAttrSet = true; 199 alpha = Math.round(0.16f * 255); 200 } 201 202 if (colorAttrSet) { 203 if (tintMode == null) { 204 tintMode = DEFAULT_MODE; 205 } 206 final int color = getThemeAttrColor(colorAttr); 207 208 tintDrawableUsingColorFilter(drawable, color, tintMode); 209 210 if (alpha != -1) { 211 drawable.setAlpha(alpha); 212 } 213 214 if (DEBUG) { 215 Log.d(TAG, "Tinted Drawable ID: " + mResources.getResourceName(resId) + 216 " with color: #" + Integer.toHexString(color)); 217 } 218 } 219 } 220 221 private static boolean arrayContains(int[] array, int value) { 222 for (int id : array) { 223 if (id == value) { 224 return true; 225 } 226 } 227 return false; 228 } 229 230 private static boolean isInTintList(int drawableId) { 231 return arrayContains(TINT_COLOR_BACKGROUND_MULTIPLY, drawableId) || 232 arrayContains(TINT_COLOR_CONTROL_NORMAL, drawableId) || 233 arrayContains(TINT_COLOR_CONTROL_ACTIVATED, drawableId) || 234 arrayContains(TINT_COLOR_CONTROL_STATE_LIST, drawableId) || 235 arrayContains(CONTAINERS_WITH_TINT_CHILDREN, drawableId); 236 } 237 238 ColorStateList getColorStateList(int resId) { 239 return arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId) 240 ? getColorStateListForKnownDrawableId(resId) 241 : null; 242 } 243 244 private ColorStateList getColorStateListForKnownDrawableId(int resId) { 245 // Try the cache first 246 ColorStateList colorStateList = mColorStateLists.get(resId); 247 248 if (colorStateList == null) { 249 // ...if the cache did not contain a color state list, try and create 250 if (resId == R.drawable.abc_edit_text_material) { 251 colorStateList = createEditTextColorStateList(); 252 } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) { 253 colorStateList = createSwitchTrackColorStateList(); 254 } else if (resId == R.drawable.abc_switch_thumb_material) { 255 colorStateList = createSwitchThumbColorStateList(); 256 } else if (resId == R.drawable.abc_btn_default_mtrl_shape 257 || resId == R.drawable.abc_btn_borderless_material) { 258 colorStateList = createButtonColorStateList(); 259 } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha 260 || resId == R.drawable.abc_spinner_textfield_background_material) { 261 colorStateList = createSpinnerColorStateList(); 262 } else { 263 // If we don't have an explicit color state list for this Drawable, use the default 264 colorStateList = getDefaultColorStateList(); 265 } 266 267 // ..and add it to the cache 268 mColorStateLists.append(resId, colorStateList); 269 } 270 return colorStateList; 271 } 272 273 private ColorStateList getDefaultColorStateList() { 274 if (mDefaultColorStateList == null) { 275 /** 276 * Generate the default color state list which uses the colorControl attributes. 277 * Order is important here. The default enabled state needs to go at the bottom. 278 */ 279 280 final int colorControlNormal = getThemeAttrColor(R.attr.colorControlNormal); 281 final int colorControlActivated = getThemeAttrColor(R.attr.colorControlActivated); 282 283 final int[][] states = new int[7][]; 284 final int[] colors = new int[7]; 285 int i = 0; 286 287 // Disabled state 288 states[i] = new int[] { -android.R.attr.state_enabled }; 289 colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal); 290 i++; 291 292 states[i] = new int[] { android.R.attr.state_focused }; 293 colors[i] = colorControlActivated; 294 i++; 295 296 states[i] = new int[] { android.R.attr.state_activated }; 297 colors[i] = colorControlActivated; 298 i++; 299 300 states[i] = new int[] { android.R.attr.state_pressed }; 301 colors[i] = colorControlActivated; 302 i++; 303 304 states[i] = new int[] { android.R.attr.state_checked }; 305 colors[i] = colorControlActivated; 306 i++; 307 308 states[i] = new int[] { android.R.attr.state_selected }; 309 colors[i] = colorControlActivated; 310 i++; 311 312 // Default enabled state 313 states[i] = new int[0]; 314 colors[i] = colorControlNormal; 315 i++; 316 317 mDefaultColorStateList = new ColorStateList(states, colors); 318 } 319 return mDefaultColorStateList; 320 } 321 322 private ColorStateList createSwitchTrackColorStateList() { 323 final int[][] states = new int[3][]; 324 final int[] colors = new int[3]; 325 int i = 0; 326 327 // Disabled state 328 states[i] = new int[]{-android.R.attr.state_enabled}; 329 colors[i] = getThemeAttrColor(android.R.attr.colorForeground, 0.1f); 330 i++; 331 332 states[i] = new int[]{android.R.attr.state_checked}; 333 colors[i] = getThemeAttrColor(R.attr.colorControlActivated, 0.3f); 334 i++; 335 336 // Default enabled state 337 states[i] = new int[0]; 338 colors[i] = getThemeAttrColor(android.R.attr.colorForeground, 0.3f); 339 i++; 340 341 return new ColorStateList(states, colors); 342 } 343 344 private ColorStateList createSwitchThumbColorStateList() { 345 final int[][] states = new int[3][]; 346 final int[] colors = new int[3]; 347 int i = 0; 348 349 final ColorStateList thumbColor = getThemeAttrColorStateList(R.attr.colorSwitchThumbNormal); 350 351 if (thumbColor != null && thumbColor.isStateful()) { 352 // If colorSwitchThumbNormal is a valid ColorStateList, extract the default and 353 // disabled colors from it 354 355 // Disabled state 356 states[i] = new int[]{-android.R.attr.state_enabled}; 357 colors[i] = thumbColor.getColorForState(states[i], 0); 358 i++; 359 360 states[i] = new int[]{android.R.attr.state_checked}; 361 colors[i] = getThemeAttrColor(R.attr.colorControlActivated); 362 i++; 363 364 // Default enabled state 365 states[i] = new int[0]; 366 colors[i] = thumbColor.getDefaultColor(); 367 i++; 368 } else { 369 // Else we'll use an approximation using the default disabled alpha 370 371 // Disabled state 372 states[i] = new int[]{-android.R.attr.state_enabled}; 373 colors[i] = getDisabledThemeAttrColor(R.attr.colorSwitchThumbNormal); 374 i++; 375 376 states[i] = new int[]{android.R.attr.state_checked}; 377 colors[i] = getThemeAttrColor(R.attr.colorControlActivated); 378 i++; 379 380 // Default enabled state 381 states[i] = new int[0]; 382 colors[i] = getThemeAttrColor(R.attr.colorSwitchThumbNormal); 383 i++; 384 } 385 386 return new ColorStateList(states, colors); 387 } 388 389 private ColorStateList createEditTextColorStateList() { 390 final int[][] states = new int[3][]; 391 final int[] colors = new int[3]; 392 int i = 0; 393 394 // Disabled state 395 states[i] = new int[]{-android.R.attr.state_enabled}; 396 colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal); 397 i++; 398 399 states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused}; 400 colors[i] = getThemeAttrColor(R.attr.colorControlNormal); 401 i++; 402 403 // Default enabled state 404 states[i] = new int[0]; 405 colors[i] = getThemeAttrColor(R.attr.colorControlActivated); 406 i++; 407 408 return new ColorStateList(states, colors); 409 } 410 411 private ColorStateList createButtonColorStateList() { 412 final int[][] states = new int[4][]; 413 final int[] colors = new int[4]; 414 int i = 0; 415 416 // Disabled state 417 states[i] = new int[]{-android.R.attr.state_enabled}; 418 colors[i] = getDisabledThemeAttrColor(R.attr.colorButtonNormal); 419 i++; 420 421 states[i] = new int[]{android.R.attr.state_pressed}; 422 colors[i] = getThemeAttrColor(R.attr.colorControlHighlight); 423 i++; 424 425 states[i] = new int[]{android.R.attr.state_focused}; 426 colors[i] = getThemeAttrColor(R.attr.colorControlHighlight); 427 i++; 428 429 // Default enabled state 430 states[i] = new int[0]; 431 colors[i] = getThemeAttrColor(R.attr.colorButtonNormal); 432 i++; 433 434 return new ColorStateList(states, colors); 435 } 436 437 private ColorStateList createSpinnerColorStateList() { 438 final int[][] states = new int[3][]; 439 final int[] colors = new int[3]; 440 int i = 0; 441 442 // Disabled state 443 states[i] = new int[]{-android.R.attr.state_enabled}; 444 colors[i] = getDisabledThemeAttrColor(R.attr.colorControlNormal); 445 i++; 446 447 states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused}; 448 colors[i] = getThemeAttrColor(R.attr.colorControlNormal); 449 i++; 450 451 states[i] = new int[0]; 452 colors[i] = getThemeAttrColor(R.attr.colorControlActivated); 453 i++; 454 455 return new ColorStateList(states, colors); 456 } 457 458 private int getThemeAttrColor(int attr) { 459 if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) { 460 if (mTypedValue.type >= TypedValue.TYPE_FIRST_INT 461 && mTypedValue.type <= TypedValue.TYPE_LAST_INT) { 462 return mTypedValue.data; 463 } else if (mTypedValue.type == TypedValue.TYPE_STRING) { 464 return mResources.getColor(mTypedValue.resourceId); 465 } 466 } 467 return 0; 468 } 469 470 private ColorStateList getThemeAttrColorStateList(int attr) { 471 if (mContext.getTheme().resolveAttribute(attr, mTypedValue, true)) { 472 if (mTypedValue.type == TypedValue.TYPE_STRING) { 473 return mResources.getColorStateList(mTypedValue.resourceId); 474 } 475 } 476 return null; 477 } 478 479 private int getThemeAttrColor(int attr, float alpha) { 480 final int color = getThemeAttrColor(attr); 481 final int originalAlpha = Color.alpha(color); 482 483 // Return the color, multiplying the original alpha by the disabled value 484 return (color & 0x00ffffff) | (Math.round(originalAlpha * alpha) << 24); 485 } 486 487 private int getDisabledThemeAttrColor(int attr) { 488 // Now retrieve the disabledAlpha value from the theme 489 mContext.getTheme().resolveAttribute(android.R.attr.disabledAlpha, mTypedValue, true); 490 final float disabledAlpha = mTypedValue.getFloat(); 491 492 return getThemeAttrColor(attr, disabledAlpha); 493 } 494 495 private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> { 496 497 public ColorFilterLruCache(int maxSize) { 498 super(maxSize); 499 } 500 501 PorterDuffColorFilter get(int color, PorterDuff.Mode mode) { 502 return get(generateCacheKey(color, mode)); 503 } 504 505 PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) { 506 return put(generateCacheKey(color, mode), filter); 507 } 508 509 private static int generateCacheKey(int color, PorterDuff.Mode mode) { 510 int hashCode = 1; 511 hashCode = 31 * hashCode + color; 512 hashCode = 31 * hashCode + mode.hashCode(); 513 return hashCode; 514 } 515 } 516 517 public static void tintViewBackground(View view, TintInfo tint) { 518 final Drawable background = view.getBackground(); 519 if (tint.mTintList != null) { 520 tintDrawableUsingColorFilter( 521 background, 522 tint.mTintList.getColorForState(view.getDrawableState(), 523 tint.mTintList.getDefaultColor()), 524 tint.mTintMode != null ? tint.mTintMode : DEFAULT_MODE); 525 } else { 526 background.clearColorFilter(); 527 } 528 } 529 530 private static void tintDrawableUsingColorFilter(Drawable drawable, int color, 531 PorterDuff.Mode mode) { 532 // First, lets see if the cache already contains the color filter 533 PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode); 534 535 if (filter == null) { 536 // Cache miss, so create a color filter and add it to the cache 537 filter = new PorterDuffColorFilter(color, mode); 538 COLOR_FILTER_CACHE.put(color, mode, filter); 539 } 540 541 drawable.setColorFilter(filter); 542 } 543} 544