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 androidx.appcompat.widget; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 20 import static androidx.appcompat.content.res.AppCompatResources.getColorStateList; 21 import static androidx.appcompat.widget.ThemeUtils.getDisabledThemeAttrColor; 22 import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColor; 23 import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColorStateList; 24 import static androidx.core.graphics.ColorUtils.compositeColors; 25 26 import android.content.Context; 27 import android.content.res.ColorStateList; 28 import android.graphics.Bitmap; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.PorterDuff; 32 import android.graphics.PorterDuffColorFilter; 33 import android.graphics.Shader; 34 import android.graphics.drawable.BitmapDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.graphics.drawable.LayerDrawable; 37 import android.util.Log; 38 39 import androidx.annotation.ColorInt; 40 import androidx.annotation.DimenRes; 41 import androidx.annotation.DrawableRes; 42 import androidx.annotation.RestrictTo; 43 import androidx.appcompat.R; 44 import androidx.core.graphics.drawable.DrawableCompat; 45 46 import org.jspecify.annotations.NonNull; 47 48 /** 49 */ 50 @RestrictTo(LIBRARY_GROUP_PREFIX) 51 public final class AppCompatDrawableManager { 52 private static final String TAG = "AppCompatDrawableManag"; 53 private static final boolean DEBUG = false; 54 private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; 55 56 private static AppCompatDrawableManager INSTANCE; 57 preload()58 public static synchronized void preload() { 59 if (INSTANCE == null) { 60 INSTANCE = new AppCompatDrawableManager(); 61 INSTANCE.mResourceManager = ResourceManagerInternal.get(); 62 INSTANCE.mResourceManager.setHooks(new ResourceManagerInternal.ResourceManagerHooks() { 63 /** 64 * Drawables which should be tinted with the value of 65 * {@code R.attr.colorControlNormal}, using the default mode using a raw color 66 * filter. 67 */ 68 private final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = { 69 R.drawable.abc_textfield_search_default_mtrl_alpha, 70 R.drawable.abc_textfield_default_mtrl_alpha, 71 R.drawable.abc_ab_share_pack_mtrl_alpha 72 }; 73 74 /** 75 * Drawables which should be tinted with the value of 76 * {@code R.attr.colorControlNormal}, using {@link DrawableCompat}'s tinting 77 * functionality. 78 */ 79 private final int[] TINT_COLOR_CONTROL_NORMAL = { 80 R.drawable.abc_ic_commit_search_api_mtrl_alpha, 81 R.drawable.abc_seekbar_tick_mark_material, 82 R.drawable.abc_ic_menu_share_mtrl_alpha, 83 R.drawable.abc_ic_menu_copy_mtrl_am_alpha, 84 R.drawable.abc_ic_menu_cut_mtrl_alpha, 85 R.drawable.abc_ic_menu_selectall_mtrl_alpha, 86 R.drawable.abc_ic_menu_paste_mtrl_am_alpha 87 }; 88 89 /** 90 * Drawables which should be tinted with the value of 91 * {@code R.attr.colorControlActivated}, using a color filter. 92 */ 93 private final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = { 94 R.drawable.abc_textfield_activated_mtrl_alpha, 95 R.drawable.abc_textfield_search_activated_mtrl_alpha, 96 R.drawable.abc_cab_background_top_mtrl_alpha, 97 R.drawable.abc_text_cursor_material, 98 R.drawable.abc_text_select_handle_left_mtrl, 99 R.drawable.abc_text_select_handle_middle_mtrl, 100 R.drawable.abc_text_select_handle_right_mtrl 101 }; 102 103 /** 104 * Drawables which should be tinted with the value of 105 * {@code android.R.attr.colorBackground}, using the 106 * {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter. 107 */ 108 private final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = { 109 R.drawable.abc_popup_background_mtrl_mult, 110 R.drawable.abc_cab_background_internal_bg, 111 R.drawable.abc_menu_hardkey_panel_mtrl_mult 112 }; 113 114 /** 115 * Drawables which should be tinted using a state list containing values of 116 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} 117 */ 118 private final int[] TINT_COLOR_CONTROL_STATE_LIST = { 119 R.drawable.abc_tab_indicator_material, 120 R.drawable.abc_textfield_search_material 121 }; 122 123 /** 124 * Drawables which should be tinted using a state list containing values of 125 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} 126 * for the checked state. 127 */ 128 private final int[] TINT_CHECKABLE_BUTTON_LIST = { 129 R.drawable.abc_btn_check_material, 130 R.drawable.abc_btn_radio_material, 131 R.drawable.abc_btn_check_material_anim, 132 R.drawable.abc_btn_radio_material_anim 133 }; 134 135 private ColorStateList createDefaultButtonColorStateList(@NonNull Context context) { 136 return createButtonColorStateList(context, 137 getThemeAttrColor(context, R.attr.colorButtonNormal)); 138 } 139 140 private ColorStateList createBorderlessButtonColorStateList( 141 @NonNull Context context) { 142 // We ignore the custom tint for borderless buttons 143 return createButtonColorStateList(context, Color.TRANSPARENT); 144 } 145 146 private ColorStateList createColoredButtonColorStateList( 147 @NonNull Context context) { 148 return createButtonColorStateList(context, 149 getThemeAttrColor(context, R.attr.colorAccent)); 150 } 151 152 private ColorStateList createButtonColorStateList(final @NonNull Context context, 153 @ColorInt final int baseColor) { 154 final int[][] states = new int[4][]; 155 final int[] colors = new int[4]; 156 int i = 0; 157 158 final int colorControlHighlight = getThemeAttrColor(context, 159 R.attr.colorControlHighlight); 160 final int disabledColor = getDisabledThemeAttrColor(context, 161 R.attr.colorButtonNormal); 162 163 // Disabled state 164 states[i] = ThemeUtils.DISABLED_STATE_SET; 165 colors[i] = disabledColor; 166 i++; 167 168 states[i] = ThemeUtils.PRESSED_STATE_SET; 169 colors[i] = compositeColors(colorControlHighlight, baseColor); 170 i++; 171 172 states[i] = ThemeUtils.FOCUSED_STATE_SET; 173 colors[i] = compositeColors(colorControlHighlight, baseColor); 174 i++; 175 176 // Default enabled state 177 states[i] = ThemeUtils.EMPTY_STATE_SET; 178 colors[i] = baseColor; 179 i++; 180 181 return new ColorStateList(states, colors); 182 } 183 184 private ColorStateList createSwitchThumbColorStateList(Context context) { 185 final int[][] states = new int[3][]; 186 final int[] colors = new int[3]; 187 int i = 0; 188 189 final ColorStateList thumbColor = getThemeAttrColorStateList(context, 190 R.attr.colorSwitchThumbNormal); 191 192 if (thumbColor != null && thumbColor.isStateful()) { 193 // If colorSwitchThumbNormal is a valid ColorStateList, extract the 194 // default and disabled colors from it 195 196 // Disabled state 197 states[i] = ThemeUtils.DISABLED_STATE_SET; 198 colors[i] = thumbColor.getColorForState(states[i], 0); 199 i++; 200 201 states[i] = ThemeUtils.CHECKED_STATE_SET; 202 colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated); 203 i++; 204 205 // Default enabled state 206 states[i] = ThemeUtils.EMPTY_STATE_SET; 207 colors[i] = thumbColor.getDefaultColor(); 208 i++; 209 } else { 210 // Else we'll use an approximation using the default disabled alpha 211 212 // Disabled state 213 states[i] = ThemeUtils.DISABLED_STATE_SET; 214 colors[i] = getDisabledThemeAttrColor(context, 215 R.attr.colorSwitchThumbNormal); 216 i++; 217 218 states[i] = ThemeUtils.CHECKED_STATE_SET; 219 colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated); 220 i++; 221 222 // Default enabled state 223 states[i] = ThemeUtils.EMPTY_STATE_SET; 224 colors[i] = getThemeAttrColor(context, R.attr.colorSwitchThumbNormal); 225 i++; 226 } 227 228 return new ColorStateList(states, colors); 229 } 230 231 @Override 232 public Drawable createDrawableFor(@NonNull ResourceManagerInternal resourceManager, 233 @NonNull Context context, int resId) { 234 if (resId == R.drawable.abc_cab_background_top_material) { 235 return new LayerDrawable(new Drawable[]{ 236 resourceManager.getDrawable(context, 237 R.drawable.abc_cab_background_internal_bg), 238 resourceManager.getDrawable(context, 239 R.drawable.abc_cab_background_top_mtrl_alpha) 240 }); 241 } 242 if (resId == R.drawable.abc_ratingbar_material) { 243 return getRatingBarLayerDrawable(resourceManager, context, 244 R.dimen.abc_star_big); 245 } 246 if (resId == R.drawable.abc_ratingbar_indicator_material) { 247 return getRatingBarLayerDrawable(resourceManager, context, 248 R.dimen.abc_star_medium); 249 } 250 if (resId == R.drawable.abc_ratingbar_small_material) { 251 return getRatingBarLayerDrawable(resourceManager, context, 252 R.dimen.abc_star_small); 253 } 254 return null; 255 } 256 257 private LayerDrawable getRatingBarLayerDrawable( 258 @NonNull ResourceManagerInternal resourceManager, 259 @NonNull Context context, @DimenRes int dimenResId) { 260 int starSize = context.getResources().getDimensionPixelSize(dimenResId); 261 262 Drawable star = resourceManager.getDrawable(context, 263 R.drawable.abc_star_black_48dp); 264 Drawable halfStar = resourceManager.getDrawable(context, 265 R.drawable.abc_star_half_black_48dp); 266 267 BitmapDrawable starBitmapDrawable; 268 BitmapDrawable tiledStarBitmapDrawable; 269 if ((star instanceof BitmapDrawable) && (star.getIntrinsicWidth() == starSize) 270 && (star.getIntrinsicHeight() == starSize)) { 271 // no need for extra conversion 272 starBitmapDrawable = (BitmapDrawable) star; 273 274 tiledStarBitmapDrawable = 275 new BitmapDrawable(starBitmapDrawable.getBitmap()); 276 } else { 277 Bitmap bitmapStar = Bitmap.createBitmap(starSize, starSize, 278 Bitmap.Config.ARGB_8888); 279 Canvas canvasStar = new Canvas(bitmapStar); 280 star.setBounds(0, 0, starSize, starSize); 281 star.draw(canvasStar); 282 starBitmapDrawable = new BitmapDrawable(bitmapStar); 283 284 tiledStarBitmapDrawable = new BitmapDrawable(bitmapStar); 285 } 286 tiledStarBitmapDrawable.setTileModeX(Shader.TileMode.REPEAT); 287 288 BitmapDrawable halfStarBitmapDrawable; 289 if ((halfStar instanceof BitmapDrawable) 290 && (halfStar.getIntrinsicWidth() == starSize) 291 && (halfStar.getIntrinsicHeight() == starSize)) { 292 // no need for extra conversion 293 halfStarBitmapDrawable = (BitmapDrawable) halfStar; 294 } else { 295 Bitmap bitmapHalfStar = Bitmap.createBitmap(starSize, starSize, 296 Bitmap.Config.ARGB_8888); 297 Canvas canvasHalfStar = new Canvas(bitmapHalfStar); 298 halfStar.setBounds(0, 0, starSize, starSize); 299 halfStar.draw(canvasHalfStar); 300 halfStarBitmapDrawable = new BitmapDrawable(bitmapHalfStar); 301 } 302 303 LayerDrawable result = new LayerDrawable(new Drawable[]{ 304 starBitmapDrawable, halfStarBitmapDrawable, tiledStarBitmapDrawable 305 }); 306 result.setId(0, android.R.id.background); 307 result.setId(1, android.R.id.secondaryProgress); 308 result.setId(2, android.R.id.progress); 309 return result; 310 } 311 312 private void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) { 313 d = d.mutate(); 314 d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE 315 : mode)); 316 } 317 318 @Override 319 public boolean tintDrawable(@NonNull Context context, int resId, 320 @NonNull Drawable drawable) { 321 if (resId == R.drawable.abc_seekbar_track_material) { 322 LayerDrawable ld = (LayerDrawable) drawable; 323 setPorterDuffColorFilter( 324 ld.findDrawableByLayerId(android.R.id.background), 325 getThemeAttrColor(context, R.attr.colorControlNormal), 326 DEFAULT_MODE); 327 setPorterDuffColorFilter( 328 ld.findDrawableByLayerId(android.R.id.secondaryProgress), 329 getThemeAttrColor(context, R.attr.colorControlNormal), 330 DEFAULT_MODE); 331 setPorterDuffColorFilter( 332 ld.findDrawableByLayerId(android.R.id.progress), 333 getThemeAttrColor(context, R.attr.colorControlActivated), 334 DEFAULT_MODE); 335 return true; 336 } else if (resId == R.drawable.abc_ratingbar_material 337 || resId == R.drawable.abc_ratingbar_indicator_material 338 || resId == R.drawable.abc_ratingbar_small_material) { 339 LayerDrawable ld = (LayerDrawable) drawable; 340 setPorterDuffColorFilter( 341 ld.findDrawableByLayerId(android.R.id.background), 342 getDisabledThemeAttrColor(context, R.attr.colorControlNormal), 343 DEFAULT_MODE); 344 setPorterDuffColorFilter( 345 ld.findDrawableByLayerId(android.R.id.secondaryProgress), 346 getThemeAttrColor(context, R.attr.colorControlActivated), 347 DEFAULT_MODE); 348 setPorterDuffColorFilter( 349 ld.findDrawableByLayerId(android.R.id.progress), 350 getThemeAttrColor(context, R.attr.colorControlActivated), 351 DEFAULT_MODE); 352 return true; 353 } 354 return false; 355 } 356 357 private boolean arrayContains(int[] array, int value) { 358 for (int id : array) { 359 if (id == value) { 360 return true; 361 } 362 } 363 return false; 364 } 365 366 @Override 367 public ColorStateList getTintListForDrawableRes(@NonNull Context context, 368 int resId) { 369 // ...if the cache did not contain a color state list, try and create one 370 if (resId == R.drawable.abc_edit_text_material) { 371 return getColorStateList(context, R.color.abc_tint_edittext); 372 } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) { 373 return getColorStateList(context, R.color.abc_tint_switch_track); 374 } else if (resId == R.drawable.abc_switch_thumb_material) { 375 return createSwitchThumbColorStateList(context); 376 } else if (resId == R.drawable.abc_btn_default_mtrl_shape) { 377 return createDefaultButtonColorStateList(context); 378 } else if (resId == R.drawable.abc_btn_borderless_material) { 379 return createBorderlessButtonColorStateList(context); 380 } else if (resId == R.drawable.abc_btn_colored_material) { 381 return createColoredButtonColorStateList(context); 382 } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha 383 || resId == R.drawable.abc_spinner_textfield_background_material) { 384 return getColorStateList(context, R.color.abc_tint_spinner); 385 } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) { 386 return getThemeAttrColorStateList(context, R.attr.colorControlNormal); 387 } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) { 388 return getColorStateList(context, R.color.abc_tint_default); 389 } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) { 390 return getColorStateList(context, R.color.abc_tint_btn_checkable); 391 } else if (resId == R.drawable.abc_seekbar_thumb_material) { 392 return getColorStateList(context, R.color.abc_tint_seek_thumb); 393 } 394 return null; 395 } 396 397 @Override 398 public boolean tintDrawableUsingColorFilter(@NonNull Context context, 399 int resId, @NonNull Drawable drawable) { 400 PorterDuff.Mode tintMode = DEFAULT_MODE; 401 boolean colorAttrSet = false; 402 int colorAttr = 0; 403 int alpha = -1; 404 405 if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) { 406 colorAttr = R.attr.colorControlNormal; 407 colorAttrSet = true; 408 } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) { 409 colorAttr = R.attr.colorControlActivated; 410 colorAttrSet = true; 411 } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) { 412 colorAttr = android.R.attr.colorBackground; 413 colorAttrSet = true; 414 tintMode = PorterDuff.Mode.MULTIPLY; 415 } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) { 416 colorAttr = android.R.attr.colorForeground; 417 colorAttrSet = true; 418 alpha = Math.round(0.16f * 255); 419 } else if (resId == R.drawable.abc_dialog_material_background) { 420 colorAttr = android.R.attr.colorBackground; 421 colorAttrSet = true; 422 } 423 424 if (colorAttrSet) { 425 drawable = drawable.mutate(); 426 427 final int color = getThemeAttrColor(context, colorAttr); 428 drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode)); 429 430 if (alpha != -1) { 431 drawable.setAlpha(alpha); 432 } 433 434 if (DEBUG) { 435 Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted " 436 + context.getResources().getResourceName(resId) 437 + " with color: #" + Integer.toHexString(color)); 438 } 439 return true; 440 } 441 return false; 442 } 443 444 @Override 445 public PorterDuff.Mode getTintModeForDrawableRes(int resId) { 446 PorterDuff.Mode mode = null; 447 448 if (resId == R.drawable.abc_switch_thumb_material) { 449 mode = PorterDuff.Mode.MULTIPLY; 450 } 451 452 return mode; 453 } 454 }); 455 } 456 } 457 458 /** 459 * Returns the singleton instance of this class. 460 */ get()461 public static synchronized AppCompatDrawableManager get() { 462 if (INSTANCE == null) { 463 preload(); 464 } 465 return INSTANCE; 466 } 467 468 private ResourceManagerInternal mResourceManager; 469 getDrawable(@onNull Context context, @DrawableRes int resId)470 public synchronized Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) { 471 return mResourceManager.getDrawable(context, resId); 472 } 473 getDrawable(@onNull Context context, @DrawableRes int resId, boolean failIfNotKnown)474 synchronized Drawable getDrawable(@NonNull Context context, @DrawableRes int resId, 475 boolean failIfNotKnown) { 476 return mResourceManager.getDrawable(context, resId, failIfNotKnown); 477 } 478 onConfigurationChanged(@onNull Context context)479 public synchronized void onConfigurationChanged(@NonNull Context context) { 480 mResourceManager.onConfigurationChanged(context); 481 } 482 onDrawableLoadedFromResources(@onNull Context context, @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId)483 synchronized Drawable onDrawableLoadedFromResources(@NonNull Context context, 484 @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId) { 485 return mResourceManager.onDrawableLoadedFromResources(context, resources, resId); 486 } 487 tintDrawableUsingColorFilter(@onNull Context context, @DrawableRes final int resId, @NonNull Drawable drawable)488 boolean tintDrawableUsingColorFilter(@NonNull Context context, 489 @DrawableRes final int resId, @NonNull Drawable drawable) { 490 return mResourceManager.tintDrawableUsingColorFilter(context, resId, drawable); 491 } 492 getTintList(@onNull Context context, @DrawableRes int resId)493 synchronized ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) { 494 return mResourceManager.getTintList(context, resId); 495 } 496 tintDrawable(Drawable drawable, TintInfo tint, int[] state)497 static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) { 498 ResourceManagerInternal.tintDrawable(drawable, tint, state); 499 } 500 getPorterDuffColorFilter( int color, PorterDuff.Mode mode)501 public static synchronized PorterDuffColorFilter getPorterDuffColorFilter( 502 int color, PorterDuff.Mode mode) { 503 return ResourceManagerInternal.getPorterDuffColorFilter(color, mode); 504 } 505 } 506