android 换肤框架详解3-自动换肤原理梳理

发布于:2025-08-14 ⋅ 阅读:(18) ⋅ 点赞:(0)

前面已经介绍了换肤的基本逻辑和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());
    }

    @Override
    public 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并且保存对应信息
     */
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        Log.d(TAG, "SkinInflaterFactory onCreateView(), create view name=" + name + "  ");
        //自己解析创建View
        View 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_launcher
                String attrValueName = context.getResources().getResourceEntryName(resId);
                //资源类型类型drawable,color
                String 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);
    }

    @Override
    public 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的第一个参数context
        Object[] constructorArgs = (Object[]) getField(inflater, "mConstructorArgs");
        if (null == constructorArgs || constructorArgs.length < 2) {
            //异常,一般不会发生
            constructorArgs = new Object[2];
            setField(inflater, "mConstructorArgs", constructorArgs);
        }
        //如果mConstructorArgs的第一个参数为空,则设置为mContext
        if (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;
    }

    @Override
    public 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();
        //获取换肤资源Resource
        themeResources = 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() {
        //换肤的时候切换resource
        currentResources = getDefaultResource();
        notifySkinChanged();
    }

    public void restoreToThemeSkin() {
        //换肤的时候切换resource
        currentResources = 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) {

        //通过资源名称,获取到资源ID
        int 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());
            // 创建资源Resources
            AssetManager 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();

这里只是简单的把代码逻辑走了一遍,后面需要更多适配工作,现在换肤逻辑走完了


网站公告

今日签到

点亮在社区的每一天
去签到