当前位置: 首页>移动开发>正文

Android 换肤怎么修改页面布局 安卓换肤

Android换肤原理分析

由于最近项目需要用到换肤功能,所以学习了下相关的知识,下面简单的说下我的总结
换肤思路:
(1)、找到需要换肤的View及其需要改变的属性
(2)、拿到需要换肤包(一般是APK,里面只有res文件下的东西即可)
(3)、通过本包的资源名称去拿到皮肤包的具体资源(所以需要资源名称之间有个关系,本文中相等
(4)、把皮肤包的资源设置到需要换肤的View上,实现换肤功能。

1、那么我们怎么去找到需要换肤的View呢?

首先我们要先了解android View是怎么从xml解析成View的,这里分析的是Activity。

以setContentView方法作为入口:可以看出调用了getWindow().setContentView(layoutResID),而这个getWindow()就是PhoneWindow

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_Android 换肤怎么修改页面布局,第1张

在这里主要是调用了LayoutInflater的inflate方法

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_ide_02,第2张

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_Android 换肤怎么修改页面布局_03,第3张

这里代码太多了,删减了一部无关代码,可以看出通过createViewFromTag(root, name, inflaterContext, attrs)我们就得到想要的View实例

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_android_04,第4张

尝试去创建View

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_android_05,第5张

关键点来了,我们发现这里有个mFactory2的东西,他其实就是一个接口,继承自Factory,只有一个未实现的onCreateView方法,可以实现了这个接口,然后干预的View创建流程

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_java_06,第6张

那么我们怎么去实现,自定义这个Factory2方法呢?原来Layoutmanager开放了这个使用,那就好办了

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_java_07,第7张

那么我们实现一下这个方法看看:

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_java_08,第8张

打印出来的就是当前Activity的所有的View的名称,而第四个参数attrs就是我们在Xml里面设置的属性

到此又产生了疑问,我怎么确定哪个View要换肤呢?新建属性值

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_bc_09,第9张

在需要换肤的View加上该属性

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_ide_10,第10张

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_bc_11,第11张

现在通过这里就可以打印出,为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一起放进去封装起来。

Android 换肤怎么修改页面布局 安卓换肤,Android 换肤怎么修改页面布局 安卓换肤_android_12,第12张

主要是这里,我们把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));
            }
        }
    }
}

以上就是我总结的换肤功能实现,第一次写博客,错误的地方还请大家指正。



https://www.xamrdz.com/mobile/48f1925750.html

相关文章: