插件化换肤
2022-02-28 本文已影响0人
w达不溜w
插件化换肤的优点
1)换肤无闪烁,立即生效,无需重启APP,用户体验好
2)扩展和维护方便,入侵性小,低耦合
3)插件化开发,任何APP可以是你的皮肤包
思路
换肤就是在需要时候替换 View的属性(src、background、textColor等),利用Android加载资源的流程,来加载第三方皮肤包。
1、收集XML数据
如何去收集布局文件中的信息呢?通过查看setContentView源码分析,利用View的实例化流程,替换LayoutInflater类中的mFactory2变量,mFactory2在Activity启动之前就已经赋值了,LayoutInflater提供了修改mFactory2的入口(setFactory2方法)
public void setFactory2(Factory2 factory) {
//调用setFactory2之后,再次调用会抛出异常
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
//第一次调用mFactorySet会被赋值为true,所以设置自定义的Factory2需要修改mFactorySet为false
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
自定义SkinLayoutInflaterFactory用来接管系统的View的生产过程
public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2{
// 当选择新皮肤后需要替换View与之对应的属性
// 页面属性管理器
private SkinAttribute skinAttribute;
// 用于获取窗口的状态框的信息
private Activity activity;
//仿系统AppCompatViewInflater具体实例化View
private SkinAppCompatViewInflater mAppCompatViewInflater;
public SkinLayoutInflaterFactory(Activity activity) {
this.activity = activity;
skinAttribute = new SkinAttribute();
}
@Override
public View onCreateView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
//可以处理不同版本实例化View (仿照系统类AppCompatViewInflater)
mAppCompatViewInflater = new SkinAppCompatViewInflater();
}
//所以这里创建 View,从而修改View属性
View view = mAppCompatViewInflater.createView(parent, name, context, attrs, false,
false,
true,
VectorEnabledTintResources.shouldBeUsed());
//这就是我们加入的逻辑,收集xml数据
if (null != view) {
//记录属性
skinAttribute.look(view, attrs);
}
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
}
使用factory2 设置布局加载工厂
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);
对于android Q上无法二次setFactroy的问题,同样的思路,反射LayoutInflaterCompat中的sCheckedField字段设置为false和修改mFactory2的值
2、记录需要换肤的属性
上面在收集xml数据的时候已经记录了View对应的属性,通过遍历筛选并记录需要换肤的属性
//需要换肤的属性集合
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
//...
}
//遍历筛选需要操作的属性信息
public void look(View view, AttributeSet attrs) {
//SkinPair记录一个属性(属性名--对应资源id)
List<SkinPair> mSkinPars = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获得属性名,如:textColor
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
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记录一个View对应多个属性(View--List<SkinPars>)
SkinView skinView = new SkinView(view, mSkinPars);
// 如果选择过皮肤 ,调用一次applySkin加载皮肤的资源
skinView.applySkin();
//mSkinViews记录多个View(List<SkinView>)
mSkinViews.add(skinView);
}
}
3、加载皮肤包资源
先来分析Resource创建过程:
ActivityThread#handleBindApplication()
> final ContextImpl appContext = ContextImpl.createAppContext(this, data.info)
> context.setResources(packageInfo.getResources())
> mResources = ResourcesManager.getInstance().getResources(...)
> return getOrCreateResources(activityToken, key, classLoader)
> ResourcesImpl resourcesImpl = createResourcesImpl(key)
> final AssetManager assets = createAssetManager(key)
最终创建了一个AssetManager对象去加载资源,我们可以利用反射执行AssetManager的addAssetPath方法去设置皮肤包的路径,然后创建一个Resource对象去加载皮肤包中的资源
try {
//宿主app的 resources;
Resources appResource = mContext.getResources();
//反射创建AssetManager 与 Resource
AssetManager assetManager = AssetManager.class.newInstance();
//反射获取addAssetPath方法 资源路径设置 目录或压缩包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
//反射调用addAssetPath方法
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);
} catch (Exception e) {
e.printStackTrace();
}
4、修改属性的值(属性的值来自皮肤包)
AssetManager中提供了获取资源id的核心方法:
//获取资源id
@AnyRes int getResourceIdentifier(@NonNull String name, @Nullable String defType,
@Nullable String defPackage)
//通过资源id获取资源类型名,如drawable,mipmap
@Nullable String getResourceTypeName(@AnyRes int resId)
//通过资源id获取资源名
@Nullable String getResourceEntryName(@AnyRes int resId)
上面已经创建到了皮肤包的Resource对象,我们可以根据资源名、资源类型和包名获取皮肤包中的资源id,然后可以皮肤包中资源id获取对应属性值
public int getIdentifier(int resId){
String resName=mAppResources.getResourceEntryName(resId);
String resType=mAppResources.getResourceTypeName(resId);
int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName);
return skinId;
}
//根据主APP的ID,到皮肤APK文件中去找到对应ID的颜色值
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);
}
然后对View中所有支持的属性进行修改
public void applySkin() {
for (SkinPair skinPair : skinPairs) {
switch (skinPair.attributeName) {
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
(skinPair.resId));
break;
//...
default:
break;
}
}
}