Android动态换肤原理解析及实践(下篇)

皮肤包的加载过程:

SkinManger:

public void load(String skinPackagePath, final ILoaderListener callback) {

    new AsyncTask<String, Void, Resources>() {

        protected void onPreExecute() {
            if (callback != null) {
                callback.onStart();
            }
        };

        @Override
        protected Resources doInBackground(String... params) {
            try {
                if (params.length == 1) {
                    String skinPkgPath = params[0];

                    File file = new File(skinPkgPath); 
                    if(file == null || !file.exists()){
                        return null;
                    }

                    PackageManager mPm = context.getPackageManager();
                    PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
                    skinPackageName = mInfo.packageName;

                    AssetManager assetManager = AssetManager.class.newInstance();
                    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                    addAssetPath.invoke(assetManager, skinPkgPath);

                    Resources superRes = context.getResources();
                    Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

                    SkinConfig.saveSkinPath(context, skinPkgPath);

                    skinPath = skinPkgPath;
                    isDefaultSkin = false;
                    return skinResource;
                }
                return null;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        };

        protected void onPostExecute(Resources result) {
            mResources = result;

            if (mResources != null) {
                if (callback != null) callback.onSuccess();
                notifySkinUpdate();
            }else{
                isDefaultSkin = true;
                if (callback != null) callback.onFailed();
            }
        };

    }.execute(skinPackagePath);
}

@Override
public void attach(ISkinUpdate observer) {
    if(skinObservers == null){
        skinObservers = new ArrayList<ISkinUpdate>();
    }
    if(!skinObservers.contains(observer)){
        skinObservers.add(observer);
    }
}

@Override
public void detach(ISkinUpdate observer) {
    if(skinObservers == null) return;
    if(skinObservers.contains(observer)){
        skinObservers.remove(observer);
    }
}

@Override
public void notifySkinUpdate() {
    if(skinObservers == null) return;
    for(ISkinUpdate observer : skinObservers){
        observer.onThemeUpdate();
    }
}

SkinManager为整个皮肤包的管理类,负责加载皮肤包文件,并得到该皮肤包的包名skinPackageName,和这个皮肤包的Resource对象skinResource,这样整个皮肤包的资源文件我们就都可以拿到了。在加载得到皮肤包的Resource之后,通知每个注册过(attach)的页面(Activity),去刷新这些页面所有保存过的需要换肤的View,进行换肤操作。

切换时如何即时更新界面:

1、SkinBaseApplication:

public class SkinApplication extends BaseApplication {

@Override
public void onCreate() {
    super.onCreate();
    SkinManager.getInstance().init(this);
    SkinManager.getInstance().load();
  }
}

主要是进行一些初始化的操作。

2、SkinBaseActivity:

public abstract class BaseActivity extends
    code.solution.base.BaseActivity implements ISkinUpdate, IDynamicNewView {

private SkinInflaterFactory mSkinInflaterFactory;

@Override
protected void onCreate(Bundle savedInstanceState) {

mSkinInflaterFactory = new SkinInflaterFactory();
LayoutInflaterCompat.setFactory(getLayoutInflater(), mSkinInflaterFactory);
super.onCreate(savedInstanceState);
changeStatusColor();
}

/**
 * dynamic add a skin view
 *
 * @param view
 * @param attrName
 * @param attrValueResId
 */
protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){
    mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
}

@Override
public void onThemeUpdate() {
    if(!isResponseOnSkinChanging){
        return;
    }
    mSkinInflaterFactory.applySkin();
    changeStatusColor();
}

在这里使用了之前自定义的SkinInflaterFactory,来替换默认的Factory,以达到截获创建View,获取View的属性,与支持换肤的属性进行对比,进行View换肤操作以及保存这些需要换肤的View到List中,在下次换肤切换时对这些View进行换肤的目的。

其中换肤操作执行时,会调用SKinManager.notifySKinUpdate方法

@Override
public void notifySkinUpdate() {
    if(skinObservers == null) return;
    for(ISkinUpdate observer : skinObservers){
        observer.onThemeUpdate();
    }
}

而这里的observer.onThemeUpdate里面主要是执行这个Activity的下述方法:

public void onThemeUpdate() {
    if(!isResponseOnSkinChanging){
        return;
    }
    mSkinInflaterFactory.applySkin();
    changeStatusColor();
}

mSkinInflaterFactory.applySkin();即为SKinInflaterFactory的applySkin方法,

public void applySkin() {
    if (ListUtils.isEmpty(mSkinItems)) {
        return;
    }

    for (SkinItem si : mSkinItems) {
        if (si.view == null) {
            continue;
        }
        si.apply();
    }
  }

其中 mSKinItems即为当前Activity通过xml 文件中skin:enbale进行标记的 及动态dynamicAddSkinEnableView(...)添加的需要换肤的View的集合,这样整个换肤的过程就完成了。

整体换肤框架类图:


如何制作皮肤包:

1). 新建工程project

2). 将换肤的资源文件添加到res文件下,无java文件

3). 直接运行build.gradle,生成apk文件(注意,运行时Run/Redebug configurations 中Launch Options选择launch nothing),否则build 会报 no default Activty的错误。

4). 将apk文件重命名如black.apk,重命名为black.skin防止用户点击安装

在线换肤:

1). 将皮肤包上传到服务器后台

2). 客户端根据接口数据下载皮肤包,进行加载及客户端换肤操作

结语:

至此,整个换肤流程的原理解析已经全部讲完了。本文针对基本的换肤原理流程做了解析,初步建立了一套相对完善的换肤框架。但是如何建立一套更加完善更加对其他开发者友善的换肤机制仍然是可以继续研究的方向。比如如何更加安全的换肤,如何对代码的侵入性做到最小(比如通过在配置文件中配置需要换肤的View的id name 而不是通过在xml文件中进行标记)等等,都是可以继续研究的方向,以后有时间会继续在这方面进行探索。

因时间关系文章难免有疏漏,欢迎提出指正,谢谢。同时对换肤感兴趣的童鞋可以参考以下链接:

1、Android-Skin-Loader

2、Android-skin-support

3、Android主题换肤 无缝切换

相关阅读:Android动态换肤原理解析及实践(上篇)

本文来自网易实践者社区,经作者朱强龙授权发布。