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; 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.annotation.SuppressLint; 27 import android.content.Context; 28 import android.content.res.ColorStateList; 29 import android.content.res.Resources; 30 import android.graphics.Color; 31 import android.graphics.PorterDuff; 32 import android.graphics.PorterDuffColorFilter; 33 import android.graphics.drawable.Drawable; 34 import android.graphics.drawable.Drawable.ConstantState; 35 import android.graphics.drawable.LayerDrawable; 36 import android.os.Build; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.util.TypedValue; 40 import android.util.Xml; 41 42 import androidx.annotation.ColorInt; 43 import androidx.annotation.DrawableRes; 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.annotation.RestrictTo; 47 import androidx.appcompat.R; 48 import androidx.collection.ArrayMap; 49 import androidx.collection.LongSparseArray; 50 import androidx.collection.LruCache; 51 import androidx.collection.SparseArrayCompat; 52 import androidx.core.content.ContextCompat; 53 import androidx.core.graphics.drawable.DrawableCompat; 54 import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; 55 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; 56 57 import org.xmlpull.v1.XmlPullParser; 58 import org.xmlpull.v1.XmlPullParserException; 59 60 import java.lang.ref.WeakReference; 61 import java.util.WeakHashMap; 62 63 /** 64 * @hide 65 */ 66 @RestrictTo(LIBRARY_GROUP) 67 public final class AppCompatDrawableManager { 68 69 private interface InflateDelegate { createFromXmlInner(@onNull Context context, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)70 Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser, 71 @NonNull AttributeSet attrs, @Nullable Resources.Theme theme); 72 } 73 74 private static final String TAG = "AppCompatDrawableManag"; 75 private static final boolean DEBUG = false; 76 private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; 77 private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip"; 78 79 private static final String PLATFORM_VD_CLAZZ = "android.graphics.drawable.VectorDrawable"; 80 81 private static AppCompatDrawableManager INSTANCE; 82 get()83 public static AppCompatDrawableManager get() { 84 if (INSTANCE == null) { 85 INSTANCE = new AppCompatDrawableManager(); 86 installDefaultInflateDelegates(INSTANCE); 87 } 88 return INSTANCE; 89 } 90 installDefaultInflateDelegates(@onNull AppCompatDrawableManager manager)91 private static void installDefaultInflateDelegates(@NonNull AppCompatDrawableManager manager) { 92 // This sdk version check will affect src:appCompat code path. 93 // Although VectorDrawable exists in Android framework from Lollipop, AppCompat will use the 94 // VectorDrawableCompat before Nougat to utilize the bug fixes in VectorDrawableCompat. 95 if (Build.VERSION.SDK_INT < 24) { 96 manager.addDelegate("vector", new VdcInflateDelegate()); 97 manager.addDelegate("animated-vector", new AvdcInflateDelegate()); 98 } 99 } 100 101 private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6); 102 103 /** 104 * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, 105 * using the default mode using a raw color filter. 106 */ 107 private static final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = { 108 R.drawable.abc_textfield_search_default_mtrl_alpha, 109 R.drawable.abc_textfield_default_mtrl_alpha, 110 R.drawable.abc_ab_share_pack_mtrl_alpha 111 }; 112 113 /** 114 * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, using 115 * {@link DrawableCompat}'s tinting functionality. 116 */ 117 private static final int[] TINT_COLOR_CONTROL_NORMAL = { 118 R.drawable.abc_ic_commit_search_api_mtrl_alpha, 119 R.drawable.abc_seekbar_tick_mark_material, 120 R.drawable.abc_ic_menu_share_mtrl_alpha, 121 R.drawable.abc_ic_menu_copy_mtrl_am_alpha, 122 R.drawable.abc_ic_menu_cut_mtrl_alpha, 123 R.drawable.abc_ic_menu_selectall_mtrl_alpha, 124 R.drawable.abc_ic_menu_paste_mtrl_am_alpha 125 }; 126 127 /** 128 * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated}, 129 * using a color filter. 130 */ 131 private static final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = { 132 R.drawable.abc_textfield_activated_mtrl_alpha, 133 R.drawable.abc_textfield_search_activated_mtrl_alpha, 134 R.drawable.abc_cab_background_top_mtrl_alpha, 135 R.drawable.abc_text_cursor_material, 136 R.drawable.abc_text_select_handle_left_mtrl_dark, 137 R.drawable.abc_text_select_handle_middle_mtrl_dark, 138 R.drawable.abc_text_select_handle_right_mtrl_dark, 139 R.drawable.abc_text_select_handle_left_mtrl_light, 140 R.drawable.abc_text_select_handle_middle_mtrl_light, 141 R.drawable.abc_text_select_handle_right_mtrl_light 142 }; 143 144 /** 145 * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground}, 146 * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter. 147 */ 148 private static final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = { 149 R.drawable.abc_popup_background_mtrl_mult, 150 R.drawable.abc_cab_background_internal_bg, 151 R.drawable.abc_menu_hardkey_panel_mtrl_mult 152 }; 153 154 /** 155 * Drawables which should be tinted using a state list containing values of 156 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} 157 */ 158 private static final int[] TINT_COLOR_CONTROL_STATE_LIST = { 159 R.drawable.abc_tab_indicator_material, 160 R.drawable.abc_textfield_search_material 161 }; 162 163 /** 164 * Drawables which should be tinted using a state list containing values of 165 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} for the checked 166 * state. 167 */ 168 private static final int[] TINT_CHECKABLE_BUTTON_LIST = { 169 R.drawable.abc_btn_check_material, 170 R.drawable.abc_btn_radio_material 171 }; 172 173 private WeakHashMap<Context, SparseArrayCompat<ColorStateList>> mTintLists; 174 private ArrayMap<String, InflateDelegate> mDelegates; 175 private SparseArrayCompat<String> mKnownDrawableIdTags; 176 177 private final Object mDrawableCacheLock = new Object(); 178 private final WeakHashMap<Context, LongSparseArray<WeakReference<Drawable.ConstantState>>> 179 mDrawableCaches = new WeakHashMap<>(0); 180 181 private TypedValue mTypedValue; 182 183 private boolean mHasCheckedVectorDrawableSetup; 184 getDrawable(@onNull Context context, @DrawableRes int resId)185 public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) { 186 return getDrawable(context, resId, false); 187 } 188 getDrawable(@onNull Context context, @DrawableRes int resId, boolean failIfNotKnown)189 Drawable getDrawable(@NonNull Context context, @DrawableRes int resId, 190 boolean failIfNotKnown) { 191 checkVectorDrawableSetup(context); 192 193 Drawable drawable = loadDrawableFromDelegates(context, resId); 194 if (drawable == null) { 195 drawable = createDrawableIfNeeded(context, resId); 196 } 197 if (drawable == null) { 198 drawable = ContextCompat.getDrawable(context, resId); 199 } 200 201 if (drawable != null) { 202 // Tint it if needed 203 drawable = tintDrawable(context, resId, failIfNotKnown, drawable); 204 } 205 if (drawable != null) { 206 // See if we need to 'fix' the drawable 207 DrawableUtils.fixDrawable(drawable); 208 } 209 return drawable; 210 } 211 onConfigurationChanged(@onNull Context context)212 public void onConfigurationChanged(@NonNull Context context) { 213 synchronized (mDrawableCacheLock) { 214 LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context); 215 if (cache != null) { 216 // Crude, but we'll just clear the cache when the configuration changes 217 cache.clear(); 218 } 219 } 220 } 221 createCacheKey(TypedValue tv)222 private static long createCacheKey(TypedValue tv) { 223 return (((long) tv.assetCookie) << 32) | tv.data; 224 } 225 createDrawableIfNeeded(@onNull Context context, @DrawableRes final int resId)226 private Drawable createDrawableIfNeeded(@NonNull Context context, 227 @DrawableRes final int resId) { 228 if (mTypedValue == null) { 229 mTypedValue = new TypedValue(); 230 } 231 final TypedValue tv = mTypedValue; 232 context.getResources().getValue(resId, tv, true); 233 final long key = createCacheKey(tv); 234 235 Drawable dr = getCachedDrawable(context, key); 236 if (dr != null) { 237 // If we got a cached drawable, return it 238 return dr; 239 } 240 241 // Else we need to try and create one... 242 if (resId == R.drawable.abc_cab_background_top_material) { 243 dr = new LayerDrawable(new Drawable[]{ 244 getDrawable(context, R.drawable.abc_cab_background_internal_bg), 245 getDrawable(context, R.drawable.abc_cab_background_top_mtrl_alpha) 246 }); 247 } 248 249 if (dr != null) { 250 dr.setChangingConfigurations(tv.changingConfigurations); 251 // If we reached here then we created a new drawable, add it to the cache 252 addDrawableToCache(context, key, dr); 253 } 254 255 return dr; 256 } 257 tintDrawable(@onNull Context context, @DrawableRes int resId, boolean failIfNotKnown, @NonNull Drawable drawable)258 private Drawable tintDrawable(@NonNull Context context, @DrawableRes int resId, 259 boolean failIfNotKnown, @NonNull Drawable drawable) { 260 final ColorStateList tintList = getTintList(context, resId); 261 if (tintList != null) { 262 // First mutate the Drawable, then wrap it and set the tint list 263 if (DrawableUtils.canSafelyMutateDrawable(drawable)) { 264 drawable = drawable.mutate(); 265 } 266 drawable = DrawableCompat.wrap(drawable); 267 DrawableCompat.setTintList(drawable, tintList); 268 269 // If there is a blending mode specified for the drawable, use it 270 final PorterDuff.Mode tintMode = getTintMode(resId); 271 if (tintMode != null) { 272 DrawableCompat.setTintMode(drawable, tintMode); 273 } 274 } else if (resId == R.drawable.abc_seekbar_track_material) { 275 LayerDrawable ld = (LayerDrawable) drawable; 276 setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background), 277 getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE); 278 setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress), 279 getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE); 280 setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress), 281 getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE); 282 } else if (resId == R.drawable.abc_ratingbar_material 283 || resId == R.drawable.abc_ratingbar_indicator_material 284 || resId == R.drawable.abc_ratingbar_small_material) { 285 LayerDrawable ld = (LayerDrawable) drawable; 286 setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background), 287 getDisabledThemeAttrColor(context, R.attr.colorControlNormal), 288 DEFAULT_MODE); 289 setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress), 290 getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE); 291 setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress), 292 getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE); 293 } else { 294 final boolean tinted = tintDrawableUsingColorFilter(context, resId, drawable); 295 if (!tinted && failIfNotKnown) { 296 // If we didn't tint using a ColorFilter, and we're set to fail if we don't 297 // know the id, return null 298 drawable = null; 299 } 300 } 301 return drawable; 302 } 303 loadDrawableFromDelegates(@onNull Context context, @DrawableRes int resId)304 private Drawable loadDrawableFromDelegates(@NonNull Context context, @DrawableRes int resId) { 305 if (mDelegates != null && !mDelegates.isEmpty()) { 306 if (mKnownDrawableIdTags != null) { 307 final String cachedTagName = mKnownDrawableIdTags.get(resId); 308 if (SKIP_DRAWABLE_TAG.equals(cachedTagName) 309 || (cachedTagName != null && mDelegates.get(cachedTagName) == null)) { 310 // If we don't have a delegate for the drawable tag, or we've been set to 311 // skip it, fail fast and return null 312 if (DEBUG) { 313 Log.d(TAG, "[loadDrawableFromDelegates] Skipping drawable: " 314 + context.getResources().getResourceName(resId)); 315 } 316 return null; 317 } 318 } else { 319 // Create an id cache as we'll need one later 320 mKnownDrawableIdTags = new SparseArrayCompat<>(); 321 } 322 323 if (mTypedValue == null) { 324 mTypedValue = new TypedValue(); 325 } 326 final TypedValue tv = mTypedValue; 327 final Resources res = context.getResources(); 328 res.getValue(resId, tv, true); 329 330 final long key = createCacheKey(tv); 331 332 Drawable dr = getCachedDrawable(context, key); 333 if (dr != null) { 334 if (DEBUG) { 335 Log.i(TAG, "[loadDrawableFromDelegates] Returning cached drawable: " + 336 context.getResources().getResourceName(resId)); 337 } 338 // We have a cached drawable, return it! 339 return dr; 340 } 341 342 if (tv.string != null && tv.string.toString().endsWith(".xml")) { 343 // If the resource is an XML file, let's try and parse it 344 try { 345 @SuppressLint("ResourceType") final XmlPullParser parser = res.getXml(resId); 346 final AttributeSet attrs = Xml.asAttributeSet(parser); 347 int type; 348 while ((type = parser.next()) != XmlPullParser.START_TAG && 349 type != XmlPullParser.END_DOCUMENT) { 350 // Empty loop 351 } 352 if (type != XmlPullParser.START_TAG) { 353 throw new XmlPullParserException("No start tag found"); 354 } 355 356 final String tagName = parser.getName(); 357 // Add the tag name to the cache 358 mKnownDrawableIdTags.append(resId, tagName); 359 360 // Now try and find a delegate for the tag name and inflate if found 361 final InflateDelegate delegate = mDelegates.get(tagName); 362 if (delegate != null) { 363 dr = delegate.createFromXmlInner(context, parser, attrs, 364 context.getTheme()); 365 } 366 if (dr != null) { 367 // Add it to the drawable cache 368 dr.setChangingConfigurations(tv.changingConfigurations); 369 if (addDrawableToCache(context, key, dr) && DEBUG) { 370 Log.i(TAG, "[loadDrawableFromDelegates] Saved drawable to cache: " + 371 context.getResources().getResourceName(resId)); 372 } 373 } 374 } catch (Exception e) { 375 Log.e(TAG, "Exception while inflating drawable", e); 376 } 377 } 378 if (dr == null) { 379 // If we reach here then the delegate inflation of the resource failed. Mark it as 380 // bad so we skip the id next time 381 mKnownDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG); 382 } 383 return dr; 384 } 385 386 return null; 387 } 388 getCachedDrawable(@onNull final Context context, final long key)389 private Drawable getCachedDrawable(@NonNull final Context context, final long key) { 390 synchronized (mDrawableCacheLock) { 391 final LongSparseArray<WeakReference<ConstantState>> cache 392 = mDrawableCaches.get(context); 393 if (cache == null) { 394 return null; 395 } 396 397 final WeakReference<ConstantState> wr = cache.get(key); 398 if (wr != null) { 399 // We have the key, and the secret 400 ConstantState entry = wr.get(); 401 if (entry != null) { 402 return entry.newDrawable(context.getResources()); 403 } else { 404 // Our entry has been purged 405 cache.delete(key); 406 } 407 } 408 } 409 return null; 410 } 411 addDrawableToCache(@onNull final Context context, final long key, @NonNull final Drawable drawable)412 private boolean addDrawableToCache(@NonNull final Context context, final long key, 413 @NonNull final Drawable drawable) { 414 final ConstantState cs = drawable.getConstantState(); 415 if (cs != null) { 416 synchronized (mDrawableCacheLock) { 417 LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context); 418 if (cache == null) { 419 cache = new LongSparseArray<>(); 420 mDrawableCaches.put(context, cache); 421 } 422 cache.put(key, new WeakReference<>(cs)); 423 } 424 return true; 425 } 426 return false; 427 } 428 onDrawableLoadedFromResources(@onNull Context context, @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId)429 Drawable onDrawableLoadedFromResources(@NonNull Context context, 430 @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId) { 431 Drawable drawable = loadDrawableFromDelegates(context, resId); 432 if (drawable == null) { 433 drawable = resources.superGetDrawable(resId); 434 } 435 if (drawable != null) { 436 return tintDrawable(context, resId, false, drawable); 437 } 438 return null; 439 } 440 tintDrawableUsingColorFilter(@onNull Context context, @DrawableRes final int resId, @NonNull Drawable drawable)441 static boolean tintDrawableUsingColorFilter(@NonNull Context context, 442 @DrawableRes final int resId, @NonNull Drawable drawable) { 443 PorterDuff.Mode tintMode = DEFAULT_MODE; 444 boolean colorAttrSet = false; 445 int colorAttr = 0; 446 int alpha = -1; 447 448 if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) { 449 colorAttr = R.attr.colorControlNormal; 450 colorAttrSet = true; 451 } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) { 452 colorAttr = R.attr.colorControlActivated; 453 colorAttrSet = true; 454 } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) { 455 colorAttr = android.R.attr.colorBackground; 456 colorAttrSet = true; 457 tintMode = PorterDuff.Mode.MULTIPLY; 458 } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) { 459 colorAttr = android.R.attr.colorForeground; 460 colorAttrSet = true; 461 alpha = Math.round(0.16f * 255); 462 } else if (resId == R.drawable.abc_dialog_material_background) { 463 colorAttr = android.R.attr.colorBackground; 464 colorAttrSet = true; 465 } 466 467 if (colorAttrSet) { 468 if (DrawableUtils.canSafelyMutateDrawable(drawable)) { 469 drawable = drawable.mutate(); 470 } 471 472 final int color = getThemeAttrColor(context, colorAttr); 473 drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode)); 474 475 if (alpha != -1) { 476 drawable.setAlpha(alpha); 477 } 478 479 if (DEBUG) { 480 Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted " 481 + context.getResources().getResourceName(resId) + 482 " with color: #" + Integer.toHexString(color)); 483 } 484 return true; 485 } 486 return false; 487 } 488 addDelegate(@onNull String tagName, @NonNull InflateDelegate delegate)489 private void addDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) { 490 if (mDelegates == null) { 491 mDelegates = new ArrayMap<>(); 492 } 493 mDelegates.put(tagName, delegate); 494 } 495 removeDelegate(@onNull String tagName, @NonNull InflateDelegate delegate)496 private void removeDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) { 497 if (mDelegates != null && mDelegates.get(tagName) == delegate) { 498 mDelegates.remove(tagName); 499 } 500 } 501 arrayContains(int[] array, int value)502 private static boolean arrayContains(int[] array, int value) { 503 for (int id : array) { 504 if (id == value) { 505 return true; 506 } 507 } 508 return false; 509 } 510 getTintMode(final int resId)511 static PorterDuff.Mode getTintMode(final int resId) { 512 PorterDuff.Mode mode = null; 513 514 if (resId == R.drawable.abc_switch_thumb_material) { 515 mode = PorterDuff.Mode.MULTIPLY; 516 } 517 518 return mode; 519 } 520 getTintList(@onNull Context context, @DrawableRes int resId)521 ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) { 522 // Try the cache first (if it exists) 523 ColorStateList tint = getTintListFromCache(context, resId); 524 525 if (tint == null) { 526 // ...if the cache did not contain a color state list, try and create one 527 if (resId == R.drawable.abc_edit_text_material) { 528 tint = getColorStateList(context, R.color.abc_tint_edittext); 529 } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) { 530 tint = getColorStateList(context, R.color.abc_tint_switch_track); 531 } else if (resId == R.drawable.abc_switch_thumb_material) { 532 tint = createSwitchThumbColorStateList(context); 533 } else if (resId == R.drawable.abc_btn_default_mtrl_shape) { 534 tint = createDefaultButtonColorStateList(context); 535 } else if (resId == R.drawable.abc_btn_borderless_material) { 536 tint = createBorderlessButtonColorStateList(context); 537 } else if (resId == R.drawable.abc_btn_colored_material) { 538 tint = createColoredButtonColorStateList(context); 539 } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha 540 || resId == R.drawable.abc_spinner_textfield_background_material) { 541 tint = getColorStateList(context, R.color.abc_tint_spinner); 542 } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) { 543 tint = getThemeAttrColorStateList(context, R.attr.colorControlNormal); 544 } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) { 545 tint = getColorStateList(context, R.color.abc_tint_default); 546 } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) { 547 tint = getColorStateList(context, R.color.abc_tint_btn_checkable); 548 } else if (resId == R.drawable.abc_seekbar_thumb_material) { 549 tint = getColorStateList(context, R.color.abc_tint_seek_thumb); 550 } 551 552 if (tint != null) { 553 addTintListToCache(context, resId, tint); 554 } 555 } 556 return tint; 557 } 558 getTintListFromCache(@onNull Context context, @DrawableRes int resId)559 private ColorStateList getTintListFromCache(@NonNull Context context, @DrawableRes int resId) { 560 if (mTintLists != null) { 561 final SparseArrayCompat<ColorStateList> tints = mTintLists.get(context); 562 return tints != null ? tints.get(resId) : null; 563 } 564 return null; 565 } 566 addTintListToCache(@onNull Context context, @DrawableRes int resId, @NonNull ColorStateList tintList)567 private void addTintListToCache(@NonNull Context context, @DrawableRes int resId, 568 @NonNull ColorStateList tintList) { 569 if (mTintLists == null) { 570 mTintLists = new WeakHashMap<>(); 571 } 572 SparseArrayCompat<ColorStateList> themeTints = mTintLists.get(context); 573 if (themeTints == null) { 574 themeTints = new SparseArrayCompat<>(); 575 mTintLists.put(context, themeTints); 576 } 577 themeTints.append(resId, tintList); 578 } 579 createDefaultButtonColorStateList(@onNull Context context)580 private ColorStateList createDefaultButtonColorStateList(@NonNull Context context) { 581 return createButtonColorStateList(context, 582 getThemeAttrColor(context, R.attr.colorButtonNormal)); 583 } 584 createBorderlessButtonColorStateList(@onNull Context context)585 private ColorStateList createBorderlessButtonColorStateList(@NonNull Context context) { 586 // We ignore the custom tint for borderless buttons 587 return createButtonColorStateList(context, Color.TRANSPARENT); 588 } 589 createColoredButtonColorStateList(@onNull Context context)590 private ColorStateList createColoredButtonColorStateList(@NonNull Context context) { 591 return createButtonColorStateList(context, 592 getThemeAttrColor(context, R.attr.colorAccent)); 593 } 594 createButtonColorStateList(@onNull final Context context, @ColorInt final int baseColor)595 private ColorStateList createButtonColorStateList(@NonNull final Context context, 596 @ColorInt final int baseColor) { 597 final int[][] states = new int[4][]; 598 final int[] colors = new int[4]; 599 int i = 0; 600 601 final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight); 602 final int disabledColor = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal); 603 604 // Disabled state 605 states[i] = ThemeUtils.DISABLED_STATE_SET; 606 colors[i] = disabledColor; 607 i++; 608 609 states[i] = ThemeUtils.PRESSED_STATE_SET; 610 colors[i] = compositeColors(colorControlHighlight, baseColor); 611 i++; 612 613 states[i] = ThemeUtils.FOCUSED_STATE_SET; 614 colors[i] = compositeColors(colorControlHighlight, baseColor); 615 i++; 616 617 // Default enabled state 618 states[i] = ThemeUtils.EMPTY_STATE_SET; 619 colors[i] = baseColor; 620 i++; 621 622 return new ColorStateList(states, colors); 623 } 624 createSwitchThumbColorStateList(Context context)625 private ColorStateList createSwitchThumbColorStateList(Context context) { 626 final int[][] states = new int[3][]; 627 final int[] colors = new int[3]; 628 int i = 0; 629 630 final ColorStateList thumbColor = getThemeAttrColorStateList(context, 631 R.attr.colorSwitchThumbNormal); 632 633 if (thumbColor != null && thumbColor.isStateful()) { 634 // If colorSwitchThumbNormal is a valid ColorStateList, extract the default and 635 // disabled colors from it 636 637 // Disabled state 638 states[i] = ThemeUtils.DISABLED_STATE_SET; 639 colors[i] = thumbColor.getColorForState(states[i], 0); 640 i++; 641 642 states[i] = ThemeUtils.CHECKED_STATE_SET; 643 colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated); 644 i++; 645 646 // Default enabled state 647 states[i] = ThemeUtils.EMPTY_STATE_SET; 648 colors[i] = thumbColor.getDefaultColor(); 649 i++; 650 } else { 651 // Else we'll use an approximation using the default disabled alpha 652 653 // Disabled state 654 states[i] = ThemeUtils.DISABLED_STATE_SET; 655 colors[i] = getDisabledThemeAttrColor(context, R.attr.colorSwitchThumbNormal); 656 i++; 657 658 states[i] = ThemeUtils.CHECKED_STATE_SET; 659 colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated); 660 i++; 661 662 // Default enabled state 663 states[i] = ThemeUtils.EMPTY_STATE_SET; 664 colors[i] = getThemeAttrColor(context, R.attr.colorSwitchThumbNormal); 665 i++; 666 } 667 668 return new ColorStateList(states, colors); 669 } 670 671 private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> { 672 ColorFilterLruCache(int maxSize)673 public ColorFilterLruCache(int maxSize) { 674 super(maxSize); 675 } 676 get(int color, PorterDuff.Mode mode)677 PorterDuffColorFilter get(int color, PorterDuff.Mode mode) { 678 return get(generateCacheKey(color, mode)); 679 } 680 put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter)681 PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) { 682 return put(generateCacheKey(color, mode), filter); 683 } 684 generateCacheKey(int color, PorterDuff.Mode mode)685 private static int generateCacheKey(int color, PorterDuff.Mode mode) { 686 int hashCode = 1; 687 hashCode = 31 * hashCode + color; 688 hashCode = 31 * hashCode + mode.hashCode(); 689 return hashCode; 690 } 691 } 692 tintDrawable(Drawable drawable, TintInfo tint, int[] state)693 static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) { 694 if (DrawableUtils.canSafelyMutateDrawable(drawable) 695 && drawable.mutate() != drawable) { 696 Log.d(TAG, "Mutated drawable is not the same instance as the input."); 697 return; 698 } 699 700 if (tint.mHasTintList || tint.mHasTintMode) { 701 drawable.setColorFilter(createTintFilter( 702 tint.mHasTintList ? tint.mTintList : null, 703 tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE, 704 state)); 705 } else { 706 drawable.clearColorFilter(); 707 } 708 709 if (Build.VERSION.SDK_INT <= 23) { 710 // Pre-v23 there is no guarantee that a state change will invoke an invalidation, 711 // so we force it ourselves 712 drawable.invalidateSelf(); 713 } 714 } 715 createTintFilter(ColorStateList tint, PorterDuff.Mode tintMode, final int[] state)716 private static PorterDuffColorFilter createTintFilter(ColorStateList tint, 717 PorterDuff.Mode tintMode, final int[] state) { 718 if (tint == null || tintMode == null) { 719 return null; 720 } 721 final int color = tint.getColorForState(state, Color.TRANSPARENT); 722 return getPorterDuffColorFilter(color, tintMode); 723 } 724 getPorterDuffColorFilter(int color, PorterDuff.Mode mode)725 public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) { 726 // First, lets see if the cache already contains the color filter 727 PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode); 728 729 if (filter == null) { 730 // Cache miss, so create a color filter and add it to the cache 731 filter = new PorterDuffColorFilter(color, mode); 732 COLOR_FILTER_CACHE.put(color, mode, filter); 733 } 734 735 return filter; 736 } 737 setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode)738 private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) { 739 if (DrawableUtils.canSafelyMutateDrawable(d)) { 740 d = d.mutate(); 741 } 742 d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE : mode)); 743 } 744 checkVectorDrawableSetup(@onNull Context context)745 private void checkVectorDrawableSetup(@NonNull Context context) { 746 if (mHasCheckedVectorDrawableSetup) { 747 // We've already checked so return now... 748 return; 749 } 750 // Here we will check that a known Vector drawable resource inside AppCompat can be 751 // correctly decoded 752 mHasCheckedVectorDrawableSetup = true; 753 final Drawable d = getDrawable(context, R.drawable.abc_vector_test); 754 if (d == null || !isVectorDrawable(d)) { 755 mHasCheckedVectorDrawableSetup = false; 756 throw new IllegalStateException("This app has been built with an incorrect " 757 + "configuration. Please configure your build for VectorDrawableCompat."); 758 } 759 } 760 isVectorDrawable(@onNull Drawable d)761 private static boolean isVectorDrawable(@NonNull Drawable d) { 762 return d instanceof VectorDrawableCompat 763 || PLATFORM_VD_CLAZZ.equals(d.getClass().getName()); 764 } 765 766 private static class VdcInflateDelegate implements InflateDelegate { VdcInflateDelegate()767 VdcInflateDelegate() { 768 } 769 770 @Override createFromXmlInner(@onNull Context context, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)771 public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser, 772 @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) { 773 try { 774 return VectorDrawableCompat 775 .createFromXmlInner(context.getResources(), parser, attrs, theme); 776 } catch (Exception e) { 777 Log.e("VdcInflateDelegate", "Exception while inflating <vector>", e); 778 return null; 779 } 780 } 781 } 782 783 private static class AvdcInflateDelegate implements InflateDelegate { AvdcInflateDelegate()784 AvdcInflateDelegate() { 785 } 786 787 @Override createFromXmlInner(@onNull Context context, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)788 public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser, 789 @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) { 790 try { 791 return AnimatedVectorDrawableCompat 792 .createFromXmlInner(context, context.getResources(), parser, attrs, theme); 793 } catch (Exception e) { 794 Log.e("AvdcInflateDelegate", "Exception while inflating <animated-vector>", e); 795 return null; 796 } 797 } 798 } 799 } 800