Andorid 换肤框架AndSkin源码解析及优缺点
Posted 一口仨馍
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Andorid 换肤框架AndSkin源码解析及优缺点相关的知识,希望对你有一定的参考价值。
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源码解析及优缺点的主要内容,如果未能解决你的问题,请参考以下文章