目标
1、启动时能够自动加载皮肤包
2、能动态进行皮肤切换
3、能支持在线下载皮肤
思路
利用Android App加载资源的流程,来加载第三方皮肤包。
皮肤包加载流程
1、C++层读取资源文件(类似于一个数据库表,有属性名、id和对应资源/路径的对应关系)
private static native long nativeLoad(@FormatType int format, @NonNull String path,
@PropertyFlags int flags, @Nullable AssetsProvider asset) throws IOException;
2、通过AssetManager.addAssetPath加载皮肤包里面的资源文件
// 这个方法只能通过反射才能调到
@Deprecated
@UnsupportedAppUsage
public int addAssetPath(String path) {
return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
}
// API 30 以上可以直接setApkAssets(ApkAssets[], boolean),该类里面重载了许多工厂方法,可以直接构建对应的Assets
// 之后再通过AssetManager.setApkAssets即可完成设置
public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
return loadFromPath(path, 0 /* flags */);
}
3、构造Resource对象
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
4、通过自己的Inflater,在Inflater是记录需要换肤的属性
5、执行换肤(setBackground,setBackgroundColor等)
View Inflate流程
1、在Activity中SetContentView,或者在Fragment中使用传过来的Inflater进行inflater。
2、不论使用那种方式,最终会都会以 LayoutInflater.from(mContext).inflate(resId, contentParent);
这种方式调用
3、进一步走到 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
4、进一步走到:我们看到这里有两个factory,factory2默认是为空的,mPrivateFactory是安卓提供的。
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
return view;
}
5、我们可以使用一个FactoryImpl继承上述Factory,然后重写onCreateView(),注入到Android中的Inflater中,但在setFactory的时候,我们发现有个标记在第一次使用后被设置为true了,导致我们无法在之后自己setFactory,这里需要通过反射将这个标记设置为true。
public void setFactory(Factory factory) {
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;
if (mFactory == null) {
mFactory = factory;
} else {
mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
}
}
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
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;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
6、通过反射将mFactorySet设置为false即可调用,LayoutInflaterCompat.setFactory/setFactory2了。
LayoutInflater layoutInflater = activity.getLayoutInflater();
try {
@SuppressLint("SoonBlockedPrivateApi")
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity);
LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);
mObservable.addObserver(skinLayoutInflaterFactory);
然后我们只需要在SkinLayoutInflaterFactory.onCreateView的时候,通过AttributeSet遍历到所有需要换肤的属性,然后记录到List,之后在对应的位置applySkin()就可以了。
我们记录属性的数据结构如下:
skinAttribute 含List<SkinView>
SkinView 含VIew、List<SkinPair>
SkinPair 含attrName和resId,例如android:background和@drawable/abc_vector_test
7、根据attrName,attrValue来决定要不要设置换肤,attrName需要mAttributes ,同时attrValue.charAt(0)不应该是"#",#类型的id不能被直接换肤,因为是直接写死的。之后将attrName和attrValue封装成SkinPair即可。
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
8、有了attrName和attrValue即可获取对应的颜色或者drawable等资源。
/**
* 1. 通过原始app中的resId(@color/xxx)获取到名字,类型
* 2. 根据名字类型在资源包中获取到对应的resId
* R.color.Red(100001) -> Color/Red -> R.color.Red(20013)
*/
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
String resName = mAppResources.getResourceEntryName(resId);
String resType = mAppResources.getResourceTypeName(resId);
return mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
}
/**
* 根据Id获取到资源包里面对应的color
*/
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);
}
9、在点击按钮的时候applySkin,遍历对应的SkinView然后set对应的资源接口,在onCreateView的时候也需要applySkin一次。