Android换肤原理分析
由于最近项目需要用到换肤功能,所以学习了下相关的知识,下面简单的说下我的总结
换肤思路:
(1)、找到需要换肤的View及其需要改变的属性
(2)、拿到需要换肤包(一般是APK,里面只有res文件下的东西即可)
(3)、通过本包的资源名称去拿到皮肤包的具体资源(所以需要资源名称之间有个关系,本文中相等
(4)、把皮肤包的资源设置到需要换肤的View上,实现换肤功能。
1、那么我们怎么去找到需要换肤的View呢?
首先我们要先了解android View是怎么从xml解析成View的,这里分析的是Activity。
以setContentView方法作为入口:可以看出调用了getWindow().setContentView(layoutResID),而这个getWindow()就是PhoneWindow
在这里主要是调用了LayoutInflater的inflate方法
这里代码太多了,删减了一部无关代码,可以看出通过createViewFromTag(root, name, inflaterContext, attrs)我们就得到想要的View实例
尝试去创建View
关键点来了,我们发现这里有个mFactory2的东西,他其实就是一个接口,继承自Factory,只有一个未实现的onCreateView方法,可以实现了这个接口,然后干预的View创建流程
那么我们怎么去实现,自定义这个Factory2方法呢?原来Layoutmanager开放了这个使用,那就好办了
那么我们实现一下这个方法看看:
打印出来的就是当前Activity的所有的View的名称,而第四个参数attrs就是我们在Xml里面设置的属性
到此又产生了疑问,我怎么确定哪个View要换肤呢?新建属性值
在需要换肤的View加上该属性
现在通过这里就可以打印出,为true的话就是我们要替换皮肤的View,我们可以用一个List保存下来,同时,需要知道要换肤的属性,这个也可以换肤的时候过滤下。
那么我们这时候能确定哪个view需要换肤了,但是我们无法拿到View,因为这个方法没有实现,所以我们仿照源码里面实现了一个类
public class SkinChangeFactory2 implements LayoutInflater.Factory2 {
static final Class<?>[] mConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
new HashMap<String, Constructor<? extends View>>();
final Object[] mConstructorArgs = new Object[2];
private Context mContext;
public static List<ViewAttrs> skinViews = new ArrayList<>();
private String[] prefixs = new String[]{
"android.view.",
"android.widget."
};
public SkinChangeFactory2(Context context) {
mContext = context;
}
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
View view;
if (-1 == name.indexOf('.')) {
view = createView(context, name, prefixs, attrs);
} else {
view = createView(context, name, null, attrs);
}
return view;
}
private View createView(Context context, String name, String[] prefix, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
if (prefix != null) {
for (String s : prefix) {
try {
clazz = Class.forName((s + name), false, mContext.getClassLoader()).asSubclass(View.class);
} catch (ClassNotFoundException e) {
continue;
}
constructor = clazz.getConstructor(mConstructorSignature);
}
} else {
clazz = Class.forName(name, false, mContext.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
}
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
View view = constructor.newInstance(args);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.skin_change, android.R.attr.textViewStyle, 0);
boolean isSupportChange = a.getBoolean(R.styleable.skin_change_is_support_change, false);
if (isSupportChange) {
for (int i = 0; i < attrs.getAttributeCount(); i++) {
if (attrs.getAttributeName(i).equals("textColor")) {
skinViews.add(new ViewAttrs(view, attrs.getAttributeValue(i)));
}
}
}
a.recycle();
return view;
} catch (IllegalAccessException | InvocationTargetException |
InstantiationException e) {
e.printStackTrace();
} finally {
mConstructorArgs[0] = lastContext;
}
} catch (NoSuchMethodException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
private static final ClassLoader BOOT_CLASS_LOADER = LayoutInflater.class.getClassLoader();
private final boolean verifyClassLoader(Constructor<? extends View> constructor) {
final ClassLoader constructorLoader = constructor.getDeclaringClass().getClassLoader();
if (constructorLoader == BOOT_CLASS_LOADER) {
// fast path for boot class loader (most common case?) - always ok
return true;
}
// in all normal cases (no dynamic code loading), we will exit the following loop on the
// first iteration (i.e. when the declaring classloader is the contexts class loader).
ClassLoader cl = mContext.getClassLoader();
do {
if (constructorLoader == cl) {
return true;
}
cl = cl.getParent();
} while (cl != null);
return false;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
}
我们把LayoutInflater.Factory2提出来了,通过skinViews去存储要换肤的View,当然我们可以设置一个Model去存储相对应的View需要改变的属性,然后把View一起放进去封装起来。
主要是这里,我们把View放进了集合。
2、第一步已经实现了,皮肤包的制作就不过多阐述了,就是新建个项目,把资源名字对应上就行,然后打包放到本项目assert里面
3、通过本APP里面的项目名称去对应拿到皮肤包里面的资源
先上代码在分析:
public class SkinManger {
private Resources skinRes;
private Resources mRes;
private Context mContext;
public SkinManger(Context context) {
this.mContext = context;
mRes = mContext.getResources();
}
@SuppressLint("DiscouragedPrivateApi")
public void getSkinResource() {
AssetManager assetManager = null;
try {
Class<?> aClass = Class.forName("android.content.res.AssetManager");
assetManager = (AssetManager) aClass.newInstance();
Method method = aClass.getDeclaredMethod("addAssetPath", String.class);
String path = copyAssetToCache(mContext, "model.apk");
if (TextUtils.isEmpty(path)) return;
method.invoke(assetManager, path);
} catch (ClassNotFoundException | InstantiationException |
IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
e.printStackTrace();
}
skinRes = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
mContext.getResources().getConfiguration());
}
public int getColorInSkin(int colorId) {
String resName = mRes.getResourceEntryName(colorId);
String typeName = mRes.getResourceTypeName(colorId);
int skinResId = skinRes.getIdentifier(resName, typeName, skinRes.getResourcePackageName(colorId));
if (skinResId > 0) return ResourcesCompat.getColor(skinRes, skinResId, null);
return ResourcesCompat.getColor(mRes, colorId, null);
}
public int getColorInSkin(String colorId) {
int i = Integer.parseInt(colorId.substring(1));
int colorInSkin = getColorInSkin(i);
return colorInSkin;
}
public static String copyAssetToCache(Context context, String fileName) {
try {
File cacheDir = context.getCacheDir();
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
File outFile = new File(cacheDir, fileName);
if (outFile.exists()) outFile.delete();
boolean res = outFile.createNewFile();
if (!res) return "";
InputStream is = context.getAssets().open(fileName);
FileOutputStream fos = new FileOutputStream(outFile);
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = is.read(buffer)) != -1) {
fos.write(buffer, 0, byteCount);
}
fos.flush();
is.close();
fos.close();
return outFile.getAbsolutePath();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}
其实主要是分两点:
1)获取要拿到资源的Resource对象,由于AssetManager不能new,所以采用反射的方式创建,而他的addAssetPath方法正是关键点所在,把皮肤包在手机的地址扔进去,他就可以工作了。
2)获取对应的资源,本例子是获取的颜色值,首先要理解Resource的几个方法:
getResourceEntryName:返回的是资源的名字(R.color.red返回的就是red)
getResourceTypeName:返回的是资源的类型(R.color.red返回的就是color)
getResourcePackageName:返回的是资源所在的包路径
getResourceName:返回的是资源在包内的路径
然后我们通过getIdentifier方法获取到了本APP中要替换的资源名在皮肤包中的资源ID,最后再通过
ResourcesCompat.getColor方法获取到了对应的颜色
好了到此其实基本上已经实现了简单功能了,最后就是把颜色赋值到对应的View上就可以了,简单的调用:
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private SkinManger skinManger;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflater.from(this).setFactory2(new SkinChangeFactory2(this));
setContentView(R.layout.activity_main);
skinManger = new SkinManger(this);
skinManger.getSkinResource();
}
public void clickc(View view) {
changeViewSkin();
}
private void changeViewSkin() {
for (ViewAttrs skinView : SkinChangeFactory2.skinViews) {
if (skinView.view instanceof TextView) {
((TextView) skinView.view).setTextColor(skinManger.getColorInSkin(skinView.color));
}
}
}
}
以上就是我总结的换肤功能实现,第一次写博客,错误的地方还请大家指正。