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