AppCompatDrawableManager.java

/*
 * 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.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.Log;

import androidx.annotation.ColorInt;
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_dark,
                        R.drawable.abc_text_select_handle_middle_mtrl_dark,
                        R.drawable.abc_text_select_handle_right_mtrl_dark,
                        R.drawable.abc_text_select_handle_left_mtrl_light,
                        R.drawable.abc_text_select_handle_middle_mtrl_light,
                        R.drawable.abc_text_select_handle_right_mtrl_light
                };

                /**
                 * 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)
                        });
                    }
                    return null;
                }

                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);
    }
}