查看原文
其他

一步步搞定Android换肤框架 从Debug 7.1.1源码开始

徐爱卿 鸿洋 2019-04-05

本文作者


作者:徐爱卿

链接:http://www.jianshu.com/p/c8748fbf6f60

本文由作者授权推送。


这个SkinAPPDemo是很早的时候就写好的,今天才来总结,实在惭愧。



其实,Android换肤这个功能呢从v7包中谷歌就跟我们做了一个很好的示范。同时呢,谷歌也给我们提供了一个针对View去做自定义操作的接口。说了这么多,不如来点实际的。


本篇博客的的demo中的build.gradle配置是:


compileSdkVersion 25

buildToolsVersion "25.0.0"

然后进行debug源码时,也是基于Android7.1.1的源码进行的。后面一大波debug来袭,请留神。


这篇文章我们会从最简单的XML文件开始聊起。


个人觉得知识点有以下:


  • 不断的debug到Android源码的内部能够加深我们对知识的了解

  • 不断的debug,可以知道那些报错的error信息生成的原因,以便于我们更好的解决问题

  • 学习Google的编码方式

  • 加深对LayoutInflater的理解

  • 看源码实现换肤


思考什么是换肤?


说白了,就是改变控件的背景以及颜色或者其本身的颜色。比如:更换TextView的字体颜色、背景颜色等等;在比如:更换LinearLayout的的背景以及颜色,等等。这些都属于换肤的范围。


思考如何换肤?


两个切入点:


  • 方法一:view初始完成后,通过findViewById等方法拿到当前的控件,然后根据主题样式,设置其颜色、背景等。

  • 方法二:在view的构建初期,直接更改其颜色、背景等。


孰好孰坏,不言而喻。我呢,就要使用方法二。这个方法听起来不错,如何实现呢?其实,Google已经告诉我们了。天下文章一大抄,看你会抄不会抄。我们直接分析Google的实现逻辑,然后再写我们需要的逻辑。


有人问了,Android源码在哪里实现了?哥们别急,开讲了。


1引入


我们写一个简单的页面,里面就一个TextView,如下:



然后我们打印java Log.d(TAG, "tv instanceof AppCompatTextView ? -> "+(tv instanceof AppCompatTextView) +"");这句话,如下:(注意红色框中的,就可以了)


--tv instanceof AppCompatTextView--


看到结果不知道大家有没有些许疑问?


在Android 7.1.1上运行的TextView竟然是AppCompatTextView的实例。我明明在XML中写的是TextView,在这里怎么就是AppCompatTextView的实例了呢?很明显,是在解析XML之后构建View对象的初期,看到是TextView标签直接使用AppCompatTextView构建这个对象。


轮廓流程如下:




我们关键看最后一步,看下如何“创建AppCompatTextView”,当我们知道了如何偶从一个XML文件变身为一个View对象后,我们就可以比葫芦画瓢,创建我们的自己的属性的View了。



2Debug开始


分析LayoutInflater


从这里开始,我们全部使用debug结果一部部分析并且来验证了,免得空口说大话了。


我这里,创建一个SelectThemeActivity,继承自AppCompatActivity 我们先从最简单的 setContentView(R.layout.activity_select_theme);开始。


起初,执行的是startActivity(new Intent(this, SelectThemeActivity.class));],然后这个东东再向后调用ActivityManagerProxy#startActivity, ActivityManagerProxy是ActivityManager的一个远程代理,不用管它。然后通过ActivityThread的内部Handler类执行performLaunchActivity,最后调用Instrumentation#callActivityOnCreate(Activity activity, Bundle icicle)


这一块的流程如下:


--从startActivity到init Factory--


一定要记得下面这张图,这是一个关键点。


--系统的Factory--


这里注意一点layoutInflater.getFactory(),返回的是LayoutInflater的一个内部接口Factory


layoutInflater.getFactory()高能注意


在这里默认没有代码干预的情况下,我们不设置Factory的情况下,layoutInflater.getFactory()等于null,系统会自己创建一个Factory去处理XML到View的转换。但是!!!他这个。


反之,如果我们设置了自己的Factory,那么系统就会走我们Factory的onCreateView,他会返回一个我们定制化的View。下面详细讲解。


Factory定义如下:



Factory


Factory是一个很强大的接口。当我们使用inflating一个XML布局时,可以使用这个类进行拦截解析到的XML中的标签属性-AttributeSet和上下文-Context,以及标签名称-name(例如:TextView)。


然后我们根据这些属性可以创建对应的View,设置一些对应的属性。

比如:我读取到XML中的TextView标签,这时,我就创建一个AppCompatTextView对象,它的构造方法中就是我读取到的XML属性。然后,将构造好的View返回即可。


默认情况下,从context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)得到LayoutInflater,然后通过layoutInflater.getFactory()刚开始是null,然后执行LayoutInflaterCompat.setFactory(layoutInflater, this);方法。


看下这个方法。



大致意识是:将一个自定义的Factory接口绑定到创建View的LayoutInflatr。这个接口的实现不能为空,同时只能设置一次(在代码中会有mFactorySet的boolean值(默认是false)标记是否已经设置过,如果重复设置,会抛异常)


在这里我们关注传入的LayoutInflaterFactory的实例,最终这个设置的LayoutInflaterFactory传入到哪里了呢?,我们向下debug,进入LayoutInflater中的下面:


--LayoutInflater#setFactory2--


给mFactory = mFactory2 = factory执行了,进行mFactory和mFactory2的赋值。

到这里走的路程,初始化好了LayoutInflater和LayoutInflaterFactory。


这里,我们就走完了SelectThemeActivity#onCreate中的super.onCreate(savedInstanceState);下面开始走setContentView(R.layout.activity_select_theme);


setContentView(int resId)


setContentView会走到LayoutInflate的下面这里:



再向下就到了LayoutInflater重点:



进入到createViewFromTag方法之中,会进入到LayoutInflate的View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)中。


到这里开始,我们开始学习源码中是如何使用Factory的。会走到下面这里:


--LayoutInflate#createViewFromTag--


这里的name传入的就是就是解析到的标签值LinearLayout。



很遗憾, callActivityOnCreateView返回的总是null:



然后进入到下面的,createView(parent, name, context, attrs);中。高潮来了》》》》》,我期盼已久的看看Google源码是如何创建View的。


从XML到View的华丽转身


--根据标签+属性创建对象--



看到木有,它是拿标签名称进行switch的比较,是哪一个就进入到哪一个中进行创建View。


有人说啦,这里没有LinearLayout对应的switch啊。的确。最终返回null。



AppCompatViewInflater#createView并没有对布局进行创建对象

这里回到最初,由于Line769返回null,同时name值LinearLayout不包含".",进入到Line785onCreateView(parent, name, attrs)。



到这里,我们知道这个标签是LinearLayout了,那么开始创建这个对象了。问题来了,我们知道这个对象名称了,但是它属于哪个包名?如何创建呢?


根据标签名称创建对象


我们知道Android控件中的包名总共就那么几个:android.widget.]android.webkit.]android.app.],既然就这么几种,那么我干脆挨个用这些字符串进行如下拼接:


android.widget.LinearLayout]android.webkit.LinearLayout]android.app.LinearLayout],然后挨个创建对象,一旦创建成功即说明这个标签所在的包名是对的,返回这个对象即可。那么,从上面debug会进入到如下源码:



进入二次解析布局标签

sClassPrefixList的定义如下:


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


注意:是final的


创建Android布局标签对象


继续向下debug,进入到真正的创建Android布局标签对象的实现。在这个方法中,才是“android.widget.”包下的,LinearLayout、RelativeLayout等等的具体实现。




name="LinearLayout"

prefix="android.widget."


下面分析下这段代码(下面的方法中去掉了一些无用代码):



在这个方法中关键的步骤就是如何去实例化布局标签对象。这也是我们下面换肤的前提知识。


总结下根据标签+属性创建View的思路:


两个关键点:

  • 是否设置了Factory

  • Factory的onCreateView是否返回null


再让我们回到最初的地方:




2换肤开始


换肤代码思路:



我们通过我们view的属性的值white,拿到skin-apk中的white属性的skinResId,然后根据skinRes.getColor(skinResId)返回color,然后设置到我们的TextView上面。


step1 实现LayoutInflaterFactory接口,创建自己的Factory



看下运行结果,创建LinearLayout



创建TextView



OK!没问题,每一个View的实现我们都可以拦截到,下一步开始拿取View的background、或者TextColor进行相应的更改。


step2 获取view需要换肤的属性



保存view的相关属性



View换肤时的操作



onCreateView创建View后,读取view的属性值,并且保存



执行换肤时调用:



step3 实现加载插件apk,并且拿到插件的资源对象



step 4 获取插件中的resId的值



step 5 SkinView中换肤




3代码跑起来


activity_main.xml



color.xml



MainActivity



下面是skin-apk的color.xml



APP-color



skin-color





换肤结果,Activity的background和TextView的textColor都换了。


4代码优化


优化这一块,有很多地方可以优化。比如:


在saveViewAttrs(View view, Context context, AttributeSet attrs)方法中,将换肤进行抽象化处理。




ViewAttrsFactory




更多优化,在我的GitHubSkinAppDemo,大家拉下来看下。这里就不在赘述了。

https://github.com/xujianhui404/SKinAppDemo




总结


多多debug,多多益善!上面的我贴的debug的流程,还希望大家多多debug。反正我不记得我debug这个流程多少遍了。


谢谢大家最后坚持看到这里,期望大家多多fork,多多start。谢谢大家。


我的博客地址是 : 

http://xuvip.top

本文项目地址:

https://github.com/xujianhui404/SKinAppDemo



推荐下100offer这个平台,合作过无数次,除了找工作以外,也可以扫码关注下行业动态。


优秀人才不缺工作机会,只缺适合自己的好机会。但是他们往往没有精力从海量机会中找到最适合的那个。

100offer 会对平台上的人才和企业进行严格筛选,让「最好的人才」和「最好的公司」相遇。

扫描下方二维码,注册 100offer,谈谈你对下一份工作的期待。一周内,收到 5-10 个满足你要求的好机会!


如果你有想学习的文章直接留言,我会整理征稿。如果你有好的文章想和大家分享欢迎投稿,直接向我投递文章链接即可。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存