| /* |
| * 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 androidx.appcompat.widget; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; |
| import static androidx.appcompat.content.res.AppCompatResources.getColorStateList; |
| import static androidx.appcompat.widget.ThemeUtils.getDisabledThemeAttrColor; |
| import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColor; |
| import static androidx.appcompat.widget.ThemeUtils.getThemeAttrColorStateList; |
| import static androidx.core.graphics.ColorUtils.compositeColors; |
| |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.Shader; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.LayerDrawable; |
| import android.util.Log; |
| |
| import androidx.annotation.ColorInt; |
| import androidx.annotation.DimenRes; |
| import androidx.annotation.DrawableRes; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.RestrictTo; |
| import androidx.appcompat.R; |
| import androidx.core.graphics.drawable.DrawableCompat; |
| |
| /** |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| public final class AppCompatDrawableManager { |
| private static final String TAG = "AppCompatDrawableManag"; |
| private static final boolean DEBUG = false; |
| private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; |
| |
| private static AppCompatDrawableManager INSTANCE; |
| |
| public static synchronized void preload() { |
| if (INSTANCE == null) { |
| INSTANCE = new AppCompatDrawableManager(); |
| INSTANCE.mResourceManager = ResourceManagerInternal.get(); |
| INSTANCE.mResourceManager.setHooks(new ResourceManagerInternal.ResourceManagerHooks() { |
| /** |
| * Drawables which should be tinted with the value of |
| * {@code R.attr.colorControlNormal}, using the default mode using a raw color |
| * filter. |
| */ |
| private 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 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 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, |
| R.drawable.abc_text_select_handle_middle_mtrl, |
| R.drawable.abc_text_select_handle_right_mtrl |
| }; |
| |
| /** |
| * 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 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 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 final int[] TINT_CHECKABLE_BUTTON_LIST = { |
| R.drawable.abc_btn_check_material, |
| R.drawable.abc_btn_radio_material, |
| R.drawable.abc_btn_check_material_anim, |
| R.drawable.abc_btn_radio_material_anim |
| }; |
| |
| private ColorStateList createDefaultButtonColorStateList(@NonNull Context context) { |
| return createButtonColorStateList(context, |
| getThemeAttrColor(context, R.attr.colorButtonNormal)); |
| } |
| |
| private ColorStateList createBorderlessButtonColorStateList( |
| @NonNull Context context) { |
| // We ignore the custom tint for borderless buttons |
| return createButtonColorStateList(context, Color.TRANSPARENT); |
| } |
| |
| private ColorStateList createColoredButtonColorStateList( |
| @NonNull Context context) { |
| return createButtonColorStateList(context, |
| getThemeAttrColor(context, R.attr.colorAccent)); |
| } |
| |
| private ColorStateList createButtonColorStateList(@NonNull final Context context, |
| @ColorInt final int baseColor) { |
| 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] = disabledColor; |
| i++; |
| |
| states[i] = ThemeUtils.PRESSED_STATE_SET; |
| colors[i] = compositeColors(colorControlHighlight, baseColor); |
| i++; |
| |
| states[i] = ThemeUtils.FOCUSED_STATE_SET; |
| colors[i] = compositeColors(colorControlHighlight, baseColor); |
| i++; |
| |
| // Default enabled state |
| states[i] = ThemeUtils.EMPTY_STATE_SET; |
| colors[i] = baseColor; |
| i++; |
| |
| return new ColorStateList(states, colors); |
| } |
| |
| private ColorStateList createSwitchThumbColorStateList(Context context) { |
| final int[][] states = new int[3][]; |
| final int[] colors = new int[3]; |
| int i = 0; |
| |
| final ColorStateList thumbColor = getThemeAttrColorStateList(context, |
| R.attr.colorSwitchThumbNormal); |
| |
| if (thumbColor != null && thumbColor.isStateful()) { |
| // If colorSwitchThumbNormal is a valid ColorStateList, extract the |
| // default and disabled colors from it |
| |
| // Disabled state |
| states[i] = ThemeUtils.DISABLED_STATE_SET; |
| colors[i] = thumbColor.getColorForState(states[i], 0); |
| i++; |
| |
| states[i] = ThemeUtils.CHECKED_STATE_SET; |
| colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated); |
| i++; |
| |
| // Default enabled state |
| states[i] = ThemeUtils.EMPTY_STATE_SET; |
| colors[i] = thumbColor.getDefaultColor(); |
| i++; |
| } else { |
| // Else we'll use an approximation using the default disabled alpha |
| |
| // Disabled state |
| states[i] = ThemeUtils.DISABLED_STATE_SET; |
| colors[i] = getDisabledThemeAttrColor(context, |
| R.attr.colorSwitchThumbNormal); |
| i++; |
| |
| states[i] = ThemeUtils.CHECKED_STATE_SET; |
| colors[i] = getThemeAttrColor(context, R.attr.colorControlActivated); |
| i++; |
| |
| // Default enabled state |
| states[i] = ThemeUtils.EMPTY_STATE_SET; |
| colors[i] = getThemeAttrColor(context, R.attr.colorSwitchThumbNormal); |
| i++; |
| } |
| |
| return new ColorStateList(states, colors); |
| } |
| |
| @Override |
| public Drawable createDrawableFor(@NonNull ResourceManagerInternal resourceManager, |
| @NonNull Context context, int resId) { |
| if (resId == R.drawable.abc_cab_background_top_material) { |
| return new LayerDrawable(new Drawable[]{ |
| resourceManager.getDrawable(context, |
| R.drawable.abc_cab_background_internal_bg), |
| resourceManager.getDrawable(context, |
| R.drawable.abc_cab_background_top_mtrl_alpha) |
| }); |
| } |
| if (resId == R.drawable.abc_ratingbar_material) { |
| return getRatingBarLayerDrawable(resourceManager, context, |
| R.dimen.abc_star_big); |
| } |
| if (resId == R.drawable.abc_ratingbar_indicator_material) { |
| return getRatingBarLayerDrawable(resourceManager, context, |
| R.dimen.abc_star_medium); |
| } |
| if (resId == R.drawable.abc_ratingbar_small_material) { |
| return getRatingBarLayerDrawable(resourceManager, context, |
| R.dimen.abc_star_small); |
| } |
| return null; |
| } |
| |
| private LayerDrawable getRatingBarLayerDrawable( |
| @NonNull ResourceManagerInternal resourceManager, |
| @NonNull Context context, @DimenRes int dimenResId) { |
| int starSize = context.getResources().getDimensionPixelSize(dimenResId); |
| |
| Drawable star = resourceManager.getDrawable(context, |
| R.drawable.abc_star_black_48dp); |
| Drawable halfStar = resourceManager.getDrawable(context, |
| R.drawable.abc_star_half_black_48dp); |
| |
| BitmapDrawable starBitmapDrawable; |
| BitmapDrawable tiledStarBitmapDrawable; |
| if ((star instanceof BitmapDrawable) && (star.getIntrinsicWidth() == starSize) |
| && (star.getIntrinsicHeight() == starSize)) { |
| // no need for extra conversion |
| starBitmapDrawable = (BitmapDrawable) star; |
| |
| tiledStarBitmapDrawable = |
| new BitmapDrawable(starBitmapDrawable.getBitmap()); |
| } else { |
| Bitmap bitmapStar = Bitmap.createBitmap(starSize, starSize, |
| Bitmap.Config.ARGB_8888); |
| Canvas canvasStar = new Canvas(bitmapStar); |
| star.setBounds(0, 0, starSize, starSize); |
| star.draw(canvasStar); |
| starBitmapDrawable = new BitmapDrawable(bitmapStar); |
| |
| tiledStarBitmapDrawable = new BitmapDrawable(bitmapStar); |
| } |
| tiledStarBitmapDrawable.setTileModeX(Shader.TileMode.REPEAT); |
| |
| BitmapDrawable halfStarBitmapDrawable; |
| if ((halfStar instanceof BitmapDrawable) |
| && (halfStar.getIntrinsicWidth() == starSize) |
| && (halfStar.getIntrinsicHeight() == starSize)) { |
| // no need for extra conversion |
| halfStarBitmapDrawable = (BitmapDrawable) halfStar; |
| } else { |
| Bitmap bitmapHalfStar = Bitmap.createBitmap(starSize, starSize, |
| Bitmap.Config.ARGB_8888); |
| Canvas canvasHalfStar = new Canvas(bitmapHalfStar); |
| halfStar.setBounds(0, 0, starSize, starSize); |
| halfStar.draw(canvasHalfStar); |
| halfStarBitmapDrawable = new BitmapDrawable(bitmapHalfStar); |
| } |
| |
| LayerDrawable result = new LayerDrawable(new Drawable[]{ |
| starBitmapDrawable, halfStarBitmapDrawable, tiledStarBitmapDrawable |
| }); |
| result.setId(0, android.R.id.background); |
| result.setId(1, android.R.id.secondaryProgress); |
| result.setId(2, android.R.id.progress); |
| return result; |
| } |
| |
| private 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)); |
| } |
| |
| @Override |
| public boolean tintDrawable(@NonNull Context context, int resId, |
| @NonNull Drawable drawable) { |
| 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); |
| return true; |
| } 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); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean arrayContains(int[] array, int value) { |
| for (int id : array) { |
| if (id == value) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public ColorStateList getTintListForDrawableRes(@NonNull Context context, |
| int resId) { |
| // ...if the cache did not contain a color state list, try and create one |
| if (resId == R.drawable.abc_edit_text_material) { |
| return getColorStateList(context, R.color.abc_tint_edittext); |
| } else if (resId == R.drawable.abc_switch_track_mtrl_alpha) { |
| return getColorStateList(context, R.color.abc_tint_switch_track); |
| } else if (resId == R.drawable.abc_switch_thumb_material) { |
| return createSwitchThumbColorStateList(context); |
| } else if (resId == R.drawable.abc_btn_default_mtrl_shape) { |
| return createDefaultButtonColorStateList(context); |
| } else if (resId == R.drawable.abc_btn_borderless_material) { |
| return createBorderlessButtonColorStateList(context); |
| } else if (resId == R.drawable.abc_btn_colored_material) { |
| return createColoredButtonColorStateList(context); |
| } else if (resId == R.drawable.abc_spinner_mtrl_am_alpha |
| || resId == R.drawable.abc_spinner_textfield_background_material) { |
| return getColorStateList(context, R.color.abc_tint_spinner); |
| } else if (arrayContains(TINT_COLOR_CONTROL_NORMAL, resId)) { |
| return getThemeAttrColorStateList(context, R.attr.colorControlNormal); |
| } else if (arrayContains(TINT_COLOR_CONTROL_STATE_LIST, resId)) { |
| return getColorStateList(context, R.color.abc_tint_default); |
| } else if (arrayContains(TINT_CHECKABLE_BUTTON_LIST, resId)) { |
| return getColorStateList(context, R.color.abc_tint_btn_checkable); |
| } else if (resId == R.drawable.abc_seekbar_thumb_material) { |
| return getColorStateList(context, R.color.abc_tint_seek_thumb); |
| } |
| return null; |
| } |
| |
| @Override |
| public boolean tintDrawableUsingColorFilter(@NonNull Context context, |
| 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; |
| } |
| |
| @Override |
| public PorterDuff.Mode getTintModeForDrawableRes(int resId) { |
| PorterDuff.Mode mode = null; |
| |
| if (resId == R.drawable.abc_switch_thumb_material) { |
| mode = PorterDuff.Mode.MULTIPLY; |
| } |
| |
| return mode; |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Returns the singleton instance of this class. |
| */ |
| public static synchronized AppCompatDrawableManager get() { |
| if (INSTANCE == null) { |
| preload(); |
| } |
| return INSTANCE; |
| } |
| |
| private ResourceManagerInternal mResourceManager; |
| |
| public synchronized Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) { |
| return mResourceManager.getDrawable(context, resId); |
| } |
| |
| synchronized Drawable getDrawable(@NonNull Context context, @DrawableRes int resId, |
| boolean failIfNotKnown) { |
| return mResourceManager.getDrawable(context, resId, failIfNotKnown); |
| } |
| |
| public synchronized void onConfigurationChanged(@NonNull Context context) { |
| mResourceManager.onConfigurationChanged(context); |
| } |
| |
| synchronized Drawable onDrawableLoadedFromResources(@NonNull Context context, |
| @NonNull VectorEnabledTintResources resources, @DrawableRes final int resId) { |
| return mResourceManager.onDrawableLoadedFromResources(context, resources, resId); |
| } |
| |
| boolean tintDrawableUsingColorFilter(@NonNull Context context, |
| @DrawableRes final int resId, @NonNull Drawable drawable) { |
| return mResourceManager.tintDrawableUsingColorFilter(context, resId, drawable); |
| } |
| |
| synchronized ColorStateList getTintList(@NonNull Context context, @DrawableRes int resId) { |
| return mResourceManager.getTintList(context, resId); |
| } |
| |
| static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) { |
| ResourceManagerInternal.tintDrawable(drawable, tint, state); |
| } |
| |
| public static synchronized PorterDuffColorFilter getPorterDuffColorFilter( |
| int color, PorterDuff.Mode mode) { |
| return ResourceManagerInternal.getPorterDuffColorFilter(color, mode); |
| } |
| } |