com.bilibili.magicasakura.utils.ThemeUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.bilibili.magicasakura.utils.ThemeUtils.java

Source

/*
 * Copyright (C) 2016 Bilibili
 *
 * 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 com.bilibili.magicasakura.utils;

import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.NinePatchDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.StyleRes;
import android.support.v4.graphics.ColorUtils;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.view.ContextThemeWrapper;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.StateSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.WrapperListAdapter;

import com.bilibili.magicasakura.R;
import com.bilibili.magicasakura.widgets.Tintable;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;

/**
 * Created by xyczero on 15/9/6.
 * Email : xyczero@sina.com
 */
public class ThemeUtils {
    private static final ThreadLocal<TypedValue> TL_TYPED_VALUE = new ThreadLocal<>();

    public static final int[] DISABLED_STATE_SET = new int[] { -android.R.attr.state_enabled };
    public static final int[] ENABLED_STATE_SET = new int[] { android.R.attr.state_enabled };
    public static final int[] FOCUSED_STATE_SET = new int[] { android.R.attr.state_focused };
    public static final int[] ACTIVATED_STATE_SET = new int[] { android.R.attr.state_activated };
    public static final int[] PRESSED_STATE_SET = new int[] { android.R.attr.state_pressed };
    public static final int[] CHECKED_STATE_SET = new int[] { android.R.attr.state_checked };
    public static final int[] SELECTED_STATE_SET = new int[] { android.R.attr.state_selected };
    public static final int[] EMPTY_STATE_SET = new int[0];

    private static final int[] TEMP_ARRAY = new int[1];

    public static Drawable tintDrawable(Drawable drawable, @ColorInt int color, PorterDuff.Mode mode) {
        if (drawable == null)
            return null;
        Drawable wrapper = DrawableCompat.wrap(drawable.mutate());
        DrawableCompat.setTint(wrapper, color);
        DrawableCompat.setTintMode(drawable, mode);
        return wrapper;
    }

    public static Drawable tintDrawable(Context context, @DrawableRes int resId, @ColorRes int colorId) {
        if (resId <= 0 || colorId <= 0)
            return null;
        Drawable drawable = context.getResources().getDrawable(resId);
        return tintDrawableByColorId(context, drawable, colorId);
    }

    public static Drawable tintDrawable(Drawable drawable, @ColorInt int color) {
        return tintDrawable(drawable, color, PorterDuff.Mode.SRC_IN);
    }

    public static Drawable tintDrawableByDrawableId(Context context, @DrawableRes int resId, @ColorInt int color) {
        if (resId <= 0)
            return null;
        Drawable drawable = context.getResources().getDrawable(resId);
        return tintDrawable(drawable, color);
    }

    public static Drawable tintDrawableByColorId(Context context, Drawable drawable, @ColorRes int colorId) {
        if (drawable == null)
            return null;
        if (colorId <= 0)
            return drawable;
        return tintDrawable(drawable, replaceColor(context, context.getResources().getColor(colorId)));
    }

    public static Drawable tintDrawable(Drawable drawable, ColorStateList cls, PorterDuff.Mode mode) {
        if (drawable == null)
            return null;
        Drawable wrapper = DrawableCompat.wrap(drawable.mutate());
        DrawableCompat.setTintList(wrapper, cls);
        DrawableCompat.setTintMode(drawable, mode);
        return wrapper;
    }

    public static Drawable tintDrawableByColorList(Context context, Drawable drawable, @ColorRes int colorListId,
            PorterDuff.Mode mode) {
        if (drawable == null)
            return null;
        if (colorListId <= 0)
            return drawable;
        return tintDrawable(drawable, TintManager.get(context).getColorStateList(colorListId),
                mode == null ? PorterDuff.Mode.SRC_IN : mode);
    }

    public static @ColorInt int getColorById(Context context, @ColorRes int colorId) {
        return replaceColorById(context, colorId);
    }

    public static @ColorInt int getColor(Context context, @ColorInt int color) {
        return replaceColor(context, color);
    }

    public static @ColorInt int getThemeAttrColor(Context context, @AttrRes int attr) {
        return hasThemeAttr(context, attr) ? replaceColorById(context, getThemeAttrId(context, attr))
                : Color.TRANSPARENT;
    }

    static int getThemeAttrColor(Context context, @AttrRes int attr, float alpha) {
        final int color = getThemeAttrColor(context, attr);
        final int originalAlpha = Color.alpha(color);

        return ColorUtils.setAlphaComponent(color, Math.round(originalAlpha * alpha));
    }

    public static ColorStateList getThemeAttrColorStateList(Context context, @AttrRes int attr) {
        TEMP_ARRAY[0] = attr;
        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
        try {
            return a.getColorStateList(0);
        } finally {
            a.recycle();
        }
    }

    public static int getDisabledThemeAttrColor(Context context, @AttrRes int attr) {
        final ColorStateList csl = getThemeAttrColorStateList(context, attr);
        if (csl != null && csl.isStateful()) {
            // If the CSL is stateful, we'll assume it has a disabled state and use it
            return csl.getColorForState(DISABLED_STATE_SET, csl.getDefaultColor());
        } else {
            // Else, we'll generate the color using disabledAlpha from the theme

            final TypedValue tv = getTypedValue();
            // Now retrieve the disabledAlpha value from the theme
            context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
            final float disabledAlpha = tv.getFloat();

            return getThemeAttrColor(context, attr, disabledAlpha);
        }
    }

    public static int getThemeAttrId(Context context, @AttrRes int attr) {
        TEMP_ARRAY[0] = attr;
        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
        try {
            return a.getResourceId(0, 0);
        } finally {
            a.recycle();
        }
    }

    public static int getThemeAttrId(Context context, AttributeSet attrs, @AttrRes int attr) {
        TEMP_ARRAY[0] = attr;
        TypedArray a = context.obtainStyledAttributes(attrs, TEMP_ARRAY);
        try {
            return a.getResourceId(0, 0);
        } finally {
            a.recycle();
        }
    }

    public static boolean getThemeAttrBoolean(Context context, @AttrRes int attr) {
        TEMP_ARRAY[0] = attr;
        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
        try {
            return a.getBoolean(0, false);
        } finally {
            a.recycle();
        }
    }

    public static boolean hasThemeAttr(Context context, @AttrRes int attr) {
        TEMP_ARRAY[0] = attr;
        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
        try {
            return a.hasValue(0);
        } finally {
            a.recycle();
        }
    }

    public static int getThemeAttrDimensionPixelSize(Context context, @AttrRes int attr) {
        TEMP_ARRAY[0] = attr;
        TypedArray a = context.obtainStyledAttributes(null, TEMP_ARRAY);
        try {
            return a.getDimensionPixelSize(0, 0);
        } finally {
            a.recycle();
        }
    }

    private static TypedValue getTypedValue() {
        TypedValue typedValue = TL_TYPED_VALUE.get();
        if (typedValue == null) {
            typedValue = new TypedValue();
            TL_TYPED_VALUE.set(typedValue);
        }
        return typedValue;
    }

    // skip animated-selector when android version is 5.0.x
    private static boolean isSkipAnimatedSelector = false;
    private static boolean hasRecordedVersion = false;

    public static boolean isSkipAnimatedSelector() {
        if (!hasRecordedVersion) {
            final String sdkVersion = Build.VERSION.RELEASE;
            isSkipAnimatedSelector = !Build.UNKNOWN.equals(sdkVersion) && "5.0".compareTo(sdkVersion) <= 0
                    && "5.1".compareTo(sdkVersion) > 0;
            hasRecordedVersion = true;
        }
        return isSkipAnimatedSelector;
    }

    public static boolean containsNinePatch(Drawable drawable) {
        drawable = getWrapperDrawable(drawable);
        if (drawable instanceof NinePatchDrawable || drawable instanceof InsetDrawable
                || drawable instanceof LayerDrawable) {
            return true;
        } else if (drawable instanceof StateListDrawable) {
            final DrawableContainer.DrawableContainerState containerState = ((DrawableContainer.DrawableContainerState) drawable
                    .getConstantState());
            //can't get containState from drawable which is containing DrawableWrapperDonut
            //https://code.google.com/p/android/issues/detail?id=169920
            if (containerState == null) {
                return true;
            }
            for (Drawable dr : containerState.getChildren()) {
                dr = getWrapperDrawable(dr);
                if (dr instanceof NinePatchDrawable || dr instanceof InsetDrawable || dr instanceof LayerDrawable) {
                    return true;
                }
            }
        }
        return false;
    }

    public static Drawable getWrapperDrawable(Drawable drawable) {
        if (drawable instanceof android.support.v4.graphics.drawable.DrawableWrapper) {
            return ((android.support.v4.graphics.drawable.DrawableWrapper) drawable).getWrappedDrawable();
        } else if (drawable instanceof android.support.v7.graphics.drawable.DrawableWrapper) {
            return ((android.support.v7.graphics.drawable.DrawableWrapper) drawable).getWrappedDrawable();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                && drawable instanceof android.graphics.drawable.DrawableWrapper) {
            return ((android.graphics.drawable.DrawableWrapper) drawable).getDrawable();
        }
        return drawable;
    }

    public static Activity getWrapperActivity(Context context) {
        if (context instanceof Activity) {
            return (Activity) context;
        } else if (context instanceof ContextWrapper) {
            return getWrapperActivity(((ContextWrapper) context).getBaseContext());
        }
        return null;
    }

    public static ContextWrapper getWrapperContext(Context context, @StyleRes int themeId) {
        if (context == null)
            return null;

        return new ContextThemeWrapper(context, themeId);
    }

    public static Resources updateNightMode(Resources resource, boolean on) {
        DisplayMetrics dm = resource.getDisplayMetrics();
        Configuration config = resource.getConfiguration();
        final int uiModeNightMaskOrigin = config.uiMode &= ~Configuration.UI_MODE_TYPE_MASK;
        final int uiModeNightMaskNew = on ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO;
        if (uiModeNightMaskOrigin != uiModeNightMaskNew) {
            config.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
            config.uiMode |= uiModeNightMaskNew;
            resource.updateConfiguration(config, dm);
        }
        return resource;
    }

    static com.bilibili.magicasakura.utils.TintInfo parseColorStateList(ColorStateList origin) {
        if (origin == null)
            return null;

        boolean hasDisable = false;
        int originDefaultColor = origin.getDefaultColor();
        LinkedList<int[]> stateList = new LinkedList<>();
        LinkedList<Integer> colorList = new LinkedList<>();

        int disableColor = origin.getColorForState(DISABLED_STATE_SET, 0);
        if (disableColor != originDefaultColor) {
            hasDisable = true;
            stateList.add(DISABLED_STATE_SET);
            colorList.add(disableColor);
        }

        int pressedColor = origin.getColorForState(wrapState(hasDisable, PRESSED_STATE_SET), 0);
        if (pressedColor != originDefaultColor) {
            stateList.add(PRESSED_STATE_SET);
            colorList.add(pressedColor);
        }

        int focusColor = origin.getColorForState(wrapState(hasDisable, FOCUSED_STATE_SET), 0);
        if (focusColor != originDefaultColor) {
            stateList.add(FOCUSED_STATE_SET);
            colorList.add(focusColor);
        }

        int checkedColor = origin.getColorForState(wrapState(hasDisable, CHECKED_STATE_SET), 0);
        if (checkedColor != originDefaultColor) {
            stateList.add(CHECKED_STATE_SET);
            colorList.add(checkedColor);
        }

        int selectedColor = origin.getColorForState(wrapState(hasDisable, SELECTED_STATE_SET), 0);
        if (selectedColor != originDefaultColor) {
            stateList.add(SELECTED_STATE_SET);
            colorList.add(selectedColor);
        }

        int normalColor = origin.getColorForState(wrapState(hasDisable, EMPTY_STATE_SET), 0);
        if (normalColor != 0) {
            stateList.add(EMPTY_STATE_SET);
            colorList.add(normalColor);
        }

        if (colorList.size() > 1) {
            return new com.bilibili.magicasakura.utils.TintInfo(stateList, colorList);
        } else {
            return null;
        }
    }

    private static int[] wrapState(boolean hasDisable, int[] targetState) {
        return targetState.length > 0
                ? (hasDisable ? new int[] { ENABLED_STATE_SET[0], targetState[0] } : targetState)
                : (hasDisable ? ENABLED_STATE_SET : targetState);
    }

    public static ColorStateList getThemeColorStateList(Context context, ColorStateList origin) {
        if (origin == null)
            return null;

        if (origin.isStateful()) {
            TintInfo tintInfo = parseColorStateList(origin);
            if (tintInfo == null || tintInfo.isInvalid()) {
                return origin;
            }

            int[] newColors;
            int[][] newStates;
            int index = 0;
            boolean hasDisableColor = StateSet.stateSetMatches(tintInfo.mTintStates[0], DISABLED_STATE_SET);
            if (!hasDisableColor) {
                newStates = new int[tintInfo.mTintStates.length + 1][];
                newColors = new int[tintInfo.mTintStates.length + 1];
                newStates[index] = DISABLED_STATE_SET;
                newColors[index] = getDisabledThemeAttrColor(context, R.attr.themeColorSecondary);
                index++;
            } else {
                newStates = new int[tintInfo.mTintStates.length][];
                newColors = new int[tintInfo.mTintStates.length];
            }

            for (int i = 0; i < tintInfo.mTintStates.length; i++) {
                newStates[index] = tintInfo.mTintStates[i];
                newColors[index] = replaceColor(context, tintInfo.mTintColors[i]);
                index++;
            }
            return new ColorStateList(newStates, newColors);
        }
        return ColorStateList.valueOf(replaceColor(context, origin.getDefaultColor()));
    }

    public static ColorStateList getThemeColorStateList(Context context, @ColorRes int colorId) {
        return ColorStateListUtils.createColorStateList(context, colorId);
    }

    public static void refreshUI(Context context) {
        refreshUI(context, null);
    }

    public static void refreshUI(Context context, ExtraRefreshable extraRefreshable) {
        TintManager.clearTintCache();
        Activity activity = getWrapperActivity(context);
        if (activity != null) {
            if (extraRefreshable != null) {
                extraRefreshable.refreshGlobal(activity);
            }
            View rootView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
            refreshView(rootView, extraRefreshable);
        }
    }

    private static Field sRecycler;
    private static Method sRecycleViewClearMethod;
    private static Field sRecyclerBin;
    private static Method sListViewClearMethod;

    private static void refreshView(View view, ExtraRefreshable extraRefreshable) {
        if (view == null)
            return;

        view.destroyDrawingCache();
        if (view instanceof Tintable) {
            ((Tintable) view).tint();
            if (view instanceof ViewGroup) {
                for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
                    refreshView(((ViewGroup) view).getChildAt(i), extraRefreshable);
                }
            }
        } else {
            if (extraRefreshable != null) {
                extraRefreshable.refreshSpecificView(view);
            }
            if (view instanceof AbsListView) {
                try {
                    if (sRecyclerBin == null) {
                        sRecyclerBin = AbsListView.class.getDeclaredField("mRecycler");
                        sRecyclerBin.setAccessible(true);
                    }
                    if (sListViewClearMethod == null) {
                        sListViewClearMethod = Class.forName("android.widget.AbsListView$RecycleBin")
                                .getDeclaredMethod("clear");
                        sListViewClearMethod.setAccessible(true);
                    }
                    sListViewClearMethod.invoke(sRecyclerBin.get(view));
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                ListAdapter adapter = ((AbsListView) view).getAdapter();
                while (adapter instanceof WrapperListAdapter) {
                    adapter = ((WrapperListAdapter) adapter).getWrappedAdapter();
                }
                if (adapter instanceof BaseAdapter) {
                    ((BaseAdapter) adapter).notifyDataSetChanged();
                }
            }
            if (view instanceof RecyclerView) {
                try {
                    if (sRecycler == null) {
                        sRecycler = RecyclerView.class.getDeclaredField("mRecycler");
                        sRecycler.setAccessible(true);
                    }
                    if (sRecycleViewClearMethod == null) {
                        sRecycleViewClearMethod = Class.forName("android.support.v7.widget.RecyclerView$Recycler")
                                .getDeclaredMethod("clear");
                        sRecycleViewClearMethod.setAccessible(true);
                    }
                    sRecycleViewClearMethod.invoke(sRecycler.get(view));
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                ((RecyclerView) view).getRecycledViewPool().clear();
                ((RecyclerView) view).invalidateItemDecorations();
            }
            if (view instanceof ViewGroup) {
                for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
                    refreshView(((ViewGroup) view).getChildAt(i), extraRefreshable);
                }
            }
        }
    }

    public interface ExtraRefreshable {
        void refreshGlobal(Activity activity);

        void refreshSpecificView(View view);
    }

    public static switchColor mSwitchColor;

    public static void setSwitchColor(switchColor switchColor) {
        mSwitchColor = switchColor;
    }

    static @ColorInt int replaceColorById(Context context, @ColorRes int colorId) {
        return mSwitchColor == null ? Color.TRANSPARENT : mSwitchColor.replaceColorById(context, colorId);
    }

    static @ColorInt int replaceColor(Context context, @ColorInt int color) {
        return mSwitchColor == null ? Color.TRANSPARENT : mSwitchColor.replaceColor(context, color);
    }

    public interface switchColor {
        @ColorInt
        int replaceColorById(Context context, @ColorRes int colorId);

        @ColorInt
        int replaceColor(Context context, @ColorInt int color);
    }
}