如题,这篇文章我讲的非常通俗易懂,是为了方便小白能够大致了解这个框架的粗略实现过程,大神们请绕道哦~
最近在做夜间模式,引用了ximsfei大神的Android-skin-support换肤框架来实现。该框架的整体的实现思想是:自定义ActivityLifecycle来记录Activities的生命周期,在每一个Activity执行完onResume()后,将Activity和LazySkinObserver接口绑定,在用户点击换肤操作时,通过接口回调来通知每一个Activity的换肤。
这篇文章以“应用内换肤”方式为例来讲解Android-skin-support换肤框架的实现。
转载请注明:https://www.jianshu.com/p/78e75dba9e55
在Application中初始化SkinCompatManager
SkinCompatManager.withoutActivity(this)
// 自定义加载策略,指定SDCard路径
.addStrategy(new CustomSDCardLoader())
// material design
.addInflater(new SkinMaterialViewInflater())
// ConstraintLayout
.addInflater(new SkinConstraintViewInflater())
// CardView v7
.addInflater(new SkinCardViewInflater())
// 关闭状态栏换肤
.setSkinStatusBarColorEnable(false)
// 关闭windowBackground换肤
.setSkinWindowBackgroundEnable(false)
.loadSkin();
用户点击换肤操作后,我们只需调用以下方法,一句话即可实现换肤:
SkinCompatManager.getInstance().loadSkin("night", null, SkinCompatManager.SKIN_LOADER_STRATEGY_BUILD_IN);
当调用了loadSkin()方法后,SkinCompatManager会开启一个SkinLoadTask,该方法的doInBackground()会获取皮肤包名字,然后在onPostExecute()方法中,将皮肤名写入SharePreference,同时调用notifyUpdateSkin()开始换肤。具体可以看SkinCompatManager中如下几段核心代码:
private class SkinLoadTask extends AsyncTask<String, Void, String> {
······
@Override
protected String doInBackground(String... params) {
······
if (params.length == 1) {
String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
return params[0];
}
······
}
@Override
protected void onPostExecute(String skinName) {
······
if (skinName != null) {
SkinPreference.getInstance()
.setSkinName(skinName)
.setSkinStrategy(mStrategy.getType())
.commitEditor();
notifyUpdateSkin();
······
} else {
······
}
}
}
SkinObservable的notifyUpdateSkin()方法,通过循环所记录的observers,一个一个地通知Activity开始换肤:
for (int i = arrLocal.length-1; i>=0; i--) {
arrLocal[i].updateSkin(this, arg);
}
那这里的observers从何而来呢?我们找下在哪里调用了addObserver()这个方法就可以了。我们可以看到,在SkinActivityLifecycle的onActivityResumed()方法中,为每一个Activity都添加了独立的SkinObservable:
@Override
public void onActivityResumed(Activity activity) {
mCurActivityRef = new WeakReference<>(activity);
if (isContextSkinEnable(activity)) {
LazySkinObserver observer = getObserver(activity);
SkinCompatManager.getInstance().addObserver(observer);
observer.updateSkinIfNeeded();
}
}
这样,通过接口回调,就可以通知每一个已经存在了的Activity去刷新UI啦。
PS:这里我们还需要注意下onActivityCreated()回调:
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (isContextSkinEnable(activity)) {
installLayoutFactory(activity);
updateWindowBackground(activity);
if (activity instanceof SkinCompatSupportable) {
((SkinCompatSupportable) activity).applySkin();
}
}
}
这里有一个很重要的方法:installLayoutFactory():
private void installLayoutFactory(Context context) {
······
LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
······
}
我们只看这行比较重要的代码,这里边有个getSkinDelegate()方法:
private SkinCompatDelegate getSkinDelegate(Context context) {
······
SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
if (mSkinDelegate == null) {
mSkinDelegate = SkinCompatDelegate.create(context);
mSkinDelegateMap.put(context, mSkinDelegate);
}
return mSkinDelegate;
}
这里将一个SkinCompatDelegate 对象和当前Activity对象绑定,放进了mSkinDelegateMap,这个mSkinDelegateMap在后续的换肤中需要用到,请记住。
刷新UI最终会走到LazySkinObserver的updateSkinForce()方法:
void updateSkinForce() {
······
if (mContext instanceof Activity && isContextSkinEnable(mContext)) {
updateWindowBackground((Activity) mContext);
}
getSkinDelegate(mContext).applySkin();
if (mContext instanceof SkinCompatSupportable) {
((SkinCompatSupportable) mContext).applySkin();
}
······
}
updateWindowBackground()对于换肤并不是那么重要,这里我们就不讨论了。主要是看这里最终调用了一个非常重要的方法:applySkin()。applySkin()方法十分重要,我们以后在写自定义View支持换肤时,都要和它打交道。getSkinDelegate(mContext).applySkin();
是通知View去刷新UI,((SkinCompatSupportable) mContext).applySkin();
则是通知实现了SkinCompatSupportable的Activity(如果你的Activity实现了SkinCompatSupportable接口,则可以在回调中做一些自己想要的操作)。
getSkinDelegate()方法则是从mSkinDelegateMap中读取当前Activity的SkinDelegate对象(这个mSkinDelegateMap就是我们在第2点中提到的):
private SkinCompatDelegate getSkinDelegate(Context context) {
······
SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
if (mSkinDelegate == null) {
mSkinDelegate = SkinCompatDelegate.create(context);
mSkinDelegateMap.put(context, mSkinDelegate);
}
return mSkinDelegate;
}
这里我们需要讲一下另外一个重要的类:SkinCompatDelegate。
SkinCompatDelegate这个类非常重要,它将我们写在XML中的View,偷偷转换成了支持换肤的自定义View。我们可以把这个类当成View的一个代理类,它继承自V4包的LayoutInflaterFactory接口,拥有View的生命周期管理:
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createView(parent, name, context, attrs);
if (view == null) {
return null;
}
if (view instanceof SkinCompatSupportable) {
mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
}
return view;
}
在onCreateView()中,createView()方法是将普通View替换为了支持换肤的自定义View, 同时,mSkinHelpers.add()方法将View记录起来。
最后,我们调用SkinCompatDelegate的applySkin()方法,去通知自定义View刷新:
public void applySkin() {
if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
for (WeakReference ref : mSkinHelpers) {
if (ref != null && ref.get() != null) {
// 这里就是调用了自定义View的applySkin()
((SkinCompatSupportable) ref.get()).applySkin();
}
}
}
}
至此,整个换肤过程完毕。
我们之前说道,SkinCompatDelegate的onCreateView()中,将我们写在XML中的View转换成了支持换肤的自定义View,这个过程是怎么实现的呢?
类的替换通过SkinLayoutInflater接口来实现:
我们可以自定义一个LayoutInflater实现自SkinLayoutInflater,我们以库中的SkinAppCompatViewInflater来分析。
@Override
public View createView(Context context, String name, AttributeSet attrs) {
View view = createViewFromFV(context, name, attrs);
if (view == null) {
view = createViewFromV7(context, name, attrs);
}
return view;
}
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;
······
}
return view;
}
我们看到,这个类将Android标准View和自定义的支持换肤的View之间进行了映射,偷偷将普通的View替换为了支持换肤的View。
映射完成后,回到我们之前说过的SkinCompatDelegate类,它的createView()方法,最终调用的就是 mSkinCompatViewInflater.createView()方法。
关于自定义支持换肤的View,很简单,只需要让你的自定义View实现SkinCompatSupportable接口,然后在applySkin()中实现换肤代码即可。这里就不多说了,具体的参照库中的Widget进行实现即可。
库中提供了SkinCompatBackgroundHelper、SkinCompatTextHelper等辅助类,这些类提供了loadFromAttributes()方法,可以获取对应的夜间模式颜色,方便你更快速地进行换肤实现。
我们还可以自定义一个SkinLayoutInflater,将XML中的View名称和自定义的View进行对应。比如RecyclerView在库中并未被自定义,那我们可以自定义一个SkinCompatRecyclerView,然后在CustomSkinLayoutInflater中写上他们的对应关系:
case "RecyclerView":
view = new SkinCompatRecyclerView(context, attrs);
break;
这个设计的好处是,即使你在XML只忘记了引用SkinCompatRecyclerView,也不会影响RecyclerView的换肤。