| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.support.v7.widget; |
| |
| import static android.support.v4.graphics.ColorUtils.compositeColors; |
| import static android.support.v7.content.res.AppCompatResources.getColorStateList; |
| import static android.support.v7.widget.ThemeUtils.getDisabledThemeAttrColor; |
| import static android.support.v7.widget.ThemeUtils.getThemeAttrColor; |
| import static android.support.v7.widget.ThemeUtils.getThemeAttrColorStateList; |
| |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.graphics.Color; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.Drawable.ConstantState; |
| import android.graphics.drawable.LayerDrawable; |
| import android.os.Build; |
| import android.support.annotation.ColorInt; |
| import android.support.annotation.DrawableRes; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.graphics.drawable.AnimatedVectorDrawableCompat; |
| import android.support.graphics.drawable.VectorDrawableCompat; |
| import android.support.v4.content.ContextCompat; |
| import android.support.v4.graphics.drawable.DrawableCompat; |
| import android.support.v4.util.ArrayMap; |
| import android.support.v4.util.LongSparseArray; |
| import android.support.v4.util.LruCache; |
| import android.support.v7.appcompat.R; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.util.TypedValue; |
| import android.util.Xml; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.WeakHashMap; |
| |
| /** |
| * @hide |
| */ |
| public final class AppCompatDrawableManager { |
| |
| private interface InflateDelegate { |
| Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser, |
| @NonNull AttributeSet attrs, @Nullable Resources.Theme theme); |
| } |
| |
| private static final String TAG = "AppCompatDrawableManager"; |
| private static final boolean DEBUG = false; |
| private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; |
| private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip"; |
| |
| private static final String PLATFORM_VD_CLAZZ = "android.graphics.drawable.VectorDrawable"; |
| |
| private static AppCompatDrawableManager INSTANCE; |
| |
| public static AppCompatDrawableManager get() { |
| if (INSTANCE == null) { |
| INSTANCE = new AppCompatDrawableManager(); |
| installDefaultInflateDelegates(INSTANCE); |
| } |
| return INSTANCE; |
| } |
| |
| private static void installDefaultInflateDelegates(@NonNull AppCompatDrawableManager manager) { |
| final int sdk = Build.VERSION.SDK_INT; |
| if (sdk < 23) { |
| // We only want to use the automatic VectorDrawableCompat handling where it's |
| // needed: on devices running before Lollipop |
| manager.addDelegate("vector", new VdcInflateDelegate()); |
| |
| if (sdk >= 11) { |
| // AnimatedVectorDrawableCompat only works on API v11+ |
| manager.addDelegate("animated-vector", new AvdcInflateDelegate()); |
| } |
| } |
| } |
| |
| private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6); |
| |
| /** |
| * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, |
| * using the default mode using a raw color filter. |
| */ |
| private static final int[] COLORFILTER_TINT_COLOR_CONTROL_NORMAL = { |
| R.drawable.abc_textfield_search_default_mtrl_alpha, |
| R.drawable.abc_textfield_default_mtrl_alpha, |
| R.drawable.abc_ab_share_pack_mtrl_alpha |
| }; |
| |
| /** |
| * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal}, using |
| * {@link DrawableCompat}'s tinting functionality. |
| */ |
| private static final int[] TINT_COLOR_CONTROL_NORMAL = { |
| R.drawable.abc_ic_commit_search_api_mtrl_alpha, |
| R.drawable.abc_seekbar_tick_mark_material, |
| R.drawable.abc_ic_menu_share_mtrl_alpha, |
| R.drawable.abc_ic_menu_copy_mtrl_am_alpha, |
| R.drawable.abc_ic_menu_cut_mtrl_alpha, |
| R.drawable.abc_ic_menu_selectall_mtrl_alpha, |
| R.drawable.abc_ic_menu_paste_mtrl_am_alpha |
| }; |
| |
| /** |
| * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated}, |
| * using a color filter. |
| */ |
| private static final int[] COLORFILTER_COLOR_CONTROL_ACTIVATED = { |
| R.drawable.abc_textfield_activated_mtrl_alpha, |
| R.drawable.abc_textfield_search_activated_mtrl_alpha, |
| R.drawable.abc_cab_background_top_mtrl_alpha, |
| R.drawable.abc_text_cursor_material, |
| R.drawable.abc_text_select_handle_left_mtrl_alpha, |
| R.drawable.abc_text_select_handle_middle_mtrl_alpha, |
| R.drawable.abc_text_select_handle_right_mtrl_alpha |
| }; |
| |
| /** |
| * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground}, |
| * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode and a color filter. |
| */ |
| private static final int[] COLORFILTER_COLOR_BACKGROUND_MULTIPLY = { |
| R.drawable.abc_popup_background_mtrl_mult, |
| R.drawable.abc_cab_background_internal_bg, |
| R.drawable.abc_menu_hardkey_panel_mtrl_mult |
| }; |
| |
| /** |
| * Drawables which should be tinted using a state list containing values of |
| * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} |
| */ |
| private static final int[] TINT_COLOR_CONTROL_STATE_LIST = { |
| R.drawable.abc_tab_indicator_material, |
| R.drawable.abc_textfield_search_material |
| }; |
| |
| /** |
| * Drawables which should be tinted using a state list containing values of |
| * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated} for the checked |
| * state. |
| */ |
| private static final int[] TINT_CHECKABLE_BUTTON_LIST = { |
| R.drawable.abc_btn_check_material, |
| R.drawable.abc_btn_radio_material |
| }; |
| |
| private WeakHashMap<Context, SparseArray<ColorStateList>> mTintLists; |
| private ArrayMap<String, InflateDelegate> mDelegates; |
| private SparseArray<String> mKnownDrawableIdTags; |
| |
| private final Object mDrawableCacheLock = new Object(); |
| private final WeakHashMap<Context, LongSparseArray<WeakReference<Drawable.ConstantState>>> |
| mDrawableCaches = new WeakHashMap<>(0); |
| |
| private TypedValue mTypedValue; |
| |
| private boolean mHasCheckedVectorDrawableSetup; |
| |
| public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) { |
| return getDrawable(context, resId, false); |
| } |
| |
| Drawable getDrawable(@NonNull Context context, @DrawableRes int resId, |
| boolean failIfNotKnown) { |
| checkVectorDrawableSetup(context); |
| |
| Drawable drawable = loadDrawableFromDelegates(context, resId); |
| if (drawable == null) { |
| drawable = createDrawableIfNeeded(context, resId); |
| } |
| if (drawable == null) { |
| drawable = ContextCompat.getDrawable(context, resId); |
| } |
| |
| if (drawable != null) { |
| // Tint it if needed |
| drawable = tintDrawable(context, resId, failIfNotKnown, drawable); |
| } |
| if (drawable != null) { |
| // See if we need to 'fix' the drawable |
| DrawableUtils.fixDrawable(drawable); |
| } |
| return drawable; |
| } |
| |
| private static long createCacheKey(TypedValue tv) { |
| return (((long) tv.assetCookie) << 32) | tv.data; |
| } |
| |
| private Drawable createDrawableIfNeeded(@NonNull Context context, |
| @DrawableRes final int resId) { |
| if (mTypedValue == null) { |
| mTypedValue = new TypedValue(); |
| } |
| final TypedValue tv = mTypedValue; |
| context.getResources().getValue(resId, tv, true); |
| final long key = createCacheKey(tv); |
| |
| Drawable dr = getCachedDrawable(context, key); |
| if (dr != null) { |
| // If we got a cached drawable, return it |
| return dr; |
| } |
| |
| // Else we need to try and create one... |
| if (resId == R.drawable.abc_cab_background_top_material) { |
| dr = new LayerDrawable(new Drawable[]{ |
| getDrawable(context, R.drawable.abc_cab_background_internal_bg), |
| getDrawable(context, R.drawable.abc_cab_background_top_mtrl_alpha) |
| }); |
| } |
| |
| if (dr != null) { |
| dr.setChangingConfigurations(tv.changingConfigurations); |
| // If we reached here then we created a new drawable, add it to the cache |
| addDrawableToCache(context, key, dr); |
| } |
| |
| return dr; |
| } |
| |
| private Drawable tintDrawable(@NonNull Context context, @DrawableRes int resId, |
| boolean failIfNotKnown, @NonNull Drawable drawable) { |
| final ColorStateList tintList = getTintList(context, resId); |
| if (tintList != null) { |
| // First mutate the Drawable, then wrap it and set the tint list |
| if (DrawableUtils.canSafelyMutateDrawable(drawable)) { |
| drawable = drawable.mutate(); |
| } |
| drawable = DrawableCompat.wrap(drawable); |
| DrawableCompat.setTintList(drawable, tintList); |
| |
| // If there is a blending mode specified for the drawable, use it |
| final PorterDuff.Mode tintMode = getTintMode(resId); |
| if (tintMode != null) { |
| DrawableCompat.setTintMode(drawable, tintMode); |
| } |
| } else if (resId == R.drawable.abc_seekbar_track_material) { |
| LayerDrawable ld = (LayerDrawable) drawable; |
| setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background), |
| getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE); |
| setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress), |
| getThemeAttrColor(context, R.attr.colorControlNormal), DEFAULT_MODE); |
| setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress), |
| getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE); |
| } else if (resId == R.drawable.abc_ratingbar_material |
| || resId == R.drawable.abc_ratingbar_indicator_material |
| || resId == R.drawable.abc_ratingbar_small_material) { |
| LayerDrawable ld = (LayerDrawable) drawable; |
| setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.background), |
| getDisabledThemeAttrColor(context, R.attr.colorControlNormal), |
| DEFAULT_MODE); |
| setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.secondaryProgress), |
| getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE); |
| setPorterDuffColorFilter(ld.findDrawableByLayerId(android.R.id.progress), |
| getThemeAttrColor(context, R.attr.colorControlActivated), DEFAULT_MODE); |
| } else { |
| final boolean tinted = tintDrawableUsingColorFilter(context, resId, drawable); |
| if (!tinted && failIfNotKnown) { |
| // If we didn't tint using a ColorFilter, and we're set to fail if we don't |
| // know the id, return null |
| drawable = null; |
| } |
| } |
| return drawable; |
| } |
| |
| private Drawable loadDrawableFromDelegates(@NonNull Context context, @DrawableRes int resId) { |
| if (mDelegates != null && !mDelegates.isEmpty()) { |
| if (mKnownDrawableIdTags != null) { |
| final String cachedTagName = mKnownDrawableIdTags.get(resId); |
| if (SKIP_DRAWABLE_TAG.equals(cachedTagName) |
| || (cachedTagName != null && mDelegates.get(cachedTagName) == null)) { |
| // If we don't have a delegate for the drawable tag, or we've been set to |
| // skip it, fail fast and return null |
| if (DEBUG) { |
| Log.d(TAG, "[loadDrawableFromDelegates] Skipping drawable: " |
| + context.getResources().getResourceName(resId)); |
| } |
| return null; |
| } |
| } else { |
| // Create an id cache as we'll need one later |
| mKnownDrawableIdTags = new SparseArray<>(); |
| } |
| |
| if (mTypedValue == null) { |
| mTypedValue = new TypedValue(); |
| } |
| final TypedValue tv = mTypedValue; |
| final Resources res = context.getResources(); |
| res.getValue(resId, tv, true); |
| |
| final long key = createCacheKey(tv); |
| |
| Drawable dr = getCachedDrawable(context, key); |
| if (dr != null) { |
| if (DEBUG) { |
| Log.i(TAG, "[loadDrawableFromDelegates] Returning cached drawable: " + |
| context.getResources().getResourceName(resId)); |
| } |
| // We have a cached drawable, return it! |
| return dr; |
| } |
| |
| if (tv.string != null && tv.string.toString().endsWith(".xml")) { |
| // If the resource is an XML file, let's try and parse it |
| try { |
| final XmlPullParser parser = res.getXml(resId); |
| final AttributeSet attrs = Xml.asAttributeSet(parser); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.START_TAG && |
| type != XmlPullParser.END_DOCUMENT) { |
| // Empty loop |
| } |
| if (type != XmlPullParser.START_TAG) { |
| throw new XmlPullParserException("No start tag found"); |
| } |
| |
| final String tagName = parser.getName(); |
| // Add the tag name to the cache |
| mKnownDrawableIdTags.append(resId, tagName); |
| |
| // Now try and find a delegate for the tag name and inflate if found |
| final InflateDelegate delegate = mDelegates.get(tagName); |
| if (delegate != null) { |
| dr = delegate.createFromXmlInner(context, parser, attrs, |
| context.getTheme()); |
| } |
| if (dr != null) { |
| // Add it to the drawable cache |
| dr.setChangingConfigurations(tv.changingConfigurations); |
| if (addDrawableToCache(context, key, dr) && DEBUG) { |
| Log.i(TAG, "[loadDrawableFromDelegates] Saved drawable to cache: " + |
| context.getResources().getResourceName(resId)); |
| } |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Exception while inflating drawable", e); |
| } |
| } |
| if (dr == null) { |
| // If we reach here then the delegate inflation of the resource failed. Mark it as |
| // bad so we skip the id next time |
| mKnownDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG); |
| } |
| return dr; |
| } |
| |
| return null; |
| } |
| |
| private Drawable getCachedDrawable(@NonNull final Context context, final long key) { |
| synchronized (mDrawableCacheLock) { |
| final LongSparseArray<WeakReference<ConstantState>> cache |
| = mDrawableCaches.get(context); |
| if (cache == null) { |
| return null; |
| } |
| |
| final WeakReference<ConstantState> wr = cache.get(key); |
| if (wr != null) { |
| // We have the key, and the secret |
| ConstantState entry = wr.get(); |
| if (entry != null) { |
| return entry.newDrawable(context.getResources()); |
| } else { |
| // Our entry has been purged |
| cache.delete(key); |
| } |
| } |
| } |
| return null; |
| } |
| |
| private boolean addDrawableToCache(@NonNull final Context context, final long key, |
| @NonNull final Drawable drawable) { |
| final ConstantState cs = drawable.getConstantState(); |
| if (cs != null) { |
| synchronized (mDrawableCacheLock) { |
| LongSparseArray<WeakReference<ConstantState>> cache = mDrawableCaches.get(context); |
| if (cache == null) { |
| cache = new LongSparseArray<>(); |
| mDrawableCaches.put(context, cache); |
| } |
| cache.put(key, new WeakReference<ConstantState>(cs)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| Drawable onDrawableLoadedFromResources(@NonNull Context context, |
| @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId) { |
| Drawable drawable = loadDrawableFromDelegates(context, resId); |
| if (drawable == null) { |
| drawable = resources.superGetDrawable(resId); |
| } |
| if (drawable != null) { |
| return tintDrawable(context, resId, false, drawable); |
| } |
| return null; |
| } |
| |
| static boolean tintDrawableUsingColorFilter(@NonNull Context context, |
| @DrawableRes final int resId, @NonNull Drawable drawable) { |
| PorterDuff.Mode tintMode = DEFAULT_MODE; |
| boolean colorAttrSet = false; |
| int colorAttr = 0; |
| int alpha = -1; |
| |
| if (arrayContains(COLORFILTER_TINT_COLOR_CONTROL_NORMAL, resId)) { |
| colorAttr = R.attr.colorControlNormal; |
| colorAttrSet = true; |
| } else if (arrayContains(COLORFILTER_COLOR_CONTROL_ACTIVATED, resId)) { |
| colorAttr = R.attr.colorControlActivated; |
| colorAttrSet = true; |
| } else if (arrayContains(COLORFILTER_COLOR_BACKGROUND_MULTIPLY, resId)) { |
| colorAttr = android.R.attr.colorBackground; |
| colorAttrSet = true; |
| tintMode = PorterDuff.Mode.MULTIPLY; |
| } else if (resId == R.drawable.abc_list_divider_mtrl_alpha) { |
| colorAttr = android.R.attr.colorForeground; |
| colorAttrSet = true; |
| alpha = Math.round(0.16f * 255); |
| } else if (resId == R.drawable.abc_dialog_material_background) { |
| colorAttr = android.R.attr.colorBackground; |
| colorAttrSet = true; |
| } |
| |
| if (colorAttrSet) { |
| if (DrawableUtils.canSafelyMutateDrawable(drawable)) { |
| drawable = drawable.mutate(); |
| } |
| |
| final int color = getThemeAttrColor(context, colorAttr); |
| drawable.setColorFilter(getPorterDuffColorFilter(color, tintMode)); |
| |
| if (alpha != -1) { |
| drawable.setAlpha(alpha); |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, "[tintDrawableUsingColorFilter] Tinted " |
| + context.getResources().getResourceName(resId) + |
| " with color: #" + Integer.toHexString(color)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private void addDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) { |
| if (mDelegates == null) { |
| mDelegates = new ArrayMap<>(); |
| } |
| mDelegates.put(tagName, delegate); |
| } |
| |
| private void removeDelegate(@NonNull String tagName, @NonNull InflateDelegate delegate) { |
| if (mDelegates != null && mDelegates.get(tagName) == delegate) { |
| mDelegates.remove(tagName); |
| } |
| } |
| |
| private static boolean arrayContains(int[] array, int value) { |
| for (int id : array) { |
| if (id == value) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static PorterDuff.Mode getTintMode(final int resId) { |
| PorterDuff.Mode mode = null; |
| |
| if (resId == R.drawable.abc_switch_thumb_material) { |
| mode = PorterDuff.Mode.MULTIPLY; |
| } |
| |
| return mode; |
| } |
| |
| ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) { |
| return getTintList(context, resId, null); |
| } |
| |
| ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId, |
| @Nullable ColorStateList customTint) { |
| // We only want to use the cache for the standard tints, not ones created using custom |
| // tints |
| final boolean useCache = customTint == null; |
| |
| // Try the cache first (if it exists) |
| ColorStateList tint = useCache ? getTintListFromCache(context, resId) : null; |
| |
| if (tint == null) { |
| // ...if the cache did not contain a color state list, try and create one |
| if (resId == R.drawable.abc_edit_text_material) { |
| tint = getColorStateList(context, R.color.abc_tint_edittext); |
| } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) { |
| tint = getColorStateList(context, R.color.abc_tint_switch_track); |
| } else if (resId == R.drawable.abc_switch_thumb_material) { |
| tint = getColorStateList(context, R.color.abc_tint_switch_thumb); |
| } else if (resId == R.drawable.abc_btn_default_mtrl_shape) { |
| tint = createDefaultButtonColorStateList(context, customTint); |
| } else if (resId == R.drawable.abc_btn_borderless_material) { |
| tint = createBorderlessButtonColorStateList(context, customTint); |
| } else if (resId == R.drawable.abc_btn_colored_material) { |
| tint = createColoredButtonColorStateList(context, customTint); |
| } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha |
| || resId == R.drawable.abc_spinner_textfield_background_material) { |
| tint = getColorStateList(context, R.color.abc_tint_spinner); |
| } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) { |
| tint = getThemeAttrColorStateList(context, R.attr.colorControlNormal); |
| } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) { |
| tint = getColorStateList(context, R.color.abc_tint_default); |
| } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) { |
| tint = getColorStateList(context, R.color.abc_tint_btn_checkable); |
| } else if (resId == R.drawable.abc_seekbar_thumb_material) { |
| tint = getColorStateList(context, R.color.abc_tint_seek_thumb); |
| } |
| |
| if (useCache && tint != null) { |
| addTintListToCache(context, resId, tint); |
| } |
| } |
| return tint; |
| } |
| |
| private ColorStateList getTintListFromCache(@NonNull Context context, @DrawableRes int resId) { |
| if (mTintLists != null) { |
| final SparseArray<ColorStateList> tints = mTintLists.get(context); |
| return tints != null ? tints.get(resId) : null; |
| } |
| return null; |
| } |
| |
| private void addTintListToCache(@NonNull Context context, @DrawableRes int resId, |
| @NonNull ColorStateList tintList) { |
| if (mTintLists == null) { |
| mTintLists = new WeakHashMap<>(); |
| } |
| SparseArray<ColorStateList> themeTints = mTintLists.get(context); |
| if (themeTints == null) { |
| themeTints = new SparseArray<>(); |
| mTintLists.put(context, themeTints); |
| } |
| themeTints.append(resId, tintList); |
| } |
| |
| private ColorStateList createDefaultButtonColorStateList(@NonNull Context context, |
| @Nullable ColorStateList customTint) { |
| return createButtonColorStateList(context, |
| getThemeAttrColor(context, R.attr.colorButtonNormal), customTint); |
| } |
| |
| private ColorStateList createBorderlessButtonColorStateList(@NonNull Context context, |
| @Nullable ColorStateList customTint) { |
| // We ignore the custom tint for borderless buttons |
| return createButtonColorStateList(context, Color.TRANSPARENT, null); |
| } |
| |
| private ColorStateList createColoredButtonColorStateList(@NonNull Context context, |
| @Nullable ColorStateList customTint) { |
| return createButtonColorStateList(context, |
| getThemeAttrColor(context, R.attr.colorAccent), customTint); |
| } |
| |
| private ColorStateList createButtonColorStateList(@NonNull final Context context, |
| @ColorInt final int baseColor, final @Nullable ColorStateList tint) { |
| final int[][] states = new int[4][]; |
| final int[] colors = new int[4]; |
| int i = 0; |
| |
| final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight); |
| final int disabledColor = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal); |
| |
| // Disabled state |
| states[i] = ThemeUtils.DISABLED_STATE_SET; |
| colors[i] = tint == null ? disabledColor : tint.getColorForState(states[i], 0); |
| i++; |
| |
| states[i] = ThemeUtils.PRESSED_STATE_SET; |
| colors[i] = compositeColors(colorControlHighlight, |
| tint == null ? baseColor : tint.getColorForState(states[i], 0)); |
| i++; |
| |
| states[i] = ThemeUtils.FOCUSED_STATE_SET; |
| colors[i] = compositeColors(colorControlHighlight, |
| tint == null ? baseColor : tint.getColorForState(states[i], 0)); |
| i++; |
| |
| // Default enabled state |
| states[i] = ThemeUtils.EMPTY_STATE_SET; |
| colors[i] = tint == null ? baseColor : tint.getColorForState(states[i], 0); |
| i++; |
| |
| return new ColorStateList(states, colors); |
| } |
| |
| private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> { |
| |
| public ColorFilterLruCache(int maxSize) { |
| super(maxSize); |
| } |
| |
| PorterDuffColorFilter get(int color, PorterDuff.Mode mode) { |
| return get(generateCacheKey(color, mode)); |
| } |
| |
| PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) { |
| return put(generateCacheKey(color, mode), filter); |
| } |
| |
| private static int generateCacheKey(int color, PorterDuff.Mode mode) { |
| int hashCode = 1; |
| hashCode = 31 * hashCode + color; |
| hashCode = 31 * hashCode + mode.hashCode(); |
| return hashCode; |
| } |
| } |
| |
| static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) { |
| if (DrawableUtils.canSafelyMutateDrawable(drawable) |
| && drawable.mutate() != drawable) { |
| Log.d(TAG, "Mutated drawable is not the same instance as the input."); |
| return; |
| } |
| |
| if (tint.mHasTintList || tint.mHasTintMode) { |
| drawable.setColorFilter(createTintFilter( |
| tint.mHasTintList ? tint.mTintList : null, |
| tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE, |
| state)); |
| } else { |
| drawable.clearColorFilter(); |
| } |
| |
| if (Build.VERSION.SDK_INT <= 23) { |
| // Pre-v23 there is no guarantee that a state change will invoke an invalidation, |
| // so we force it ourselves |
| drawable.invalidateSelf(); |
| } |
| } |
| |
| private static PorterDuffColorFilter createTintFilter(ColorStateList tint, |
| PorterDuff.Mode tintMode, final int[] state) { |
| if (tint == null || tintMode == null) { |
| return null; |
| } |
| final int color = tint.getColorForState(state, Color.TRANSPARENT); |
| return getPorterDuffColorFilter(color, tintMode); |
| } |
| |
| public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) { |
| // First, lets see if the cache already contains the color filter |
| PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode); |
| |
| if (filter == null) { |
| // Cache miss, so create a color filter and add it to the cache |
| filter = new PorterDuffColorFilter(color, mode); |
| COLOR_FILTER_CACHE.put(color, mode, filter); |
| } |
| |
| return filter; |
| } |
| |
| private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) { |
| if (DrawableUtils.canSafelyMutateDrawable(d)) { |
| d = d.mutate(); |
| } |
| d.setColorFilter(getPorterDuffColorFilter(color, mode == null ? DEFAULT_MODE : mode)); |
| } |
| |
| private void checkVectorDrawableSetup(@NonNull Context context) { |
| if (mHasCheckedVectorDrawableSetup) { |
| // We've already checked so return now... |
| return; |
| } |
| // Here we will check that a known Vector drawable resource inside AppCompat can be |
| // correctly decoded. We use one that will almost definitely be used in the future to |
| // negate any wasted work |
| mHasCheckedVectorDrawableSetup = true; |
| final Drawable d = getDrawable(context, R.drawable.abc_ic_ab_back_material); |
| if (d == null || !isVectorDrawable(d)) { |
| mHasCheckedVectorDrawableSetup = false; |
| throw new IllegalStateException("This app has been built with an incorrect " |
| + "configuration. Please configure your build for VectorDrawableCompat."); |
| } |
| } |
| |
| private static boolean isVectorDrawable(@NonNull Drawable d) { |
| return d instanceof VectorDrawableCompat |
| || PLATFORM_VD_CLAZZ.equals(d.getClass().getName()); |
| } |
| |
| private static class VdcInflateDelegate implements InflateDelegate { |
| @Override |
| public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser, |
| @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) { |
| try { |
| return VectorDrawableCompat |
| .createFromXmlInner(context.getResources(), parser, attrs, theme); |
| } catch (Exception e) { |
| Log.e("VdcInflateDelegate", "Exception while inflating <vector>", e); |
| return null; |
| } |
| } |
| } |
| |
| private static class AvdcInflateDelegate implements InflateDelegate { |
| @Override |
| public Drawable createFromXmlInner(@NonNull Context context, @NonNull XmlPullParser parser, |
| @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) { |
| try { |
| return AnimatedVectorDrawableCompat |
| .createFromXmlInner(context, context.getResources(), parser, attrs, theme); |
| } catch (Exception e) { |
| Log.e("AvdcInflateDelegate", "Exception while inflating <animated-vector>", e); |
| return null; |
| } |
| } |
| } |
| } |