來自B站的開源的MagicaSakura原始碼解析

Anderson大碼渣發表於2017-06-05

簡介

MagicaSakura是Bilibili開源的一套主題切換框架,其功能是在不重啟Activity的情況下,能夠無閃屏的對程式中的控制元件進行更換主題顏色.之所以能做到這一點,是因為其實現方式是切換主題時,設定主題顏色,通過其提供的ThemeUtils.refreshUI方法讓每個控制元件進行改變顏色.

關於該框架的使用可以看原作者介紹www.xyczero.com/blog/articl…

來自B站的開源的MagicaSakura原始碼解析

初步使用

我們需要完成 switchColor 介面.

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

    @ColorInt int replaceColor(Context context, @ColorInt int color);
}複製程式碼

在該介面裡面, 有兩個方法返回的均是colorId, 我們就是在這個切換器介面裡進行根據主題變換返回不同的顏色值即可.

並將該介面設定為全域性變數,因此建議在Application中實現該介面,並設定,設定其為全域性切換器

ThemeUtils.setSwitchColor(this);

    public static switchColor mSwitchColor;
    public static void setSwitchColor(switchColor switchColor) {
        mSwitchColor = switchColor;
    }複製程式碼

ThemeUtils.refreshUI原理

在初始化介面後, 我們可以使用public static void refreshUI(Context context, ExtraRefreshable extraRefreshable)方法進行主題的切換.

我們看一看該方法的原始碼.其先拿到介面的rootview,再呼叫了`refreshView方法進行重新整理.

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);
    }
}複製程式碼

再來看refreshView方法, 可以看到,如果該view 完成了Tintable介面, 讓其執行((Tintable) view).tint()方法, 若是viewGroup, 則不斷遞迴進行該操作. 若是ListView(GridView)或者RecylerView就notify一下.若是RecyclerView,也是重新整理一下。

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 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));
            }
            ...
            ((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);
            }
        }
    }
}複製程式碼

view.tint()是怎麼做的?

我們來看tint()方法原始碼。發現其是通過三個helper的tint來做的。其抽象出三個Helper,分別控制的是文字顏色變換,背景顏色變換以及複合繪圖變換。

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

public void tint() {
    if (mTextHelper != null) {
        mTextHelper.tint();
    }
    if (mBackgroundHelper != null) {
        mBackgroundHelper.tint();
    }
    if (mCompoundDrawableHelper != null) {
        mCompoundDrawableHelper.tint();
    }
}複製程式碼

我們從TintTextView原始碼來看。

先看其建構函式,直接呼叫幾個Helper的void loadFromAttribute(AttributeSet attrs, int defStyleAttr)方法,也就是說在這些View的載入時,便去從配置的屬性中進行載入顏色,這解決了在重新整理UI時,那些未出現的控制元件顏色無法更改的問題。

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);
}複製程式碼

來看一個Helper的load方法

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();
}複製程式碼

其實裡面就是獲取顏色,設定顏色這些事情。

為什麼需要複寫那些控制元件?

MagicaSakura的原理我們知道是遍歷Tintable類View, 其會自動根據主題顏色換色,但是對於還未出現的那些View, 之後再出現,若是原生的,其不會更新自己的主題色的.我本想避免使用複寫控制元件的方式通過其他屬性進行主題變換的,發現根本沒法解決未出現的控制元件的主題問題。

缺點

  1. MagicaSakura多主題框架是針對的換色而言,其設計就是為換色而生,而對於其他的明星皮膚等換膚需求,則做不了該需求
  2. 使用該框架,我們的xml檔案需要大改,很多需要改色的控制元件都需要使用其提供的Tint工具包的類替換原來的控制元件,有寫Tint包裡面沒有類比如Toolbar則需要自己處理.

本文作者:Anderson/Jerey_Jobs

部落格地址 : jerey.cn/
簡書地址 : Anderson大碼渣
github地址 : github.com/Jerey-Jobs

相關文章