写点什么

Android 无缝换肤深入了解与使用,android 快速开发

用户头像
Android架构
关注
发布于: 刚刚

}@Overridepublic View onCreateView(String s, Context context, AttributeSet attributeSet) {return null;}


@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {


boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);//是否是可换肤的 viewAppCompatDelegate delegate = mAppCompatActivity.getDelegate();View view = delegate.createView(parent, name, context, attrs);//处理系统逻辑 if (view instanceof TextView && SkinConfig.isCanChangeFont()) {TextViewRepository.add(mAppCompatActivity, (TextView) view);}


if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {if (view == null) {view = ViewProducer.createViewFromTag(context, name, attrs);}if (view == null) {return null;}parseSkinAttr(context, attrs, view);}return view;}}


当内部的初始化操作完成后,如果判断没有创建好 view,则需要我们自己去创建 view


  • 看上一步是通过ViewProducer.createViewFromTag(context, name, attrs)来创建

  • 那么直接来看一下这个类ViewProducer,原理功能请看代码注释

  • 在 AppCompatViewInflater 中你可以看到相同的代码

  • 代码位置:ViewProducer.java


class ViewProducer {//该处定义的是 view 构造方法的参数,也就是 View 两个参数的构造方法:public View(Context context, AttributeSet attrs)private static final Object[] mConstructorArgs = new Object[2];//存放反射得到的构造器 private static final Map<String, Constructor<? extends View>> sConstructorMap= new ArrayMap<>();//这是 View 两个参数的构造器所对应的两个参数 private static final Class<?>[] sConstructorSignature = new Class[]{Context.class, AttributeSet.class};//如果是系统的 View 或 ViewGroup 在 xml 中并不是全路径的,通过反射来实例化是需要全路径的,这里列出来它们可能出现的位置 private static final String[] sClassPrefixList = {"android.widget.","android.view.","android.webkit."};


static View createViewFromTag(Context context, String name, AttributeSet attrs) {if (name.equals("view")) {//如果是 view 标签,则获取里面的 class 属性(该 View 的全名)name = attrs.getAttributeValue(null, "class");}


try {//需要传入构造器的两个参数的值 mConstructorArgs[0] = context;mConstructorArgs[1] = attrs;


if (-1 == name.indexOf('.')) {//如果不包含小点,则是内部 Viewfor (int i = 0; i < sClassPrefixList.length; i++) {//由于不知道 View 具体在哪个路径,所以通过循环所有路径,直到能实例化或结束 final View view = createView(context, name, sClassPrefixList[i]);if (view != null) {return view;}}return null;} else {//否则就是自定义 Viewreturn createView(context, name, null);}} catch (Exception e) {//如果抛出异常,则返回 null,让 LayoutInflater 自己去实例化 return null;} finally {// 清空当前数据,避免和下次数据混在一起 mConstructorArgs[0] = null;mConstructorArgs[1] = null;}}


private static View createView(Context context, String name, String prefix)throws ClassNotFoundException, InflateException {//先从缓存中获取当前类的构造器 Constructor<? extends View> constructor = sConstructorMap.get(name);try {if (constructor == null) {// 如果缓存中没有创建过,则尝试去创建这个构造器。通过类加载器加载这个类,如果是系统内部 View 由于不是全路径的,则前面加上 Class<? extends View> clazz = context.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);//获取构造器 constructor = clazz.getConstructor(sConstructorSignature);//将构造器放入缓存 sConstructorMap.put(name, constructor);}//设置为无障碍(设置后即使是私有方法和成员变量都可访问和修改,除了 final 修饰的)constructor.setAccessible(true);//实例化 return constructor.newInstance(mConstructorArgs);} catch (Exception e) {// We do not want to catch these, lets return null and let the actual LayoutInflater// tryreturn null;}}}


  • 当然还有另外的方式来创建,就是直接用 LayoutInflater 内部的那一套

  • view = ViewProducer.createViewFromTag(context, name, attrs);删除,换成下方代码:

  • 代码位置:SkinInflaterFactory.java


LayoutInflater inflater = mAppCompatActivity.getLayoutInflater();if (-1 == name.indexOf('.'))//如果为系统内部的 View 则,通过循环这几个地方来实例化 View,道理跟上面 ViewProducer 里面一样{for (String prefix : sClassPrefixList){try{view = inflater.createView(name, prefix, attrs);} catch (ClassNotFoundException e){e.printStackTrace();}if (view != null) break;}} else{try{view = inflater.createView(name, null, attrs);} catch (ClassNotFoundException e){e.printStackTrace();}}


  • sClassPrefixList的定义


private static final String[] sClassPrefixList = {"android.widget.","android.view.","android.webkit."};


最后是最终的拦截获取需要换肤的 View 的部分,也就是上面SkinInflaterFactory类的onCreateView最后调用的parseSkinAttr方法


  • 定义类一个成员来保存所有需要换肤的 View, SkinItem 里面的逻辑就是定义了设置换肤的方法。如:View 的 setBackgroundColor 或 setColor 等设置换肤就是靠它。


private Map<View, SkinItem> mSkinItemMap = new HashMap<>();


  • SkinAttr: 需要换肤处理的 xml 属性,如何定义请参照官方文档:https://github.com/burgessjp/ThemeSkinning


private void parseSkinAttr(Context context, AttributeSet attrs, View view) {//保存需要换肤处理的 xml 属性 List<SkinAttr> viewAttrs = new ArrayList<>();//变量该 view 的所有属性 for (int i = 0; i < attrs.getAttributeCount(); i++) {String attrName = attrs.getAttributeName(i);//获取属性名 String attrValue = attrs.getAttributeValue(i);//获取属性值//如果属性是 style,例如 xml 中设置:style="@style/test_style"if ("style".equals(attrName)) {//可换肤的属性 int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};//经常在自定义 View 时,构造方法中获取属性值的时候使用到。//这里通过传入 skinAttrs,TypeArray 中将会包含这两个属性和值,如果 style 里没有那就没有 - -TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);//获取属性对应资源的 id,第一个参数这里对应下标的就是上面 skinAttrs 数组里定义的下标,第二个参数是没有获取到的默认值 int textColorId = a.getResourceId(0, -1);int backgroundId = a.getResourceId(1, -1);if (textColorId != -1) {//如果有颜色属性//<style name="test_style">//<item name="android:textColor">@color/colorAccent</item>//<item name="android:background">@color/colorPrimary</item>//</style>//以上边的参照来看//entryName 就是 colorAccentString entryName = context.getResources().


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


getResourceEntryName(textColorId);//typeName 就是 colorString typeName = context.getResources().getResourceTypeName(textColorId);//创建一换肤属性实力类来保存这些信息 SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);if (skinAttr != null) {viewAttrs.add(skinAttr);}}if (backgroundId != -1) {//如果有背景属性 String entryName = context.getResources().getResourceEntryName(backgroundId);String typeName = context.getResources().getResourceTypeName(backgroundId);SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);if (skinAttr != null) {viewAttrs.add(skinAttr);}


}a.recycle();continue;}//判断是否是支持的属性,并且值是引用的,如:@color/redif (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {try {//去掉属性值前面的“@”则为 idint id = Integer.parseInt(attrValue.substring(1));if (id == 0) {continue;}//资源名字,如:text_color_selectorString entryName = context.getResources().getResourceEntryName(id);//资源类型,如:color、drawableString typeName = context.getResources().getResourceTypeName(id);SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);if (mSkinAttr != null) {viewAttrs.add(mSkinAttr);}} catch (NumberFormatException e) {SkinL.e(TAG, e.toString());}}}//是否有需要换肤的属性?if (!SkinListUtils.isEmpty(viewAttrs)) {SkinItem skinItem = new SkinItem();skinItem.view = view;skinItem.attrs = viewAttrs;mSkinItemMap.put(skinItem.view, skinItem);//是否换肤 if (SkinManager.getInstance().isExternalSkin() ||SkinManager.getInstance().isNightMode()) {//如果当前皮肤来自于外部或者是处于夜间模式 skinItem.apply();//应用于这个 view}}}

采用方案的注意事项和疑问

  1. 可能系统会更改相关方法,但好处大于弊端

  2. 插件化也是外置 apk 来加载,如何做到呢?


  • 占时不去研究


  1. 皮肤从网络上下载到哪个目录?如何断定皮肤已经下载?


  • 可以通过SkinFileUtils工具类调用getSkinDir方法获取皮肤的缓存目录

  • 下载的时候可以直接下载到这个目录

  • 有没有某个皮肤就判断该文件夹下有没有这个文件了


  1. 如何不打包之前可以直接预览?


  • 想要能在打包前提前预览效果,而不每次想看一看效果就要打一个 apk 包

  • 首先,大家都应该知道分渠道的概念。通过分渠道打包,因为我们能把资源也分成不同渠道的,运行不同渠道,所得到的资源是不一样的。

  • 然后,我们在:项目目录\app\src,创建一个和渠道相同名字的目录。比如说有个red渠道。




  • 最后,我们选编译的渠道为 red,然后直接运行就可以看到效果了。如果可以直接把 res 拷贝到皮肤项目打包就行了。



  1. 换肤对应的属性需要是 View 提供了 set 方法的的属性!


  • 如果没有提供则不能在 java 代码中设置值

  • 如果是自定义 View 那么就添加对应方法

  • 如果是系统或类库 View,额(⊙o⊙)…


  1. 换肤的属性值需要是 @开头的数据引用,如:@color/red


  • 原因是因为固定的值一般不可能是需要换肤的属性,在SkinInfaterFactory的方法parseSkinAttr中有这样一句来进行过滤没有带 @的属性值:



  • 但此时,正好有一个自定义 View 没有按照常路出牌,它的值就是图片名字没有类型没有引用,通过 java 代码context.getResources().getIdentifier(name, "mipmap", context.getPackageName())来获取图片资源(参考这奇葩方式的库)。但由于这个属性是需要换肤更换的属性,于是没办法,专门为这两个属性在SkinInfaterFactoryparseSkinAttr方法中写了个判断



参考这代码

其他参考

  1. Android主题换肤 无缝切换 (主要参考对象,用的也是他修改Android-Skin-Loader后的框架ThemeSkinning

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android 无缝换肤深入了解与使用,android快速开发