Introduction to the

MagicaSakura is Bilibili’s open source theme switching framework. Its function is to change the theme color of the controls in the program without a flash screen without restarting the Activity. This is done by setting the theme color when switching themes, and having each control change color through the themeutils.refreshui method it provides.

About the use of the framework we can see the author introduce www.xyczero.com/blog/articl…

Initial use

We need to complete the switchColor interface.

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

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

In this interface, there are two methods return colorId, we are in this switcher interface to return different color values according to the theme transformation.

And set this interface as a global variable, so it is recommended to implement this interface in Application and set, set it as a global switcher

ThemeUtils.setSwitchColor(this);

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

ThemeUtils refreshUI principle

After initializing the interface, we can use the public static void refreshUI(Context Context, ExtraRefreshable ExtraRefreshable) method to switch the theme.

Let’s take a look at the source code for this method. It retrieves the interface rootView and then calls the ‘refreshView’ method to refresh.

public static void refreshUI(Context context, ExtraRefreshable extraRefreshable) {
    TintManager.clearTintCache();
    Activity activity = getWrapperActivity(context);
    if(activity ! =null) {
        if(extraRefreshable ! =null) {
            extraRefreshable.refreshGlobal(activity);
        }
        // Get the root directory of the interface.View rootView = activity.getWindow().getDecorView().findViewById(android.R.id.content); refreshView(rootView, extraRefreshable); }}Copy the code

If the view completes the Tintable interface, make it perform ((Tintable) view).tint(), or if the view group does so recursively. Notify the ListView or RecylerView. If RecyclerView, also refresh.

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)); }... ListAdapter adapter = ((AbsListView) view).getAdapter();while (adapter instanceof WrapperListAdapter) {
                adapter = ((WrapperListAdapter) adapter).getWrappedAdapter();
            }
            if (adapter instanceofBaseAdapter) { ((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)); }... ((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); }}}}Copy the code

What does view.tint() do?

Let’s look at the tint() method source code. Discover that this is done through the tinTs of the three helpers. It abstracts three helpers, which control text color transformation, background color transformation and composite drawing transformation respectively.

@Override
private AppCompatBackgroundHelper mBackgroundHelper;
private AppCompatCompoundDrawableHelper mCompoundDrawableHelper;
private AppCompatTextHelper mTextHelper;

public void tint(a) {
    if(mTextHelper ! =null) {
        mTextHelper.tint();
    }
    if(mBackgroundHelper ! =null) {
        mBackgroundHelper.tint();
    }
    if(mCompoundDrawableHelper ! =null) { mCompoundDrawableHelper.tint(); }}Copy the code

We look from TintTextView source code.

The constructor calls the void loadFromAttribute(AttributeSet Attrs, int defStyleAttr) method of several helpers. This means that the View loads the color from the configured attribute. This solves the problem that the color of controls that do not appear cannot be changed when the UI is refreshed.

public TintTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    if (isInEditMode()) {
        return;
    }
    TintManager tintManager = TintManager.get(getContext());

    mTextHelper = new AppCompatTextHelper(this, tintManager);
    mTextHelper.loadFromAttribute(attrs, defStyleAttr);

    mBackgroundHelper = new AppCompatBackgroundHelper(this, tintManager);
    mBackgroundHelper.loadFromAttribute(attrs, defStyleAttr);

    mCompoundDrawableHelper = new AppCompatCompoundDrawableHelper(this, tintManager);
    mCompoundDrawableHelper.loadFromAttribute(attrs, defStyleAttr);
}Copy the code

Let’s look at a load method for a Helper

void loadFromAttribute(AttributeSet attrs, int defStyleAttr) {
    TypedArray array = mView.getContext().obtainStyledAttributes(attrs, ATTRS, defStyleAttr, 0);

    int textColorId = array.getResourceId(0.0);
    if (textColorId == 0) {
        setTextAppearanceForTextColor(array.getResourceId(2.0), false);
    } else {
        setTextColor(textColorId);
    }

    if (array.hasValue(1)) {
        setLinkTextColor(array.getResourceId(1.0));
    }
    array.recycle();
}Copy the code

It’s all about getting the color and setting the color.

Why do I need to clone those controls?

MagicaSakura iterates through Tintable views, which will automatically change color according to the theme color, but for those views that have not yet appeared, they will not update their theme color if they are native. I tried to avoid using a copy control to change the theme through other properties, but found that there was no way to solve the theme problem of the control that did not appear.

disadvantages

  1. MagicaSakuraMulti-theme frame is aimed at changing color, its design is born for changing color, but for other star skin and other skin needs, can not do this demand
  2. Using this framework, our XML files need to be changed greatly, and many of the controls that need to be changed need to be usedTintToolkit classes to replace the original control, there is no Tint package written inside the class such asToolbarYou have to deal with it yourself.

Anderson/Jerey_Jobs

Blog address: jerey.cn/ Jane Book address: Anderson Big code cinder Github address: github.com/Jerey-Jobs