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