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