0x0 背景
无论是出于用户个性化的考虑,或者是不同场景下的氛围渲染,客户端应用存在着换肤的需求。本文举出三种常见的换肤方案,并加以对比,以作后续参考。无论何种方案,换肤的核心都包含皮肤的管理,皮肤的加载,以及皮肤的生效。不同的方案在解决这些问题上有不同的思路。
0x1 手动重新设置UI资源
这种方式最简单,在业务代码里面手动写设置新皮肤的逻辑,当新皮肤下发时,回调该逻辑重新设置UI资源,就达到了换肤的逻辑。这种方案思路简单,但是业务入侵的,需要手动写代码,有维护成本。而且由于需要手动写重新设置UI的逻辑,因此一般不会对所有的控件都更换UI资源,因此换肤的范围存在局限性。
public void onSkinChanged(JSONObject newSkin) {
int newTextColor = newSkin.optInt("my_text_color");
TextView myTextView = (TextView)findViewById("R.id.mytext");
myTextView.setTextColor(newTextColor);
String newImageUrl = newSkin.optString("my_img_url");
UrlImageView myImageView = (UrlImageView)findViewById("R.id.myimage");
myImageView.setImageUrl(newImageUrl);
}
0x2 自定义资源框架
Android原生的资源都是通过Resources加载定义在res目录下的xml中的资源。由于res目录下的资源是随打包发布的,因此无法做到动态替换。所以这里的思路是自定义一个CustomResources,并自定义resources目录,里面区分default,night, custom等等。default目录下是默认资源,其他目录下是特定场景的皮肤资源,其id和default目录保持一致,就可以实现替换。
resources
|--- default
|--- res
|--- drawable
|--- night
|--- res
|--- drawable
CustomResources可以加载APK包里面的默认资源,也可以加载从服务端下发的资源。在应用开发过程中,不能再使用系统的Resources,必须使用CustomResources,否则无法实现换肤。
TextView myTextView = (TextView)findViewById("R.id.mytext");
myTextView.setTextColor(getCustomResrouce().getColor(R.color.mytextcolor));
当然至此为止还不能cover布局xml中直接使用资源id的场景,如果要cover,需要hook系统resources。这种方式需要在应用开发之初就有换肤的设计,开发团队内部约定统一用CustomResources而不用系统的资源。
0x3 Hook系统LayoutInflater
这种思路是hook系统的LayoutInflater,在解析布局xml生成View的时候,替换成继承了换肤逻辑的子类。ximsfei/Android-skin-support是这种思路的代表。替换LayoutInflater的方式是:
private void installLayoutFactory(Context context) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
try {
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
在ximsfei/Android-skin-support的实现方案中,下发皮肤其实是在一个只包含资源文件的APK文件中,保证资源APK中的资源命名和默认的保持一致就能实现替换。读取APK文件中的资源的方式是:
public Resources getSkinResources(String skinPkgPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = mAppContext.getResources();
return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
这种方式对于应用开发者来说几乎是透明的,对于业务也没有侵入性,尤其是已有的应用代码上迭代增加换肤功能,这是一种比较好的方式。