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