IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android换肤原理分析 -> 正文阅读

[移动开发]Android换肤原理分析

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));
            }
        }
    }
}

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

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-12 19:42:39  更:2021-11-12 19:43:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 3:27:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码