android 换肤框架详解3-自动换肤原理梳理
前面已经介绍了换肤的基本逻辑和LayoutInflater源码解析传送门如下
android 换肤框架详解1-换肤逻辑基本-CSDN博客
android 换肤框架详解2-LayoutInflater源码解析-CSDN博客
android 换肤框架详解3-自动换肤原理梳理-CSDN博客
接下来就是整体自动换肤逻辑了
代码如下
- 自定义LayoutInflater
import android.content.Context;
import android.view.LayoutInflater;public class SkinLayoutInflaterT extends LayoutInflater {protected SkinLayoutInflaterT(LayoutInflater original, Context newContext) {super(original, newContext);//创建解析接口监听类setFactory2(new SkinLayoutInflaterFactoryT());}@Overridepublic LayoutInflater cloneInContext(Context newContext) {return new SkinLayoutInflaterT(this, newContext);}public static LayoutInflater from(Context context) {LayoutInflater original = LayoutInflater.from(context);return new SkinLayoutInflaterT(original, context);}}
- 创建监听类LayoutInflater.Factory2
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;import java.lang.reflect.Field;
import java.util.HashMap;public class SkinLayoutInflaterFactoryT implements LayoutInflater.Factory2 {public static final String TAG = "SkinLayoutInflaterFactoryT";/*** 在解析创建的时候,会执行这方法,在这个方法创建View并且保存对应信息*/@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {Log.d(TAG, "SkinInflaterFactory onCreateView(), create view name=" + name + " ");//自己解析创建ViewView view = createView(context, name, attrs);//获取xml中所有属性,进行解析int count = attrs.getAttributeCount();HashMap<String, SkinAttrT> hashMap = null;for (int i = 0; i < count; i++) {//解析属性String attributeName = attrs.getAttributeName(i);// 属性名,如 "layout_width"String attributeValue = attrs.getAttributeValue(i); // 属性值,如 "match_parent"//资源文件获取到的值前面会加一个@,这里判断属性值是否是资源文件if (!attributeValue.startsWith("@")) {continue;}//判断是否在自己已经支持的View设置方法if (isSupport(attributeName)) {//拿到Res值int resId = Integer.parseInt(attributeValue.substring(1));if (resId == 0) {continue;}if (hashMap == null) {hashMap = new HashMap<>();}//返回格式:ic_launcher(仅资源名)R.drawable.ic_launcher,R.color.ic_launcherString attrValueName = context.getResources().getResourceEntryName(resId);//资源类型类型drawable,colorString attrValueType = context.getResources().getResourceTypeName(resId);//创建需要保存的属性和对应的值//这里保存设置方法,ResId值,resId的名字,resId的类型SkinAttrT skinAttrT = new SkinAttrT(attributeName, resId, attrValueName, attrValueType);//一个View的xml中可能有多个设置方法和resId,这里设置hashMap.put(attributeName, skinAttrT);}Log.d(TAG, "attributeName: " + attributeName + " = " + attributeValue);}if (view != null && hashMap != null) {//将属View和其对应的属性值进行保存SkinManagerT.getInstance().saveSkinView(view, hashMap);}return view;}public boolean isSupport(String attributeName) {return SkinConstantT.getSkinConstantSet().contains(attributeName);}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}private View createView(Context context, String name, AttributeSet attrs) {View view = null;try {LayoutInflater inflater = LayoutInflater.from(context);assertInflaterContext(inflater, context);if (-1 == name.indexOf('.')) {if ("View".equals(name) || "ViewStub".equals(name) || "ViewGroup".equals(name)) {view = inflater.createView(name, "android.view.", attrs);}if (view == null) {view = inflater.createView(name, "android.widget.", attrs);}if (view == null) {view = inflater.createView(name, "android.webkit.", attrs);}} else {view = inflater.createView(name, null, attrs);}} catch (Exception ex) {Log.e(TAG, "createView(), create view failed" + ex);view = null;}return view;}private void assertInflaterContext(LayoutInflater inflater, Context context) {Context inflaterContext = inflater.getContext();if (inflaterContext == null) {setField(inflater, "mContext", context);}//设置mConstructorArgs的第一个参数contextObject[] constructorArgs = (Object[]) getField(inflater, "mConstructorArgs");if (null == constructorArgs || constructorArgs.length < 2) {//异常,一般不会发生constructorArgs = new Object[2];setField(inflater, "mConstructorArgs", constructorArgs);}//如果mConstructorArgs的第一个参数为空,则设置为mContextif (null == constructorArgs[0]) {constructorArgs[0] = inflater.getContext();}}public static Object setField(Object receiver, String fieldName, Object value) {try {Field field;field = findField(receiver.getClass(), fieldName);if (field == null) {return null;}field.setAccessible(true);Object old = field.get(receiver);field.set(receiver, value);return old;} catch (IllegalAccessException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();}return null;}private static Field findField(Class<?> clazz, String name) {try {return clazz.getDeclaredField(name);} catch (NoSuchFieldException e) {if (clazz.equals(Object.class)) {e.printStackTrace();return null;}Class<?> base = clazz.getSuperclass();return findField(base, name);}}//获取类的实例的变量的值public static Object getField(Object receiver, String fieldName) {return getField(null, receiver, fieldName);}private static Object getField(String className, Object receiver, String fieldName) {Class<?> clazz = null;Field field;if (!TextUtils.isEmpty(className)) {try {clazz = Class.forName(className);} catch (ClassNotFoundException e) {e.printStackTrace();}} else {if (receiver != null) {clazz = receiver.getClass();}}if (clazz == null) return null;try {field = findField(clazz, fieldName);if (field == null) return null;field.setAccessible(true);return field.get(receiver);} catch (IllegalAccessException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();} catch (NullPointerException e) {e.printStackTrace();}return null;}
}
- 创建属性值字段SkinConstantT
import java.util.HashSet;/**已经适配的属性值*/
public class SkinConstantT {public static final String BACKGROUND = "background";public static final String TEXT_COLOR = "textColor";public static final String COLOR = "color";public static final HashSet<String> skinConstantSet = new HashSet<>();static {skinConstantSet.add(BACKGROUND);skinConstantSet.add(TEXT_COLOR);}public static HashSet<String> getSkinConstantSet() {return skinConstantSet;}
}
- 创建属性值保存类SkinAttrT
import android.util.Log;public class SkinAttrT {public static final String TAG = "SkinAttrT";/*** 方法名称,比如android:background="@color/content",这里是background* 这里用于代码中调用对应的方法,比如调用setBackground*/public String attrName;/*** ResID,比如R文件中的Id,R.color.red=0x-- ,R.drawable.--,这里就是0x--* 这里用于默认Resource进行修改值,默认res直接通过这个拿到资源文件*/public int attrValueRefId;/*** ResID对应的名称,比如android:background="@color/content",这里就是content* 这个是换肤资源resource拿对应 的resId,要和attrValueTypeName一起用* getIdentifier(* attrValueRefName,// 资源名* attrValueTypeName,// 资源类型* "应用包名"* );*/public String attrValueRefName;/*** ResID类型名称,比如比如android:background="@color/content",这里就是color* 这个是换肤资源resource拿对应 的resId,要和attrValueRefId一起用* getIdentifier(* attrValueRefName,// 资源名* attrValueTypeName,// 资源类型* "应用包名"* );*/public String attrValueTypeName;public Object[] attrValueFormat;public SkinAttrT(String attrName, int attrValueRefId, String attrValueRefName, String attrValueTypeName) {Log.d(TAG, attrName + " attrValueRefId = " +attrValueRefId + " attrValueRefName = " + attrValueRefName + " attrValueTypeName = " + attrValueTypeName);this.attrName = attrName;this.attrValueRefId = attrValueRefId;this.attrValueRefName = attrValueRefName;this.attrValueTypeName = attrValueTypeName;}public SkinAttrT setAttrValueFormat(Object[] format) {this.attrValueFormat = format;Log.d(TAG, attrName + "");return this;}@Overridepublic String toString() {return "SkinAttr \n[\nattrName=" + attrName + ", \n"+ "attrValueRefId=" + attrValueRefId + ", \n"+ "attrValueRefName=" + attrValueRefName + ", \n"+ "attrValueTypeName=" + attrValueTypeName+ "\n]";}
}
创建换肤关了类SkinManagerT
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.util.Log;
import android.view.View;import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class SkinManagerT {public static final String TAG = "SkinManagerT";public volatile static SkinManagerT instance;private Context mContext;private Resources currentResources;private Resources themeResources;private final ConcurrentHashMap<View, HashMap<String, SkinAttrT>> mSkinAttrMap = new ConcurrentHashMap<>();public void init(Context context) {mContext = context;currentResources = mContext.getResources();//获取换肤资源ResourcethemeResources = getThemeResources(mContext);}private SkinManagerT() {}public static SkinManagerT getInstance() {if (instance == null) {synchronized (SkinManagerT.class) {if (instance == null) {instance = new SkinManagerT();}}}return instance;}//将View保存到被监听的view列表中,使得在换肤时能够及时被更新public void saveSkinView(View view, HashMap<String, SkinAttrT> viewAttrs) {if (view == null || viewAttrs == null || viewAttrs.size() == 0) {return;}HashMap<String, SkinAttrT> originalSkinAttr = mSkinAttrMap.get(view);if (originalSkinAttr != null && originalSkinAttr.size() > 0) {originalSkinAttr.putAll(viewAttrs);mSkinAttrMap.put(view, originalSkinAttr);} else {mSkinAttrMap.put(view, viewAttrs);}}public Resources getDefaultResource() {return mContext.getResources();}public void restoreToDefaultSkin() {//换肤的时候切换resourcecurrentResources = getDefaultResource();notifySkinChanged();}public void restoreToThemeSkin() {//换肤的时候切换resourcecurrentResources = themeResources;notifySkinChanged();}/*** 遍历部署资源文件,将对应的资源部署到对应View上* 更换皮肤时,通知view更换资源*/private void notifySkinChanged() {View view;HashMap<String, SkinAttrT> viewAttrs;Iterator<Map.Entry<View, HashMap<String, SkinAttrT>>> iter = mSkinAttrMap.entrySet().iterator();while (iter.hasNext()) {Map.Entry<View, HashMap<String, SkinAttrT>> entry = iter.next();view = entry.getKey();viewAttrs = entry.getValue();if (view != null) {deployViewSkinAttrs(view, viewAttrs);}}Log.d(TAG, "notifySkinChanged skinSize " + mSkinAttrMap.size());}public void deployViewSkinAttrs(View view, HashMap<String, SkinAttrT> viewAttrs) {if (view == null || viewAttrs == null || viewAttrs.size() == 0) {return;}Iterator<Map.Entry<String, SkinAttrT>> iter = viewAttrs.entrySet().iterator();while (iter.hasNext()) {Map.Entry<String, SkinAttrT> entry = iter.next();SkinAttrT attr = entry.getValue();//判断当前保存的资源文件设置到哪个方法上的if (attr.attrName.equals(SkinConstantT.BACKGROUND)) {//判断保存的支援文件是哪个类型if (attr.attrValueTypeName.equals(SkinConstantT.COLOR)) {//判断当前是说你哪个资源主题模式if (currentResources == getDefaultResource()) {//如果是默认,直接部署上去view.setBackgroundColor(getDefaultResource().getColor(attr.attrValueRefId, null));} else {//如果是主题资源,通过对应方法部署上去view.setBackgroundColor(getThemeResourcesColorResId(attr, currentResources));}}}}}public int getThemeResourcesColorResId(SkinAttrT viewAttrs, Resources resources) {//通过资源名称,获取到资源IDint newResId = resources.getIdentifier(viewAttrs.attrValueRefName,// 资源名viewAttrs.attrValueTypeName, // 资源类型"com.kx.skin" // 应用包名);Log.d(TAG, "getThemeResourcesColor newResId " + newResId);//通过资源ID,获取到对应资源的值int newColorResId = resources.getColor(newResId, null);Log.d(TAG, "getThemeResourcesColor newColorResId " + newColorResId);return newColorResId;}public Resources getThemeResources(Context context) {try {//通过反射创建AssetManager
// AssetManager assetManager = AssetManager.class.newInstance();
// Method add = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);//通过反射加载路径
// int cookie = (int) add.invoke(assetManager, "/sdcard/skin/skin.skin");//将assets中的文件拷贝到自己私有的文件中File skinFile = copyAssetToFiles(context,"skin-debug.apk", // assets 下的相对路径"skin-debug.apk"); // 目标文件名boolean exists = skinFile.exists(); // true 表示存在Log.e(TAG, "加载文件 exists " + exists + " getAbsolutePath " + skinFile.getAbsolutePath());// 创建资源ResourcesAssetManager assetManager = new AssetManager();int cookie = assetManager.addAssetPath(skinFile.getAbsolutePath());if (cookie == 0) {Log.e(TAG, "加载失败,路径无效或权限不足");}Resources oldRes = context.getResources();Resources newRes = new Resources(assetManager,oldRes.getDisplayMetrics(),oldRes.getConfiguration());return newRes;} catch (Throwable e) {e.printStackTrace();Log.d(TAG, "Throwable " + e);}return null;}public File copyAssetToFiles(Context ctx, String assetPath, String fileName) throws IOException {File outFile = new File(ctx.getFilesDir(), fileName);try (InputStream in = ctx.getAssets().open(assetPath);OutputStream out = Files.newOutputStream(outFile.toPath())) {byte[] buf = new byte[8192];int len;while ((len = in.read(buf)) > 0) {out.write(buf, 0, len);}}return outFile;}
}
使用
setContentView(SkinLayoutInflaterT.from(this).inflate(R.layout.activity_theme, null));SkinManagerT.getInstance().restoreToThemeSkin();
这里只是简单的把代码逻辑走了一遍,后面需要更多适配工作,现在换肤逻辑走完了