前言
上回咱们说到ViewRootImpl.performTraversals()
这个方法,从这里开始,会进入真正的View的绘制流程。第一次看的同学先去隔壁我奶奶都能懂的UI绘制流程(上)!汲取预备知识,剩下的同学系好安全带,发车啦!
Measures
MeasureSpec:
我们从performTraversals()
开始参观,发现一上来就有个叫childWidthMeasureSpec
的玩意儿
从名字上可以猜测,这是用来在测量时确定测量方式的。啥意思?在Android中,控件的大小有三种选择方式,match_parent,wrap_content以及具体的值。想一想,你叫系统wrap_content,它哪知道该怎么wrap_content,content有多大?父布局给它的空间又有多大?
所以这时候就需要MeasureSpec出场了。
在Measure流程中,系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,在onMeasure中根据这个MeasureSpec来确定view的测量宽高。
下面来具体看看这个类
MeasureSpec是一个32位的int型数值,高2位表示mode,低30位表示size。
可以很清晰的看到,MeasureSpec有以下三种类型
但是开头的MODE_MASK = 0x3 << MODE_SHIFT
又是什么鬼?
这就涉及到与或非操作了,这玩意儿不是给人看的,这句话没毛病,他们是给计算机看、以及程序员看的,程序员真的不是人。
从或("|")操作开始,在这里是用来将mode与size结合起来。
接着看与("&"),MODE_MASK
的作用就类似于网络中的掩码,是用来将内容过滤出来的,此处是用来获取mode。
最后看与非("~&"),跟上面的与操作类似也是过滤内容,这里是用来将size过滤出来
到这里还是懵逼的道友,建议你们去学习下计算机组成原理相关的知识,在这里推荐下《程序是怎样跑起来的》(日)矢泽久雄著
,感觉很棒。大家放心阅读,我没有淘宝链接。
GetRootMesureSpec
现在来看看int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width)
,我们需要注意第二个参数,找到他的起源:
从上面的代码可以看到,lp会在初始化时调用父类的构造函数,其默认值是LayoutParams.MATCH_PARENT
。现在回到getRootMeasureSpec(mWidth, lp.width)
中,查阅这个方法完整的代码
可以看到,子View的MeasureSpec是由父View的LayoutParams决定的,这里一共有三种类型,验证了之前对MeasureSpec的总结。
而此时我们传进来的参数为LayoutParams.MATCH_PARENT
,因此返回的childWidthMeasureSpec
就是MeasureSpec.EXACTLY
。
View.measure()
准备好了参数,下面就来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)
,该方法会调用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
,这个mView就是decorview,因此最终会调用view的measure()
方法。
这个方法比较长,我们从上到下慢慢看。
首先看optical
,这个就是光阴效果,在测量时,阴影也是要占空间的 。
接着看mMeasureCache
,这是用来处理测量缓存的。结合后面的代码可以看到,测量前会先尝试着从mMeasureCache
取出缓存,测量后又会将测量结果放置到缓存中。
最后onMeasure(widthMeasureSpec, heightMeasureSpec)
是重点。在整个measure()
方法中,我们并没有看到具体的测量代码,因为不同的View其测量方法也是不同的,需要由子类自己去决定。
这是一个典型的模板方法模式(设计模式之一,以后将Android架构的时候填坑),其中measure()
是模板,由于所有控件最终都是继承自View的,因此只需要在View中实现measure()
就可以了;而onMeasure()
则需要由子View自定义,因此子View会重写onMeasure()
方法。
View.onMeasure()
在阅读decorview的onMeasure()
之前,我们先来看看View的onMeasure()
方法
很简单,只是调用了setMeasuredDimension
可以看到这里也对光阴效果进行了处理,最后调用setMeasuredDimensionRaw()
在这里,我们对mMeasuredWidth
以及mMeasuredHeight
进行赋值。
有没有想起来什么?以前大家会在onCreate()方法中通过getMeasuredXXX()
来获取控件的宽高,结果失败了,为什么?以getMeasuredHeight()
为例
这里会返回mMeasuredHeight
,而mMeasuredHeight
是在onResume()
中通过ViewRootImpl进行一系列复杂的调用最终在View的setMeasuredDimensionRaw()
中被赋值,所以在onCreate()
中自然是获取不到的。
回到上面的方法中,在默认的情况下,这个measuredWidth
和measuredHeight
又是哪来的呢?我们来看看getSuggestedMinimumWidth()
做了什么
这个mMinWidth
得记住它,在自定义控件时是很关键的一个数值。一般都需要为其赋值,可以通过代码与XML两种方式。
Framelayout.onMeausre()
说了一大堆废话,现在我们回去看看DecorView的onMeasure()
方法。
遗憾的是,这里面也没做具体的测量行为,反而是调用了super.onMeasure(widthMeasureSpec, heightMeasureSpec)
,也就是FrameLayout的onMeasure()
。
循环前获取了子View的数量,接着开始对每一个子View进行测量以获取其测量宽高。主要就是通过measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
方法。
是不是又有点熟悉?对啦,开始测量时,performTraversals()
也是这样做的。
首先获取childWidthMeasureSpec
以及childHeightMeasureSpec
,然后通过child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
完成测量。很明显这是一个迭代的过程。
不同的是,performTraversals()
获取到的是根节点的MeasureSpec,而这里要获取的是子View的MeasureSpec,因此要考虑的父View与本身两个因素.
MeasureSpec九兄弟
我们来看看getChildMeasureSpec()
,一共有3*3=9种情况。文中我只介绍其中3种的代码
代码结构很清楚,首先判断父View
的MeasureSpec,如果是MeasureSpec.EXACTLY
,则开始判断子View
的childDimension
。
1.如果是具体的>0点值,就直接将这个值赋给子View,并将类型设置为MeasureSpec.EXACTLY;
2.如果是LayoutParams.MATCH_PARENT,则将值设置为父容器的大小,类型为MeasureSpec.EXACTLY; 3.如果是LayoutParams.WRAP_CONTENT,则将值设置为父容器的大小,类型为 MeasureSpec.AT_MOST;
剩下的6种情况也是类似的,代码就不展示了,直接上总结的图片
这张表不用刻意去记,先想一想,你会发现确实是这么一回事儿。
Measure总结
子View的测量在measureChildWithMargins()
中也终于搞定,说了这么多,UI绘制的第一步measure终于差不多了,我们来总结下吧。
1、View的测量
onMeasure
方法里面调用setMeasuredDimension()
确定当前View的大小
2.、ViewGroup的测量
2.1、遍历测量Child,可以通过下面三个方法来遍历测量:ChildmeasureChildWithMargins
、measureChild
、measureChildren
2.2、setMeasuredDimension
确定当前ViewGroup的大小
Layout
前面花了那么大的篇幅介绍Measure过程,现在回过头再来看Layout就会比较简单,因为他们的套路都是一样的。
从performLayout()
开始,直接调用layout()
方法,简洁明了
其中host就是我们的decorview,来看看最关键的layout()
方法
一开始,先要根据flag判断是否需要再次measure。
接着,将左上右下的位置依次赋值给oldL、oldT、oldR、oldB
继续,boolean changed
的作用和measure
中cache相似,是用来减少布局操作的。这儿是个三目运算符,根据有无光影调用不同的方法,我们以setFrame(l, t, r, b)
为例
怎么样,是不是相当的通俗易懂?就是十分常见的缓存策略。
最后,layout()
负责调用onLayout(changed, l, t, r, b)
,用志玲姐姐的话来说,“这是一个空箱子”
再去看看ViewGroup的onLayout(),更绝了,是个抽象方法。也就是说,每一个View的onLayout()
都需要自己去实现。想想也是这个道理,自己想成为什么样的人,不是自己说了算吗?
在这里,为了观影体验,我们以FrameLayout为例,看看他的onLayout()
做了什么
一上来就是为子View布局
layoutChildren()
本身也很简单明了,获取子控件数量,然后循环,依次获取宽高,判断各种情况,并调用子控件的布局方法。
没错就是这么简单,Layout也吹完了。
Draw
有了Measure和Layout的基础,Draw理解起来就更加简单了。
按照国际惯例,我们从ViewRootImpl.performDraw()
看起
ViewRootImpl.draw()
又会调用ViewRootImpl.drawSoftware()
,然后调用mView.draw(canvas)
。我们知道mView就是DecorView,而这个方法最终就会走进到View.draw()
方法中。
太贴心了,这些注释已经说明了一切。
挑重点的说,先看第一步,绘制背景。
这里有一个叫dirtyOpaque
的标志。在自定义ViewGroup时,一般是不会调用onDraw方法的,除非设置了background。仔细想想这也是理所当然的,我没有背景,有什么好画的。这也是产生过度绘制的原因之一。
稍微拓展一下,为什么说LinearLayout比RelativeLayout绘制快?其实他们在measure和layout上所花的时间是差不多的,区别就在于draw,RelativeLayout要从左右、上下两个方向绘制,而LinearLayout只需要绘制一次。
第三步绘制内容也是同理,一般ViewGroup本身都不会有内容,有的只是childView。
最后看下绘制子View,这是个空方法,留给后人继承。
我们一般不会和他打招呼,draw更多的是应用在自定义View中,也就是说只要重写onDraw()
方法即可。
到此为止,Draw也说完了,整个UI绘制结束!
实践是检验真理的唯一标准
说完原理,我们来看一个应用。
scrollview和listview不兼容
众所周知,在ScrollView中嵌套ListView时,ListView只会显示第一行,这是为什么呢?让我们一起走进ListView.onMeasure()
的内心世界
这是啥?在heightMode 为MeasureSpec.UNSPECIFIED
时,ListView的高度竟然就只测量第一个childView的高度!
再来看看ScrollView,他重写了measureChildWithMargins()
,有这么一句话
搜滴寺内!ScrollView会将子View的HeightMeasureSpec
设置为MeasureSpec.UNSPECIFIED
,于是ListView到了这里就懵逼了。
至于解决方法的话,重写ListView或者ScrollView都可以,是不是感觉思路很清晰?