插件化换肤技术,就是通过插件的方式,加载外部资源文件,无需更新App就可以实现换肤,具有耦合低,入侵小的特点。总结起来就是。
- 更好的用户体验,无闪烁换肤架构
- 扩展和维护方便,入侵性小,低耦合
- 插件化开发,任何APP都是你的皮肤包
- 立即生效,无需要重启APP
github项目地址 https://github.com/Rainpler/EnjoySkin
为实现插件化换肤的技术,需要先了解布局原理与资源加载原理,这些都在上一篇中进行了分析。整个插件化框架共分为三个部分,一个是主App,一个是换肤框架,另一个是资源包apk。
在资源加载原理中我们分析到,当ResourcesImpl创建完成后,又会接着调用getOrCreateResourcesLocked()去初始化Resources对象实例。最终将包装好的resources作为资源类返回,资源的信息都被存储在Resources中的ResourcesImpl中的Asset对象中。
所以实际上,Resource和ResourceImpl都是包装的壳,最终资源的读取都是通过assets来进行的。而此外,还有这些重要的API:
/*package*/ native final int getResourceIdentifier(String name,String defType,String defPackage);
/*package*/ native final String getResourceName(int resid);
/*package*/ native final String getResourcePackageName(int resid);
/*package*/ native final String getResourceTypeName(int resid);
/*package*/ native final String getResourceEntryName(int resid);
所以,我们就可以利用资源映射,通过主App中的资源id,寻找到其在资源apk对应的资源,并动态替换。本文直接从实战出发,讲解插件化换肤技术的基本流程。共分为以下五个步骤:
- 制作皮肤包
- 收集XML数据
- 统计换肤需要的属性
- 读取皮肤包资源
- 执行换肤
制作皮肤包
皮肤包其实就是一个apk文件,我们可以新建一个工程,将除了资源文件外的其他文件删除,然后build生成apk,就可以得到我们所需要的apk了。需要注意的是,该皮肤包中的资源文件命名必须与主工程中待替换的资源文件命名一致,这样才可以通过资源映射找到相关文件。
这样一来,不同的皮肤我们就可以打包成不同的apk,当执行换肤的时候就可以根据要换肤的apk,映射到相关的资源文件中。
收集XML数据
我们执行换肤操作的时候,需要先统计,在布局原理中我们讲到,view的创建过程中,如果factory不为空,则view的创建会被拦截,所以我们利用view生产对象的过程中的Factory2接口,自定义SkinLayoutInflateFactory。
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
Factory2中有一个抽象方法onCreateView(),在view的创建时会调用该方法,所以我们可以直接沿用LayoutInflater源码中的实现,只需要在适当的位置添加我们自己的逻辑就可以了,在这里,我们需要统计每一个view需要换肤的属性。
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//换肤就是在需要时候替换 View的属性(src、background等)
//所以这里创建 View,从而修改View属性
View view = createSDKView(name, context, attrs);
if (null == view) {
view = createView(name, context, attrs);
}
//这就是我们加入的逻辑
if (null != view) {
//加载属性
skinAttribute.look(view, attrs);
}
return view;
}
统计需要换肤的属性
为了统计需要换肤的属性,我们定义了一个SkinAttribute的类,包含两个重要的属性。
- SkinPair 记录一个属性 属性名字——对应的资源id
- SkinView 一个view对应多个属性 List<SkinPair>
而SkinPair是其一个内部类,用于记录一对属性的名称与id。
- attributeName 属性名
- int resId 对应的资源id
SkinView也是其一个内部类,用于记录一个view对应的多个属性SkinPair
- view view对象
- List<SkinPair> 对应的属性列表
look方法实现如下:
//记录下一个VIEW身上哪几个属性需要换肤textColor/src
public void look(View view, AttributeSet attrs) {
List<SkinPair> mSkinPars = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获得属性名 textColor/background
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
// #
// ?andorid:
// @drawable/xxx.png
String attributeValue = attrs.getAttributeValue(i);
// 比如color 以#开头表示写死的颜色 不可用于换肤
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
// 以 ?开头的表示使用 属性
if (attributeValue.startsWith("?")) {
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
// 正常以 @ 开头
resId = Integer.parseInt(attributeValue.substring(1));
}
SkinPair skinPair = new SkinPair(attributeName, resId);
mSkinPars.add(skinPair);
}
}
if (!mSkinPars.isEmpty() || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, mSkinPars);
// 如果选择过皮肤 ,调用 一次 applySkin 加载皮肤的资源
skinView.applySkin();
mSkinViews.add(skinView);
}
}
读取皮肤包资源
为了读取皮肤包的资源,我们再创建一个SkinResource的类,用于管理读取资源的方法。前文中提到的几个Asset的重要Api,则是在这里用到。
public class SkinResources {
private String mSkinPkgName;
private boolean isDefaultSkin = true;
// app原始的resource
private Resources mAppResources;
// 皮肤包的resource
private Resources mSkinResources;
private SkinResources(Context context) {
mAppResources = context.getResources();
}
//单例
private volatile static SkinResources instance;
public static void init(Context context) {
if (instance == null) {
synchronized (SkinResources.class) {
if (instance == null) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
return instance;
}
public void reset() {
mSkinResources = null;
mSkinPkgName = "";
isDefaultSkin = true;
}
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
//是否使用默认皮肤
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
/**
* 1.通过原始app中的resId(R.color.XX)获取到自己的 名字
* 2.根据名字和类型获取皮肤包中的ID
*/
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
String resName = mAppResources.getResourceEntryName(resId);
String resType = mAppResources.getResourceTypeName(resId);
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
return skinId;
}
/**
* 输入主APP的ID,到皮肤APK文件中去找到对应ID的颜色值
*
* @param resId
* @return
*/
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
//通过 app的resource 获取id 对应的 资源名 与 资源类型
//找到 皮肤包 匹配 的 资源名资源类型 的 皮肤包的 资源 ID
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
/**
* 可能是Color 也可能是drawable
*
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if ("color".equals(resourceTypeName)) {
return getColor(resId);
} else {
// drawable
return getDrawable(resId);
}
}
}
应用皮肤工厂
那么我们的自定义Factory类需要在什么时候应用呢?由于setContenView()是在Activity的onCreate()函数中执行的,所以我们需要对Activity的生命周期进行一个观察,这就要用到ActivityLifecycleCallbacks。
public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {
private Observable mObserable;
private ArrayMap<Activity, SkinLayoutInflaterFactory> mLayoutInflaterFactories = new
ArrayMap<>();
public ApplicationActivityLifecycle(Observable observable) {
mObserable = observable;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
/**
* 更新状态栏
*/
SkinThemeUtils.updateStatusBarColor(activity);
/**
* 更新布局视图
*/
//获得Activity的布局加载器
LayoutInflater layoutInflater = activity.getLayoutInflater();
try {
//Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
//如设置过抛出一次
//设置 mFactorySet 标签为false
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//使用factory2 设置布局加载工程
SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory
(activity);
LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);
mObserable.addObserver(skinLayoutInflaterFactory);
}
...
}
皮肤管理器
public class SkinManager extends Observable {
private volatile static SkinManager instance;
/**
* Activity生命周期回调
*/
private ApplicationActivityLifecycle skinActivityLifecycle;
private Application mContext;
/**
* 初始化 必须在Application中先进行初始化
*
* @param application
*/
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}
private SkinManager(Application application) {
mContext = application;
//共享首选项 用于记录当前使用的皮肤
SkinPreference.init(application);
//资源管理类 用于从 app/皮肤 中加载资源
SkinResources.init(application);
//注册Activity生命周期,并设置被观察者
skinActivityLifecycle = new ApplicationActivityLifecycle(this);
application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
//加载上次使用保存的皮肤
loadSkin(SkinPreference.getInstance().getSkin());
}
public static SkinManager getInstance() {
return instance;
}
/**
* 记载皮肤并应用
*
* @param skinPath 皮肤路径 如果为空则使用默认皮肤
*/
public void loadSkin(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
//还原默认皮肤
SkinPreference.getInstance().reset();
SkinResources.getInstance().reset();
} else {
try {
//宿主app的 resources;
Resources appResource = mContext.getResources();
//
//反射创建AssetManager 与 Resource
AssetManager assetManager = AssetManager.class.newInstance();
//资源路径设置 目录或压缩包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);
//根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resources
Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics
(), appResource.getConfiguration());
//获取外部Apk(皮肤包) 包名
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
SkinResources.getInstance().applySkin(skinResource, packageName);
//记录
SkinPreference.getInstance().setSkin(skinPath);
} catch (Exception e) {
e.printStackTrace();
}
}
//通知采集的View 更新皮肤
//被观察者改变 通知所有观察者
setChanged();
notifyObservers(null);
}
}
为了记录换肤后的属性,我们还需要一个pref来保存
public class SkinPreference {
private static final String SKIN_SHARED = "skins";
private static final String KEY_SKIN_PATH = "skin-path";
private volatile static SkinPreference instance;
private final SharedPreferences mPref;
public static void init(Context context) {
if (instance == null) {
synchronized (SkinPreference.class) {
if (instance == null) {
instance = new SkinPreference(context.getApplicationContext());
}
}
}
}
public static SkinPreference getInstance() {
return instance;
}
private SkinPreference(Context context) {
mPref = context.getSharedPreferences(SKIN_SHARED, Context.MODE_PRIVATE);
}
public void setSkin(String skinPath) {
mPref.edit().putString(KEY_SKIN_PATH, skinPath).apply();
}
public void reset() {
mPref.edit().remove(KEY_SKIN_PATH).apply();
}
public String getSkin() {
return mPref.getString(KEY_SKIN_PATH, null);
}
}