MagicaSakura
bilibili的又一Android開源作品, 可以無閃屏地對(duì)程序中的控件更換主題色, 其采用的遍歷View樹的方式對(duì)每一個(gè)控件進(jìn)行操作(區(qū)別于保存集合). 在控件變色上使用的是對(duì)Drawable
進(jìn)行tint(區(qū)別于只對(duì)Drawable
或者ImageView
設(shè)置ColorFilter
), 其中使用到了V4包的DrawableCompat
, 還對(duì)特別的View
進(jìn)行了特殊處理. 使用TintDrawable的方式不會(huì)影響原來的屬性和使用方式. 要說明的是這種方式要對(duì)所有要變色的View
進(jìn)行自定義, 以后項(xiàng)目中就不能夠好好寫換件了...更多的介紹可以看原作者的介紹.
MagicaSakura使用
原作者有在博客中說明使用方法: 實(shí)現(xiàn)切換顏色的SwitchColor
, 重寫其兩個(gè)方法. 再有要自己確定各個(gè)主題色, 然后切換主題色時(shí)使用的方法是ThemeUtils
的一個(gè)全局方法refreshUI
, 它最終會(huì)使用到SwitchColor
來得到色值.
MagicaSakura分析
下面先分析換扶主要流程, 再去分析每一個(gè)View
進(jìn)行換膚的流程, 最后再說一些特殊的View
進(jìn)行換膚的細(xì)節(jié)
流程分析
首先要去自己實(shí)現(xiàn)SwitchColor
, 并通過ThemeUtils
將其注冊(cè)成為全局變量, 在以后的換膚中方便使用.
//將切換顏色的對(duì)象作為全局變量存儲(chǔ)起來
ThemeUtils.setSwitchColor(this);
其中的this
實(shí)現(xiàn)了SwitchColor
接口, 負(fù)責(zé)給出皮膚的顏色, 通過兩個(gè)接口方法給出.
public interface switchColor {
//通過指定ID來更換顏色
@ColorInt int replaceColorById(Context context, @ColorRes int colorId);
@ColorInt int replaceColor(Context context, @ColorInt int color);
}
下面分析在我們點(diǎn)換膚的時(shí)候程序運(yùn)程的流程
上面說過每一次換膚都要對(duì)View樹進(jìn)行遍歷, 封閉遍歷的方法在ThemeUtils.refreshUI(Context context, ExtraRefreshable extraRefreshable)
中.
//在這里對(duì)整個(gè)view樹進(jìn)行遍歷
public static void refreshUI(Context context, ExtraRefreshable extraRefreshable) {
TintManager.clearTintCache();
Activity activity = getWrapperActivity(context);
if (activity != null) {
if (extraRefreshable != null) {
extraRefreshable.refreshGlobal(activity);
}
//對(duì)contentView進(jìn)行遍歷
View rootView = activity.getWindow().getDecorView().findViewById(android.R.id.content);
refreshView(rootView, extraRefreshable);
}
}
兩個(gè)參數(shù), ctx不用說, 第二個(gè)ExtraRefreshable
接口有兩個(gè)方法, void refreshGlobal(Activity activity);
是每次換膚是調(diào)用一次的方法, void refreshSpecificView(View view)
是對(duì)特殊的View進(jìn)行染色時(shí)都要調(diào)用的方法.
我們可以看到他是通過對(duì)Activity去拿到contentView去進(jìn)行遍歷的. refreshView(rootView, extraRefreshable);
是對(duì)View樹進(jìn)行遞歸遍歷的方法.
private static void refreshView(View view, ExtraRefreshable extraRefreshable) {
if (view == null) return;
//下面進(jìn)行遞歸遍歷
view.destroyDrawingCache();
if (view instanceof Tintable) {
//最關(guān)鍵的部分, 拿到每個(gè)view后tint一下
((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);
}
//ListView和GridView之類
if (view instanceof AbsListView) {
ListAdapter adapter = ((AbsListView) view).getAdapter();
//拿到根本的Adapter
while (adapter instanceof WrapperListAdapter) {
adapter = ((WrapperListAdapter) adapter).getWrappedAdapter();
}
if (adapter instanceof BaseAdapter) {
((BaseAdapter) adapter).notifyDataSetChanged();
}
}
if (view instanceof RecyclerView) {
try {
if (mRecycler == null) {
mRecycler = RecyclerView.class.getDeclaredField("mRecycler");
mRecycler.setAccessible(true);
}
if (mClearMethod == null) {
mClearMethod = Class.forName("android.support.v7.widget.RecyclerView$Recycler")
.getDeclaredMethod("clear");
mClearMethod.setAccessible(true);
}
mClearMethod.invoke(mRecycler.get(view));
} catch (NoSuchMethodException e) {
...
}
((RecyclerView) view).getRecycledViewPool().clear();
((RecyclerView) view).invalidateItemDecorations();
}
//不是tintabale, 遍歷孩子
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
refreshView(((ViewGroup) view).getChildAt(i), extraRefreshable);
}
}
}
}
整個(gè)過程就是遞歸遍歷, Tintable
的實(shí)現(xiàn)就直接渲染, 是ViewGroup
就遞歸, 是ListView(GridView)
或者RecylerView
就notify一下, 就這么完了, 這就是一個(gè)簡(jiǎn)單的流程了.
其中的tint
方法就是對(duì)每個(gè)具體的View
進(jìn)行渲染, 達(dá)到自定義顏色的效果. 具體tint的過程下面會(huì)講一下, 幾乎對(duì)所有常用的View
進(jìn)行了重寫, 工作量很大, 但一個(gè)控件的流程走通了, 其他的控件原理都是類似的, 也很快就了解了. 此時(shí)配上MagicaSakura
的包結(jié)果圖, 可以看到widgets包下的所有常用View都被重寫了.
View進(jìn)行渲染過程
對(duì)View的渲染都是通過在View中保存的幾個(gè)Helper實(shí)現(xiàn)的, 每個(gè)要換膚的View
在構(gòu)造的時(shí)候會(huì)根據(jù)跟隨皮膚變化的屬性構(gòu)建對(duì)應(yīng)的Helper, 比如說TextView
在換膚的時(shí)候要變換自己的TextColor
, BackGround
以及drawableLeft
, drawableRight
之類的屬性所以在TintTextView
中會(huì)保存對(duì)應(yīng)的三個(gè)Helper, 如圖:
這么做不僅能將換膚功能的代碼解耦出來, 最重要的是可以在不同的控件上復(fù)用這個(gè)Helper, 比如
TintImageView
也要在換膚時(shí)對(duì)Background
進(jìn)行變換, 直接重用AppCompatBackgroundHelper
就可以了.下面以
TextView
為例, 分析一下作者是怎樣讓一個(gè)View
能顯示任意一種顏色, 并且還能動(dòng)態(tài)地切換View
的色值.先看其構(gòu)造方法:
public TintTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (isInEditMode()) {
return;
}
//TintManager負(fù)責(zé)管理Drawable資源, 后面會(huì)講到
TintManager tintManager = TintManager.get(getContext());
//控制TextColor之類的屬性
mTextHelper = new AppCompatTextHelper(this, tintManager);
mTextHelper.loadFromAttribute(attrs, defStyleAttr);
//控制Background屬性
mBackgroundHelper = new AppCompatBackgroundHelper(this, tintManager);
mBackgroundHelper.loadFromAttribute(attrs, defStyleAttr);
//控制DrawableLeft, DrawableRight之類的屬性
mCompoundDrawableHelper = new AppCompatCompoundDrawableHelper(this, tintManager);
mCompoundDrawableHelper.loadFromAttribute(attrs, defStyleAttr);
}
TintTextView
使用了三個(gè)Helper,都作為成員保存起來, 構(gòu)造出來之后直接調(diào)用其void loadFromAttribute(AttributeSet attrs, int defStyleAttr)
方法, 其它Helper也是類似. 這三個(gè)Helper就在View的tint方法中類似于下面方式使用. 另外, 涉及這些屬性變化的方法都要進(jìn)行重寫, 都要使用這些Helper進(jìn)行變化屬性值.
if (mTextHelper != null) {
mTextHelper.tint();
}
下面以AppCompatTextHelper
為例分析他們的工作原理.
其構(gòu)造方法只是將當(dāng)前的view
和tintManager
保存為成員. 其loadFromAttribute
方法要對(duì)View
的幾個(gè)屬性進(jìn)行處理, 代碼如下:
void loadFromAttribute(AttributeSet attrs, int defStyleAttr) {
TypedArray array = mView.getContext().obtainStyledAttributes(attrs, ATTRS, defStyleAttr, 0);
int textColorId = array.getResourceId(0, 0);
if (textColorId == 0) {//如果沒有設(shè)定TextColor就使用TextAppearance
setTextAppearanceForTextColor(array.getResourceId(2, 0), false);
} else {
setTextColor(textColorId);//此方法去會(huì)去找到真正的顏色并且設(shè)置給這個(gè)View
}
if (array.hasValue(1)) {
setLinkTextColor(array.getResourceId(1, 0));
}
array.recycle();
}
插一句, 當(dāng)時(shí)看這一點(diǎn)的時(shí)候犯了個(gè)迷糊...這里使用了0, 2什么的是是因?yàn)樯厦鎸?duì)ATTRS
的定義:
private static final int[] ATTRS = {
android.R.attr.textColor,
android.R.attr.textColorLink,
android.R.attr.textAppearance,
};
//這里就要處理三個(gè)屬性, 所在先組成一個(gè)數(shù)組
//拿到的TypeArray里面就應(yīng)該只有三個(gè)值, 這也是后面使用0, 1, 2的原因
回到正題上來, 看setTextColor
方法
private void setTextColor(@ColorRes int resId) {
if (mTextColorId != resId) {
//記錄色值, 清除染色信息, 放心, 在下面一句又將這個(gè)信息給加上了
resetTextColorTintResource(resId);
if (resId != 0) {
setSupportTextColorTint(resId);
}
}
}
//設(shè)置染色信息
private void setSupportTextColorTint(int resId) {
if (resId != 0) {
if (mTextColorTintInfo == null) {
mTextColorTintInfo = new TintInfo();
}
mTextColorTintInfo.mHasTintList = true;
//這個(gè)過程會(huì)在后面解釋, 就是能拿到要渲染的ColorStateList
mTextColorTintInfo.mTintList = mTintManager.getColorStateList(resId);
}
applySupportTextColorTint();
}
在applySupportTextColorTint
中直接使用了上面的mTextColorTintInfo.mTintList
, 直接將其設(shè)置給TextView
. Helper也會(huì)有tint方法, 此方法會(huì)對(duì)View進(jìn)行渲染, 類似于
if (mTextColorId != 0) {
setSupportTextColorTint(mTextColorId);
}
TintManager分析
還剩下最后一部分, TintManager
是怎么找到Drawable
并給他設(shè)置了皮膚包的顏色的, 下面進(jìn)行簡(jiǎn)單分析
@Nullable
public ColorStateList getColorStateList(@ColorRes int resId) {
if (resId == 0) return null;
//對(duì)Ctx進(jìn)行弱引用處理
final Context context = mContextRef.get();
if (context == null) return null;
//對(duì)colorStateList進(jìn)行了LRU緩存處理
ColorStateList colorStateList = mCacheTintList != null ? mCacheTintList.get(resId) : null;
if (colorStateList == null) {
colorStateList = ColorStateListUtils.createColorStateList(context, resId);//創(chuàng)建tintcolorStateList
if (colorStateList != null) {
if (mCacheTintList == null) {
mCacheTintList = new SparseArray<>();
}
mCacheTintList.append(resId, colorStateList);
}
}
return colorStateList;
}
這段代碼主要是處理異常和緩存問題, 真正拿到ColorStateList
是在ColorStateListUtils.createColorStateList(context, resId);
的方法中.
static ColorStateList createColorStateList(Context context, int resId) {
if (resId <= 0) return null;
TypedValue value = new TypedValue();
context.getResources().getValue(resId, value, true);
ColorStateList cl = null;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
cl = ColorStateList.valueOf(ThemeUtils.replaceColorById(context, value.resourceId));
} else {
final String file = value.string.toString();
try {
if (file.endsWith("xml")) {
final XmlResourceParser rp = context.getResources().getAssets().openXmlResourceParser(
value.assetCookie, file);
final AttributeSet attrs = Xml.asAttributeSet(rp);
int type;
while ((type = rp.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// Seek parser to start tag.
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
cl = createFromXmlInner(context, rp, attrs);
rp.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
}
return cl;
}
木有錯(cuò), 作者選擇了直接去解析XML, 這是我當(dāng)時(shí)做換膚時(shí)根本不考慮的方式, 想不到就這樣被實(shí)現(xiàn)了...其中使用了Android對(duì)XML文件進(jìn)行解析的方法, 非常值得我們?nèi)W(xué)習(xí). 通過資源的ID去取資源的信息, 如果只是顏色值就創(chuàng)建ColorStateList
, 再如果資源是XML文件的話就開始解析這個(gè)文件.
在createFromXmlInner
中判斷了文件是不是一個(gè)selector
, 是的話才繼續(xù)執(zhí)行, 否則不處理. 繼續(xù)執(zhí)行會(huì)調(diào)用到static ColorStateList inflateColorStateList(Context context, XmlPullParser parser, AttributeSet attrs) throws IOException, XmlPullParserException
的方法, 來看看真正的實(shí)現(xiàn)
static ColorStateList inflateColorStateList(Context context, XmlPullParser parser, AttributeSet attrs) throws IOException, XmlPullParserException {
final int innerDepth = parser.getDepth() + 1;
int depth;
int type;
LinkedList<int[]> stateList = new LinkedList<>();
LinkedList<Integer> colorList = new LinkedList<>();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG || depth > innerDepth
|| !parser.getName().equals("item")) {
continue;
}
TypedArray a1 = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.color});
//這里面會(huì)使用到最開始的SwitchColor, 拿到真正的Color
final int baseColor = com.bilibili.magicasakura.utils.ThemeUtils.replaceColorById(context, a1.getResourceId(0, Color.MAGENTA));
a1.recycle();
TypedArray a2 = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.alpha});
final float alphaMod = a2.getFloat(0, 1.0f);
a2.recycle();
colorList.add(alphaMod != 1.0f
? ColorUtils.setAlphaComponent(baseColor, Math.round(Color.alpha(baseColor) * alphaMod))
: baseColor);
stateList.add(extractStateSet(attrs));
}
if (stateList.size() > 0 && stateList.size() == colorList.size()) {
int[] colors = new int[colorList.size()];
for (int i = 0; i < colorList.size(); i++) {
colors[i] = colorList.get(i);
}
return new ColorStateList(stateList.toArray(new int[stateList.size()][]), colors);
}
return null;
}
上面的所有就是對(duì)換膚的流程進(jìn)行了一個(gè)簡(jiǎn)單的分析, 是否能在自己的項(xiàng)目中使用這個(gè)庫已經(jīng)可以做出部分判斷, 還有很多的細(xì)節(jié)沒有講到, 下面會(huì)無規(guī)則地介紹一些細(xì)節(jié)的問題.
部分細(xì)節(jié)問題
- 在
TintMManager
中使用了LruCache
, 對(duì)于解析等到的Drawable
要進(jìn)行緩存, 下次再取用的時(shí)候可以不去解析XML這么復(fù)雜 - 解析資源時(shí)主要支持下面三種
Drawable
, 對(duì)于不同的Drawable
解析的方式也不全一樣
- 程序中使用的
ColorFilter
都是PorterDuffColorFilter
- 上面的例子中使用的是
AppCompatTextHelper,
還有另一種使用更多的方式渲染(比如在AppCompatBackgroundHelper
中)
private boolean applySupportBackgroundTint() {
Drawable backgroundDrawable = mView.getBackground();
if (backgroundDrawable != null && mBackgroundTintInfo != null && mBackgroundTintInfo.mHasTintList) {
backgroundDrawable = DrawableCompat.wrap(backgroundDrawable);
backgroundDrawable = backgroundDrawable.mutate();
if (mBackgroundTintInfo.mHasTintList) {
DrawableCompat.setTintList(backgroundDrawable, mBackgroundTintInfo.mTintList);
}
if (mBackgroundTintInfo.mHasTintMode) {
DrawableCompat.setTintMode(backgroundDrawable, mBackgroundTintInfo.mTintMode);
}
if (backgroundDrawable.isStateful()) {
backgroundDrawable.setState(mView.getDrawableState());
}
setBackgroundDrawable(backgroundDrawable);
return true;
}
return false;
}
其中使用了V4包的DrawableCompat
, 才能使用setTintList
.
- 如果
Drawable
是ColorDrawable
的話是不能設(shè)置ColorFilter
的, 在API21以下都是不起效果的, 使用的是ColorDrawable
的setColor
方法.
結(jié)語
MagicaSakura
中將解析XML, 屬性渲染, 控件功能分離得很好, 結(jié)構(gòu)非常清晰利于擴(kuò)展, 可以學(xué)到很多.
他使用系統(tǒng)解析XML方法去自己解析, 值得學(xué)習(xí).
如果項(xiàng)目中要實(shí)現(xiàn)換膚功能的話可以考慮使用, 就是項(xiàng)目如果已經(jīng)比較大的話, 工作量可能也會(huì)很大, 也可以考慮一下Android_Skin_Loader也是不錯(cuò)的選擇.