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优点

  1. 支持应用内换肤和动态换肤
  2. 不干预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加载应该暴露个接口,让用户可以使用图片加载框架加载。。。

目前就发现这么多,欢迎吐槽。。

原创粉丝点击