背景
很多app需要進(jìn)行換膚,到網(wǎng)絡(luò)上找到了一個(gè)庫--android-skin-support巷燥。很快就實(shí)現(xiàn)了需求赡盘,根據(jù)文檔集成很方便,代碼侵入性也低缰揪。
本來也沒有去深究它的實(shí)現(xiàn)原理陨享,前幾天有個(gè)迭代的需求如下:
美股市場是“綠漲紅跌”,而A股市場是“紅漲綠跌”,產(chǎn)品要求用戶可以自由選擇抛姑。這個(gè)需求跟以前的換膚很類似赞厕,所以就仔細(xì)研究學(xué)習(xí)了"android-skin-support"庫的實(shí)現(xiàn)原理,并根據(jù)其原理實(shí)現(xiàn)“紅漲綠跌”.
原理
-
使用觀察者模式
框架抽象了一個(gè)SkinCompatSupportable
接口:
public interface SkinCompatSupportable {
void applySkin();
}
所有需要換膚的控件都實(shí)現(xiàn)該接口定硝,在用戶執(zhí)行“換膚”操作時(shí)候皿桑,通知所有實(shí)現(xiàn)該接口的訂閱者執(zhí)行換膚操作applySkin()
。
按照上面的原理蔬啡,那所有的控件都需要實(shí)現(xiàn)接口SkinCompatSupportable
诲侮,那原生的控件怎么辦呢?框架層面把所有常用的原生控件都重寫了一遍星爪,在包skin.support.widget
下浆西。框架層在解析布局文件的時(shí)候會把原生的控件替換成實(shí)現(xiàn)了接口SkinCompatSupportable
的對應(yīng)控件顽腾〗悖框架層是怎么做的呢?請繼續(xù)查看下文抄肖。
-
注冊activity生命周期監(jiān)聽器
查看類skin.support.app.SkinActivityLifecycle
,private SkinActivityLifecycle(Application application) { application.registerActivityLifecycleCallbacks(this); installLayoutFactory(application); SkinCompatManager.getInstance() .addObserver(getObserver(application)); } private void installLayoutFactory(Context context) { LayoutInflater layoutInflater = LayoutInflater.from(context); try { Field field = LayoutInflater.class.getDeclaredField("mFactorySet"); field.setAccessible(true); field.setBoolean(layoutInflater, false); LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context)); } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { if (isContextSkinEnable(activity)) { installLayoutFactory(activity); updateStatusBarColor(activity); updateWindowBackground(activity); if (activity instanceof SkinCompatSupportable) { ((SkinCompatSupportable) activity).applySkin(); } if (activity instanceof SkinCompatChangeGreenRed) { boolean isSupport = ((SkinCompatChangeGreenRed)activity).isSupportChange(); ((SkinCompatChangeGreenRed)activity).applyChangeColor(isSupport ? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT); } } }
注意 installLayoutFactory 方法久信,在每個(gè)activity的#onActivityCreated中把自己的LayoutInflaterFactory類(SkinCompatDelegate)設(shè)置進(jìn)去,而把原生控件替換成庫中的控件就在這個(gè)類中實(shí)現(xiàn)的漓摩,并且把所有實(shí)現(xiàn)SkinCompatSupportable接口的觀察者都收集起來裙士。這樣代碼的侵入性就變得很低,使得幾行代碼就可以實(shí)現(xiàn)換膚操作管毙。
或許有人疑惑為什么這樣做就可以實(shí)現(xiàn)xml解析攔截腿椎? 我們再來看看 ```AppCompatActivity``` 的代碼實(shí)現(xiàn)。
```java
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
我們看到有一個(gè)AppCompatDelegate,它是Activity的委托,AppCompatActivity將大部分生命周期都委托給了AppCompatDelegate,這點(diǎn)可從上面的源碼中可以看出.
繼續(xù)看源碼我們發(fā)現(xiàn)夭咬,解析xml布局的解析起也是在AppCompatDelegate對象中設(shè)置的啃炸。
AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat.setFactory2(layoutInflater, this);最終是調(diào)用的LayoutInflater的setFactory2()方法,看看實(shí)現(xiàn)
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
這里有個(gè)小細(xì)節(jié),Factory2只能被設(shè)置一次,設(shè)置完成后mFactorySet屬性就為true,下一次設(shè)置時(shí)被直接拋異常.
那么Factory2有什么用呢?看看其實(shí)現(xiàn)
它是一個(gè)接口,只有一個(gè)方法,看起來是用來創(chuàng)建View的.
綜上所述,我們就可以根據(jù)其機(jī)制實(shí)現(xiàn)xml解析并攔截卓舵,讓解析xml原生控件的時(shí)候返回我們想要的支持“換膚”動作的對應(yīng)控件南用。
- 攔截xml控件解析事件
代碼如下
SkinCompatViewInflater.java
private View createViewFromFV(Context context, String name, AttributeSet attrs) {
View view = null;
if (name.contains(".")) {
return null;
}
switch (name) {
case "View":
view = new SkinCompatView(context, attrs);
break;
case "LinearLayout":
view = new SkinCompatLinearLayout(context, attrs);
break;
case "RelativeLayout":
view = new SkinCompatRelativeLayout(context, attrs);
break;
case "FrameLayout":
view = new SkinCompatFrameLayout(context, attrs);
break;
case "TextView":
view = new SkinCompatTextView(context, attrs);
break;
case "ImageView":
view = new SkinCompatImageView(context, attrs);
break;
case "Button":
view = new SkinCompatButton(context, attrs);
break;
case "EditText":
view = new SkinCompatEditText(context, attrs);
break;
case "Spinner":
view = new SkinCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new SkinCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new SkinCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new SkinCompatRadioButton(context, attrs);
break;
case "RadioGroup":
view = new SkinCompatRadioGroup(context, attrs);
break;
case "CheckedTextView":
view = new SkinCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new SkinCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new SkinCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new SkinCompatSeekBar(context, attrs);
break;
case "ProgressBar":
view = new SkinCompatProgressBar(context, attrs);
break;
case "ScrollView":
view = new SkinCompatScrollView(context, attrs);
break;
}
return view;
}
這里還是有點(diǎn)迷惑, 那么我們再來看看android創(chuàng)建view的過程
平時(shí)我們最常使用的Activity中的setContentView()設(shè)置布局ID,看看Activity中的實(shí)現(xiàn),
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
調(diào)用的是Window中的setContentView(),而Window只有一個(gè)實(shí)現(xiàn)類,就是PhoneWindow.看看setContentView()實(shí)現(xiàn)
@Override
public void setContentView(int layoutResID) {
...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
看到了今天的主角mLayoutInflater,mLayoutInflater是在PhoneWindow的構(gòu)造方法中初始化的.用mLayoutInflater去加載這個(gè)布局(layoutResID).點(diǎn)進(jìn)去看看實(shí)現(xiàn),來看看createViewFromTag()的實(shí)現(xiàn)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
...
return view;
}
可以看到如果mFactory2不為空的話,那么就會調(diào)用mFactory2去創(chuàng)建View(mFactory2.onCreateView(parent, name, context, attrs)) . 這句結(jié)論很重要.前面的答案已揭曉.如果設(shè)置了mFactory2就會用mFactory2去創(chuàng)建View.而mFactory2在上面已經(jīng)被我們替換了掏湾。****
- 加載皮膚
我們前面已經(jīng)了解了換膚的原理裹虫,現(xiàn)在就根據(jù)上述原理進(jìn)行換膚。 該框架提供了幾種加載皮膚的策略融击,前后綴價(jià)值筑公,apk加載等等。
SkinCompatManager.getInstance().loadSkin
總結(jié)
簡單總結(jié)一下原理(本文精髓)
監(jiān)聽APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
在每個(gè)Activity的onCreate()方法調(diào)用時(shí)setFactory(),設(shè)置創(chuàng)建View的工廠.將創(chuàng)建View的瑣事交給SkinCompatViewInflater去處理.
庫中自己重寫了系統(tǒng)的控件(比如View對應(yīng)于庫中的SkinCompatView),實(shí)現(xiàn)換膚接口(接口里面只有一個(gè)applySkin()方法),表示該控件是支持換膚的.并且將這些控件在創(chuàng)建之后收集起來,方便隨時(shí)換膚.
在庫中自己寫的控件里面去解析出一些特殊的屬性(比如:background, textColor),并將其保存起來
在切換皮膚的時(shí)候,遍歷一次之前緩存的View,調(diào)用其實(shí)現(xiàn)的接口方法applySkin(),在applySkin()中從皮膚資源(可以是從網(wǎng)絡(luò)或者本地獲取皮膚包)中獲取資源.獲取資源后設(shè)置其控件的background或textColor等,就可實(shí)現(xiàn)換膚.
借鑒應(yīng)用
現(xiàn)在根據(jù)上述原理低侵入性實(shí)現(xiàn)“紅漲綠跌”尊浪,
- 接口抽象
定義一個(gè)接口匣屡,讓支持“紅漲綠跌”切換的控件都實(shí)現(xiàn)該接口
SkinCompatChangeGreenRed
- 所有觀察者都集合起來
- 執(zhí)行“紅漲綠跌”操作的時(shí)候通知所有觀察者涩拙。
public void notifyChangeColor(boolean isSupport){
SkinObserver[] arrLocal;
synchronized (this) {
arrLocal = observers.toArray(new SkinObserver[observers.size()]);
}
for (int i = arrLocal.length-1; i>=0; i--)
arrLocal[i].updateChangeColor(this, isSupport? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT);
}
- 切換顏色的動作 每個(gè)控制自己處理。