Andorid 换肤框架AndSkin源码解析及优缺点
来源:互联网 发布:js判断ios版本号 编辑:程序博客网 时间:2024/05/29 12:29
AndSkin简介及使用教程
AndSkin gaybug: https://github.com/RrtoyewxXu/andSkin
AndSkin 作者写的说明: http://blog.csdn.net/zhi184816/article/details/53436761
AndSkin源码解析
初始化
BaseSkinApplication解析
BaseSkinApplication中其实就一行初始化的代码:SkinLoader.getDefault().init(this)
。其中SkinLoader.getDefault()
采用单例返回了SkinLoader对象,中间啥也没干,故省去这部分代码。
public void init(Context context) { mLoadSkinDeliver = new LoadSkinDeliver(); DataManager.getDefault().init(context, mLoadSkinDeliver); String pluginAPKPackageName = DataManager.getDefault().getPluginPackageName(); String pluginAPKPath = DataManager.getDefault().getPluginPath(); String pluginAPKSuffix = DataManager.getDefault().getResourceSuffix(); GlobalManager.getDefault().init(context, pluginAPKPackageName, pluginAPKPath, pluginAPKSuffix); ResourceManager.getDefault().init(pluginAPKPackageName, pluginAPKPath, pluginAPKSuffix, mLoadSkinDeliver); }
初始化的代码基本都在这里。后续会用到这里初始化的很多对象,所以下面会比较详细的讲解这个方法及中间生成的各种对象。
LoadSkinDeliver是SkinLoader的内部类,继承自IDeliver,而且内部有个获取了主线程looper的Handler,功能是负责消息的分发。在后续的换肤操作消息分发都由LoadSkinDeliver负责通知到各个界面的各个View。
private class LoadSkinDeliver implements IDeliver { private Handler mHandler = new Handler(Looper.getMainLooper()); ... }
DataManager实现了ILoadSkin接口,实际上这个类的作用只是采用SP的方式保存下当前皮肤的后缀(suffix),对于动态换肤还会保存插件包包名(plugin_package_name)和插件包路径(plugin_path)。作用相当于SP的封装类,相信读者能够看快看透。这里就不详细讲述了。
GlobalManager这个类就是个JavaBean,在内存中保存了ApplicationContext、PackageName、PluginAPKPackageName、PluginAPKPath和ResourceSuffix属性。除此之外,没有任何作用。
ResourceManager比较重要,也是动态换肤的精髓所在。ResourceManager实现了ILoadSkin接口,并持有一个Resource的引用。
public class ResourceManager implements ILoadSkin { private Resource mResource; private IDeliver mIDeliver; ... void init(String pluginPackageName, String pluginPath, String pluginSuffix, IDeliver deliver) { mIDeliver = deliver; smartCreateResource(pluginPackageName, pluginPath, pluginSuffix, true); } private boolean smartCreateResource(String pluginPackageName, String pluginPath, String suffix, boolean firstInit) { boolean shouldCreate = checkIfReCreateDateResource(pluginPackageName, pluginPath, suffix); if (shouldCreate) { try { createDataResource(pluginPackageName, pluginPath, suffix); mIDeliver.postResourceManagerLoadSuccess(firstInit, pluginPackageName, pluginPath, suffix); } catch (Exception e) { e.printStackTrace(); mIDeliver.postResourceManagerLoadError(firstInit); } } else { mResource.changeResourceSuffix(suffix); mIDeliver.postResourceManagerLoadSuccess(false, pluginPackageName, pluginPath, suffix); } return mResource != null; }}
smartCreateResource方法中首先会依据pluginPackageName, pluginPath, suffix判断是否需要重新生成Resource对象,这里依据是本地换肤还是动态换肤生成相应的LocalResource或者PluginResource对象。这里只是初始化,暂时不用深入,后续换肤章节中会详细解析这部分。目前是第一次实例化,mResource==null,即shouldCreate为true。
private void createDataResource(String pluginPackageName, String pluginPath, String suffix) throws Exception { mResource = ResourceFactory.newInstance().createResource(pluginPackageName, pluginPath, suffix, mIDeliver); }
public abstract class ResourceFactory { private ResourceFactory() { } public static ResourceFactory newInstance() { return new ResourceFactoryImp(); } public abstract Resource createResource(String pluginPackageName, String pluginPath, String suffix, IDeliver deliver) throws Exception; static class ResourceFactoryImp extends ResourceFactory { private ResourceFactoryImp() { } @Override public Resource createResource(String pluginPackageName, String pluginPath, String suffix, IDeliver deliver) throws Exception { String packageName = GlobalManager.getDefault().getPackageName(); Context context = GlobalManager.getDefault().getApplicationContext(); if (!TextUtils.isEmpty(pluginPackageName) && !pluginPackageName.equals(packageName)) { return new PluginResource(context, pluginPackageName, pluginPath, suffix); } return new LocalResource(context, pluginPackageName, pluginPath, suffix); } }}
mResource属性会在这里初始化,生成的逻辑也很简单,就是依据报名判断是不是动态换肤。如果不是,返回PluginResource,反之则返回LocalResource。这两个类都继承自Resource,并重写了部分方法,差别是PluginResource实例化时传入了动态插件(.apk)的AssetManager,这个AssetManager使用动态插件的路径构建的。这部分仍在会在换肤章节重点解析,现在回到ResourceManager#smartCreateResource。在获取到Resource之后,调用mIDeliver.postResourceManagerLoadSuccess
分发消息到监听器中,告诉监听器初自己始化完毕。代码体现如下:
mHandler.post(new Runnable() { @Override public void run() { if (firstInit && mOnInitLoadSkinResourceListener != null) { mOnInitLoadSkinResourceListener.onInitResourceSuccess(); } else { boolean findResourceSuccess = notifyAllChangeSkinObserverListToFindResource(); if (findResourceSuccess) { postGetAllResourceSuccessOnMainThread(pluginPackageName, pluginPath, resourceSuffix); } else { postGetResourceErrorOnMainThread(); } } } });
换肤
换肤需要换肤的Activity继承自BaseSkinActivity。BaseSkinActivity继承自AppCompatActivity,并实现了IChangeSkin接口。核心代码如下:
public class BaseSkinActivity extends AppCompatActivity implements IChangeSkin { protected BaseSkinActivity mActivity; private SkinLayoutInflater mSkinLayoutInflater; ... @Override protected void onCreate(@Nullable Bundle savedInstanceState) { mActivity = this; if (shouldRegister()) { mSkinLayoutInflater = new SkinLayoutInflater(this); } super.onCreate(savedInstanceState); } @Override public void setContentView(@LayoutRes int layoutResID) { super.setContentView(layoutResID); if (shouldRegister()) { findLayoutInflaterSkinViews(); generateStatusBarIfShould(); SkinLoader.getDefault().register(mActivity); } }
由于LayoutInflaterCompat.setFactory
只有在第一次调用的时候有效,所以AppCompatActivity#installViewFactory(在onCreate被调用,方法内会调用LayoutInflaterCompat.setFactory)在被调用之前提前调用LayoutInflaterCompat.setFactory
。代码体现为:
public class SkinLayoutInflater { ... public SkinLayoutInflater(BaseSkinActivity baseSkinActivity) { this.mBaseSkinActivity = baseSkinActivity; mSkinInflaterFactory = new SkinInflaterFactory(); mDynamicAddSkinViewList = new ArrayList<>(); mLayoutInflaterSkinViewList = new ArrayList<>(); LayoutInflaterCompat.setFactory(mBaseSkinActivity.getLayoutInflater(), mSkinInflaterFactory); } ...}
这里的SkinInflaterFactory实现了LayoutInflaterFactory接口,如此一来,在继承BaseSkinActivity的页面中,View从XML到变成View的解析工作就交给了SkinInflaterFactory。
下面回到BaseSkinActivity#setContentView。在调用super.setContentView(layoutResID)
之后会调用findLayoutInflaterSkinViews方法。而在View从XML变成View之前,会调用SkinInflaterFactory#onCreateView。这部分逻辑体现在framework层的LayoutInflater#createViewFromTag。感兴趣的可以查看我的另一篇博文 Android XML布局文件解析过程源码解析。
SkinInflaterFactory中有个属性mSkinViewList,其中保存了所有需要换肤View的id。接下来会分析怎么获取到的这些id,这也是换肤框架的一个核心难点所在。
public class SkinInflaterFactory implements LayoutInflaterFactory { private List<SkinView> mSkinViewList = new ArrayList<>(); @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { boolean isSkinEnable = attrs.getAttributeBooleanValue(ConfigConstants.SKIN_NAMES_SPACE, ConfigConstants.ATTR_SKIN_ENABLE, false); String attrList = attrs.getAttributeValue(ConfigConstants.SKIN_NAMES_SPACE, ConfigConstants.ATTR_SKIN_LIST); if (isSkinEnable) { try { if (TextUtils.isEmpty(attrList)) { parseSkinAttr(context, attrs, name); } else { attrList = attrList.trim(); parseSkinAttrByAttrList(context, attrs, attrList, name); } } catch (Exception e) { e.printStackTrace(); SkinL.e("解析xml文件失败,请检查xml文件"); } } return null; } ...}
先看下大概的逻辑:
SKIN_NAMES_SPACE是常量:http://schemas.android.com/android/andSkin。
ATTR_SKIN_ENABLE是常量:enable
ATTR_SKIN_LIST是常量:attrs
首先查看View是否设置了enable属性。如果没有,则不需要换肤。如果有,再去判断哪些属性需要换肤。这里能够获取View所有的属性及对应的值,所以查找及添加到mSkinViewList的代码就不一一解析了。跟普通写个list.add()没啥区别。需要注意的地方是,这里onCreateView返回了null,也就是说创建View的工作交给了LayoutInflater#onCreateView。还一个需要注意的是,这里保存的是View的id,并不是真正的View的引用。很明显,返回null的话,View这时候还没创建。
所以初始化及准备工作到这里就算真正完成了,接下来是真正调用换肤API实现换肤的解析。
本地换肤
假设我现在有两套皮肤,一套叫day,另一套叫night。day皮肤为默认皮肤,文件命名大概为:icon_search.png。night为夜间皮肤,文件命名大概为icon_search_night.png。那么,应用内由day皮肤切换到night的代码为:
SkinLoader.getDefault().loadSkin("night");
一行代码实现换肤,使用非常简单。跟进。
public class SkinLoader implements ILoadSkin { ... @Override public void loadSkin(String suffix) { loadSkin("", "", suffix); } @Override public void loadSkin(String pluginPackageName, String pluginPath, String suffix) { loadSkinInner(pluginPackageName, pluginPath, suffix, true); } private void loadSkinInner(String pluginPackageName, String pluginPath, String suffix, boolean needCallSkinChangeListener) { cancelLoadSkinTask(); startLoadSkinTask(pluginPackageName, pluginPath, suffix, needCallSkinChangeListener); } private void startLoadSkinTask(String pluginAPKPackageName, String pluginAPKPath, String resourceSuffix, boolean needCallSkinChangeListener) { mLoadSkinTask = new LoadSkinTask(); mLoadSkinTask.setNeedCallSkinChangeListener(needCallSkinChangeListener); mLoadSkinTask.execute(pluginAPKPackageName, pluginAPKPath, resourceSuffix); } ...}
经过一些列的重载进入到startLoadSkinTask方法中。LoadSkinTask继承自AsyncTask,实例化mLoadSkinTask属性之后,设置needCallSkinChangeListener为true。最后调用execute()方法,并将pluginAPKPackageName、pluginAPKPath,、resourceSuffix传递进去。因为是本地换肤,pluginAPKPackageName和pluginAPKPath参数暂时为null。
在onPreExecute()中会通知所有的观察者换肤开始,通常观察者只有一个。在doInBackground中调用DataManager#loadSkin。跟进。
public class DataManager implements ILoadSkin { ... @Override public void loadSkin(String pluginPackageName, String pluginPath, String suffix) { if (pluginPackageName != null && pluginPackageName.equals(getPluginPackageName()) && pluginPath != null && pluginPath.equals(getPluginPath()) && suffix != null && suffix.equals(getResourceSuffix())) { mDeliver.postDataManagerLoadError(); } else { savePluginPackageName(pluginPackageName); savePluginPath(pluginPath); saveResourceSuffix(suffix); mDeliver.postDataManagerLoadSuccess(pluginPackageName, pluginPath, suffix); } }}
else中三个save方法都是在SP文件中保存即将要应用的皮肤的信息,主要是suffix。
@Override public void postDataManagerLoadSuccess(String pluginPackageName, String pluginPath, String resourceSuffix) { SkinL.d("保存本次换肤的相关信息成功"); ResourceManager.getDefault().loadSkin(pluginPackageName, pluginPath, resourceSuffix); }
经过在DataManager中保存SP信息之后,由LoadSkinDeliver分发消息到ResourceManager#loadSkin。
@Override public void loadSkin(String pluginPackageName, String pluginPath, String suffix) { try { smartCreateResource(pluginPackageName, pluginPath, suffix, false); } catch (Exception e) { e.printStackTrace(); mIDeliver.postResourceManagerLoadError(false); } }
初始化的时候简单分析过smartCreateResource方法,和上次不同的是,这次最后一个参数firstInit为false。
private boolean smartCreateResource(String pluginPackageName, String pluginPath, String suffix, boolean firstInit) { boolean shouldCreate = checkIfReCreateDateResource(pluginPackageName, pluginPath, suffix); SkinL.d("should create resource : " + shouldCreate); if (shouldCreate) { try { createDataResource(pluginPackageName, pluginPath, suffix); mIDeliver.postResourceManagerLoadSuccess(firstInit, pluginPackageName, pluginPath, suffix); } catch (Exception e) { e.printStackTrace(); mIDeliver.postResourceManagerLoadError(firstInit); } } else { mResource.changeResourceSuffix(suffix); mIDeliver.postResourceManagerLoadSuccess(false, pluginPackageName, pluginPath, suffix); } return mResource != null; }
由于初始化的时候,默认创建了LocalResource,所以这里shouldCreate为false,走else的逻辑。mResource.changeResourceSuffix(suffix)
只是简单的记录night的后缀。之后继续由LoadSkinDeliver分发消息。
public void postResourceManagerLoadSuccess(final boolean firstInit, final String pluginPackageName, final String pluginPath, final String resourceSuffix) { SkinL.d("生成Resource对象成功"); mHandler.post(new Runnable() { @Override public void run() { if (firstInit && mOnInitLoadSkinResourceListener != null) { mOnInitLoadSkinResourceListener.onInitResourceSuccess(); } else { boolean findResourceSuccess = notifyAllChangeSkinObserverListToFindResource(); if (findResourceSuccess) { postGetAllResourceSuccessOnMainThread(pluginPackageName, pluginPath, resourceSuffix); } else { postGetResourceErrorOnMainThread(); } } } }); }
这次firstInit为false。终于要走换肤的逻辑了。。。
private boolean notifyAllChangeSkinObserverListToFindResource() { boolean findResourceSuccess = true; SkinL.d("通知所有的观察者查找资源"); for (IChangeSkin changeSkin : mChangeSkinObserverList) { findResourceSuccess = changeSkin.findResource(); if (!findResourceSuccess) { break; } } return findResourceSuccess; } @Override public void postGetAllResourceSuccessOnMainThread(String pluginPackageName, String pluginPath, String resourceSuffix) { SkinL.d("查找所有资源成功"); GlobalManager.getDefault().flushPluginInfos(pluginPackageName, pluginPath, resourceSuffix); notifyAllChangeSkinObserverListToApplySKin(); } private void notifyAllChangeSkinObserverListToApplySKin() { SkinL.d("通知所有的组件进行换肤"); for (IChangeSkin changeSkin : mChangeSkinObserverList) { changeSkin.changeSkin(); } }
所有的BaseSkinActivity对象都会被add到mChangeSkinObserverList属性。也就是说首先会调用所有BaseSkinActivity对象的findResource方法,找到所有换肤需要的资源,之后再统一调用changeSkin。逻辑缕清了,直接顺序查看这两个方法。
public class BaseSkinActivity extends AppCompatActivity implements IChangeSkin { @Override public boolean findResource() { ... List<SkinView> layoutInflaterSkinViewList = mSkinLayoutInflater.getLayoutInflaterSkinViewList(); for (IChangeSkin skinView : layoutInflaterSkinViewList) { findResourceSuccess = skinView.findResource(); if (!findResourceSuccess) { break; } } ... return findResourceSuccess; } @Override public void changeSkin() { List<SkinView> layoutInflaterSkinViewList = mSkinLayoutInflater.getLayoutInflaterSkinViewList(); for (IChangeSkin skinView : layoutInflaterSkinViewList) { skinView.changeSkin(); } }}
SkinView实现了IChangeSkin接口,在执行SkinInflaterFactory#onCreateView时,添加进mSkinViewList的View,作为需要换肤View的包装类,其中保存了需要换肤View的id。在调用mSkinLayoutInflater.getLayoutInflaterSkinViewList()
时,会将进行findViewById将id对应的View也存进SkinView。跟进。
public class SkinView implements IChangeSkin { @Override public boolean findResource() { boolean changed = true; for (BaseSkinAttr attr : mSkinAttrList) { changed = attr.findResource(); if (!changed) { break; } } return changed; } @Override public void changeSkin() { for (BaseSkinAttr attr : mSkinAttrList) { attr.applySkin(mView); } }}
mSkinAttrList中保存了View所有需要换肤的属性。目前AndSkin只支持三种属性,分别是“background”、”src”和”TextColor”。BaseSkinAttr是个抽象类,三个子类分别为:BackgroundAttr、SrcAttr和TextColorAttr。这里以BackgroundAttr为例分析,其余两个同理。
public class BackgroundAttr extends BaseSkinAttr { public BackgroundAttr(String mAttrType, String mAttrName, String mAttrValueRef) { super(mAttrType, mAttrName, mAttrValueRef); } @Override public boolean findResource() { resetResourceValue(); if (TYPE_ATTR_DRAWABLE.equals(mAttrType)) { mFindDrawable = ResourceManager.getDefault().getDataResource().getDrawableByName(mAttrValueRef); return mFindDrawable != null; } else if (TYPE_ATTR_COLOR.equals(mAttrType)) { mFindColor = ResourceManager.getDefault().getDataResource().getColorByName(mAttrValueRef); return mFindColor != Resource.VALUE_ERROR_COLOR; } return true; } @Override public void applySkin(View view) { if (TYPE_ATTR_DRAWABLE.equals(mAttrType) && mFindDrawable != null) { view.setBackgroundDrawable(mFindDrawable); SkinL.d(view + " : " + mAttrName + " apply " + mAttrValueRef); } else if (TYPE_ATTR_COLOR.equals(mAttrType) && mFindColor != Resource.VALUE_ERROR_COLOR) { view.setBackgroundColor(mFindColor); SkinL.d(view + " : " + mAttrName + " apply " + mAttrValueRef); } resetResourceValue(); }}
在findResource中通过ResourceManager.getDefault().getDataResource().getXXXByName
获取到mFindDrawable或者mFindColor。在applySkin方法中设置给View即可。到此,我们已经完整的看到了整个轮廓,现在唯一差的是ResourceManager.getDefault().getDataResource().getXXXByName
的实现细节,这也是换肤的真正精髓所在。
本地换肤使用的是LocalResource对象。
public class LocalResource extends Resource { public LocalResource(Context baseSkinActivity, String pluginPackageName, String pluginPath, String resourcesSuffix) { super(baseSkinActivity, pluginPackageName, pluginPath, resourcesSuffix); mResources = baseSkinActivity.getResources(); } @Override public Drawable getDrawableByName(String drawableResName) { Drawable trueDrawable = null; drawableResName = appendSuffix(drawableResName); SkinL.d("getDrawableByName drawableResName:" + drawableResName); try { int trueDrawableId = mResources.getIdentifier(drawableResName, "drawable", GlobalManager.getDefault().getPackageName()); trueDrawable = mResources.getDrawable(trueDrawableId); } catch (Exception e) { e.printStackTrace(); } return trueDrawable; }}public abstract class Resource { final String appendSuffix(String name) { if (!TextUtils.isEmpty(mResourcesSuffix)) { return name + "_" + mResourcesSuffix; } return name; }}
前面说到:mResource.changeResourceSuffix(suffix)
只是简单的记录night的后缀。现在就要用到这个night后缀了。在getDrawableByName中首先拼接名称,例如:将icon_search拼接为icon_search_night。之后通过mResources.getIdentifier
获取到拼接后的资源的id。之后再转换成相应的Drawable。至此,本地换肤流程完毕。
动态换肤
动态换肤和本地换肤相比,换肤的流程是一样的,唯一的区别在于动态换肤的资源在另一个apk文件中。本地换肤是通过应用的Resources获取到相应的资源,那么动态换肤需要处理的问题就是怎么获取到外部apk的Resources对象。这个和插件化加载资源是一样的,构造外部apk的AssetManager即可。核心代码如下:
public class PluginResource extends Resource { public PluginResource(Context baseSkinActivity, String pluginPackageName, String pluginPath, String resourcesSuffix) throws Exception { super(baseSkinActivity, pluginPackageName, pluginPath, resourcesSuffix); loadPlugin(); } private void loadPlugin() throws Exception { File file = new File(PATH_EXTERNAL_PLUGIN + "/" + mPluginPath); SkinL.d(file.getAbsolutePath()); if (mPluginPath == null || !file.exists()) { throw new IllegalArgumentException("plugin skin not exit, please check"); } AssetManager assetManager = null; assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, file.getAbsolutePath()); Resources superRes = mContext.getResources(); mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); SkinL.d("加载外部插件的皮肤成功"); } ...}
AndSkin优点
- 支持应用内换肤和动态换肤
- 不干预View的生成
AndSkin缺/槽点
由于在ConfigConstants写死了一堆常量。所以xml中的SKIN_NAMES_SPACE必须为“http://schemas.android.com/android/andSkin”。同理写死,状态栏的颜色值必须为status_bar_color。
为了不干预View的生成,存储了View的id,也就意味这同一个布局文件中需要换肤的View都设置id属性并且不能重复。假设需要在代码中动态inflate10个xml,那你要写10个相同的xml,只是View的id不同。这点特别坑爹。。。
src加载应该暴露个接口,让用户可以使用图片加载框架加载。。。
目前就发现这么多,欢迎吐槽。。
- Andorid 换肤框架AndSkin源码解析及优缺点
- andorid网络框架retrofit源码解析三
- Andorid事件分发源码解析
- WINDOWS 换肤的原理及解析
- SSM框架搭建及源码解析--AOP源码解析(五)
- Volley源码解析<九> 优缺点
- Android换肤原理和Android-Skin-Loader框架解析
- Andorid的优缺点
- SSM框架搭建及源码解析--框架搭建(一)
- 换肤框架
- Android换肤框架
- 换肤框架
- 换肤框架
- 换肤框架
- Andorid框架
- Alibaba-AndFix Bug热修复框架原理及源码解析
- Alibaba-Dexposed Bug框架原理及源码解析
- Alibaba-Dexposed Bug框架原理及源码解析
- ArcEngine 中把地图坐标转为屏幕坐标的方法
- 二进制开关工具类
- C语言提高-第20讲: 经典:查找的艺术(有序数组中插入数据)
- Java中null关键字的强转
- sequlize 简单操作
- Andorid 换肤框架AndSkin源码解析及优缺点
- [分治] 51Nod1555 布丁怪
- 特征匹配之Brute-Force 匹配和FLANN 匹配器
- 在HyperLedger Fabric中启用CouchDB作为State Database(区块链数据库)
- c语言操作符简介
- spark本地调试hive
- 杂题 [Ceoi2010]A huge tower
- 二叉树系列三:二叉树的深度遍历(DFS)
- Android5.0禁止状态栏下拉