View 的绘制原理
基本概念
ViewRoot
ViewRoot
对应于ViewRootImpl
类,它是连接WindowManager
和DecorView
的纽带。View
的三大流程都是通过ViewRoot
来完成的。
在ActivityThread
中,当Activity
对象被创建完毕后,会将DecorView
添加到Window
中,同时也会创建ViewRootImpl
对象,并将ViewRootImpl
对象与DecorView
建立关联。
View
的三大绘制流程
View
的绘制流程主要有measure
、layout
和draw
过程。
measure
:用来确定View
的测量宽高。layout
:用来确定View
的最终宽高以及四个顶点的位置。draw
:将View
绘制在屏幕上。
View
的绘制流程由ViewRoot
的performTraversals
方法开始。
performTraversals
方法会依次调用performMeasure
、performLayout
、performDraw
方法。这三个方法会分别完成顶层View
的measure
、layout
、draw
过程。
其中、performMeasure
方法会调用其中的measure
方法,在measure
方法中又会调用onMeasure
方法,在onMeasure
方法中会对所有的子元素进行measure
过程,这个时候measure
的流程就从父容器传递到了子元素中,这样就完成了一轮measure
过程。不断的对子元素进行measure
过程。如此反复便完成了对View
树的遍历。
其中,performMeasure
方法位于ViewRootImpl
中的measureHierarchy
方法中(hierarchy
:n.
层次结构)。
performLayout
方法、performDraw
方法的流程与之同理,需要注意一点的是,draw
流程的传递是通过draw
方法中的dispatchDraw
实现的,不过并无本质的区别。
DecorView
DecorView
作为顶层View
,继承自FrameLayout
。一般情况下它的内部都会包含一个LinearLayout
。而LinearLayout
中有上下两个部分,分别为标题栏和内容栏。我们平时设置指定布局文件的方法setContentView
,就是指的是内容栏中的布局。
通过源码,我们可以得知,DecorView
其实是一个 LinearLayout
,View
层的事件都先通过 DecorView
,然后才传递给我们的 View
。
MeasureSpec
spec
英[spek]
n.
规格vt.
按特定标准设计并制造
MeasureSpec
参与了View
的measure
过程。在测量过程中,系统将View
的LayoutParams
根据父容器所施加的规则转换成对应的MeasureSpec
,然后再根据这个MeasureSpec
来测量出View
的测量宽/高。
MeasureSpec
代表一个32位的int
值,高两位代表 测量模式SpecMode
,低30位代表 该测量模式下的规格大小SpecSize
。
MeasureSpec
与SpecMode
、SpecSize
之间可以通过位运算互相求出。
每个View
的measure
过程之前都会先计算其MeasureSpec
的值,然后再对其进行measure
。
注意:View
的宽和高各有一个MeasureSpec
值,分别为widthMeasureSpec
和heightMeasureSpec
。
SpecMode
的三种状态
UNSPECIFIED
父容器不对
View
做任何的限制,要多大给多大。这种情况一般用于系统的内部,表示一种测量方式的状态。EXACTLY
父容器已经测出了
View
所需要的精确大小,此时的大小就是View
的最终大小。它对应于
LayoutParams
的match_parent
和具体的数值这两种模式。AT_MOST
父容器指定了一个可用的大小,
View
的大小不能大于这个值。它对应于
LayoutParams
中的wrap_content
。
MeasureSpec
和LayoutParams
的对应关系
对于DecorView
和普通View
来说,MeasureSpec
的转化过程略有不同。
对于DecorView
:
DecorView
的MeasureSpec
由窗口的大小和DecorView
自身的LayoutParams
共同决定的。
通过在ViewRootImpl
中的measureHierarchy
方法中,调用getRootMeasureSpec
方法,获得DecorView
的MeasureSpec
。
其中getRootMeasureSpec
方法中,根据自身LayoutParams
的不同大小,共有三种情况:
LayoutParams.MATCH_PARENT
:精确模式,大小就是窗口的大小。LayoutParams.WRAP_CONTENT
:最大模式,大小不定,但是不能超过窗口的大小。- 固定大小(比如100dp):精确模式,大小为
LayoutParams
中指定的大小。
对于普通View
:
普通View
的MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
共同决定的。
通过在父容器的measureChildWithMargins
中的getChildMeasureSpec
方法获得View
的MeasureSpec
。
其中getChildMeasureSpec
方法的参数中使用到了父容器的MeasureSpec
。在这个方法中通过switch
和if
语句,对View
的MeasureSpec
进行了决定。结果如下:
childLayoutParams \ parentMeasureSpec |
EXACTLY |
AT_MOST |
UNSPECIFIED |
---|---|---|---|
dp /px |
EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
match_parent |
EXACTLY parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
wrap_content |
AT_MOST parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
上图可以简单记为:除了dp
/px
的精确模式情况外,三种MeasureSpec
的级别为:EXACTLY
< AT_MOST
< UNSPECIFIED
且childMeasureSpec
= max(childLayoutParams
, parentMeasureSpec
)。(实际上并无级别之分)
通过上面的表格,可以更加直观的看出:普通View
的MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
共同决定的。
View
的工作流程
measure
过程
measure
过程分为两种情况:
View
的measure
过程:只需要测量自身即可。ViewGroup
的measure
过程:则除了完成自己的测量外,还需要遍历去调用所有子元素的measure
过程。
以下对这两种情况分别讨论。
View
的measure
过程
View
的measure
方法是final
类型的方法,不可以重写,measure
方法里面调用了onMeasure
方法。
在onMeasure
方法里面也只调用了一个setMeasuredDimension
方法来设置View
的宽/高的测量值。
1 |
|
而在setMeasureDimension
方法的参数中,则使用了getDefaultSize
的方法。
1 |
|
对于AT_MOST
和EXACTLY
的情况
通过代码可以看出,最终返回的值就是传入的MeasureSpec
的中View
测量后的大小(View
的最终的大小是在layout
阶段确定的,但是几乎所有情况下,View
的测量大小和最终大小都是相同的)。
对于UNSPECIFIED
的情况
getDefaultSize
方法返回的值是getsuggestedMininumWidth
方法和getsuggestedMininumHeight
方法决定的。
这里只讨论getsuggestedMininumWidth
方法的逻辑,另一个方法同理:
- 如果
View
没有设置背景,那么此方法的返回值就是android:minWidth
这个属性所指定的值,这个值默认为0。 - 如果
View
设置了背景,则返回android:minWidth
和背景的最小宽度这两者中的最大值。
对于自定义View
的情况
对于直接继承View
的自定义View
来说,需要重写onMeasure
方法并设置在wrap_content
情况时的自身大小,否则在布局中使用wrap_content
就相当于使用match_parent
。
因为当自定义View
处于wrap_content
时,它的SpecMode
为AT_MOST
模式,由前文可知,当父容器无论为AT_MOST
还是ECACTLY
模式时,自定义View
都为AT_MOST
模式且尺寸大小为parentSize
,也就是父容器的剩余空间,即自定义View
的大小变得与父容器的剩余空间大小一致,显然不是我们需要的。
解决方法:
解决方法也是非常简单的。我们只需要在onMeasure
方法中对
- 仅
width
方向为AT_MOST
模式 - 仅
height
方向为AT_MOST
模式 width
方向以及height
方向都为AT_MOST
模式- 其他的情况
这四个状态单独判断,分别使用setMeasuredDimension
方法直接为自定义View
设置我们需要的值即可。
源码中对于TextView
、ImageView
等控件的wrap_content
也做了特殊的处理。
ViewGroup
的measure
过程
对于ViewGroup
来说,处理要完成它自己的measure
过程,还需要遍历去调用所有子元素的measure
方法,对子元素也进行measure
过程。
由于ViewGroup
是一个抽象类,不能重写View
的onMeasure
方法,但是他提供了一个measureChildren
的方法:
1 |
|
从代码中,可以清晰的看出,measureChildren
方法遍历了所有的子元素,并对他们使用了measureChild
方法。
在measureChild
方法中,通过
- 参数
child
获得了child.LayoutParams
; measureChild
方法中的getChildMeasureSpec
方法配合参数中父容器的两个MeasureSpec
值,得到子元素的两个MeasureSpec
值。
接着将子元素的两个MeasureSpec
值传递给child.measure
方法来进行测量,到此便结束了一轮的measure
过程。
注意:不同的ViewGroup
有着不同的布局特性,不易写出通用的供ViewGroup
使用的onMeasure
方法。故设置成抽象类,需要测量过程中的各个子类(如LinearLayout
、RelativeLayout
等)自己去具体实现onMeasure
方法。
在Activity
启动时获得一个View
的宽/高信息的方法
由于Activity
的启动和View
的测量过程并不是同步进行的,因此无法保证在Activity
启动时某个View
已经测量完毕了。所以我们在Activity
启动的时候,如果直接通过getMeasuredWidth
/getMeasuredHeight
方法获得的值可能为0。
以下有四种方法可以解决这个问题(详细代码见《Android开发艺术探索》190页)。
Activity/View#onWindowFocusChanged
在
Activity
的onResume
和onPause
方法启动时,onWindowFocusChanged
方法也会同时被调用,可以重写里面的内容,使之计算View
的宽高。注意:
onWindowFocusChanged
方法会在View
已经初始化完毕后才开始调用。view.post(runnable)
通过
post
可以将一个runnable
投递都消息队列的尾部,然后等待Looper
调用此runnable
的时候,View
也已经初始化好了。ViewTreeObserver
ViewTreeObserver
中的众多回调可以完成这个功能。比如
OnGlobalLayoutListener
接口,当View
树的状态发生改变或者View
树内部的View
的可见性发生改变的时候,onGlobalLayout
方法将被回调,此时就可以获取View
的宽高了。view.measure(int widthMeasureSpec, int heightMeasureSpec)
可以主动调用该方法开始计算
view
的宽高。match_parent
由于正常的
measure
过程中,我们需要父容器的MeasureSpec
的值,而此时父容器并没开始计算,我们无从得知这两个值的大小。故我们无法对match_parent
的view
计算其宽高。dp
/px
因为有精确的数值了,所以我们可以直接使用这个精确的数值通过
MeasureSpec.makeMeasureSpec
方法构建view
的MeasureSpec
。wrap_content
直接将
MeasureSpec.makeMeasureSpec
方法中参数的值设置为(1<<30)-1
即可,这是View
理论上可以支持的最大值,所以这样构建MeasureSpec
是合理的。
layout
过程
layout
的作用是ViewGroup
用来确定子元素的位置。当调用一个View
的layout
方法时,它会在layout
方法中通过setFrame
方法确定自身的位置,然后调用onLayout
方法确定子元素的位置。
在onLayout
方法中,会遍历所有子元素,对它们计算各自的位置后,调用子元素的layout
方法,完成一轮layout
过程。
View
中实现了layout
方法,但是由于onLayout
的实现与不同View
各自的布局有关,所以源码中仅给出了一个空onLayout
方法。需要每个View
和ViewGroup
自己去重写。
注意:单一View
一般不需要重写onLayout
方法。
1 |
|
draw
过程
draw
过程就比较简单,它的作用是将View
绘制到屏幕上面。
View
的绘制过程遵循以下的4步:
- 绘制背景:
background.draw(canvas)
- 绘制自己:
onDraw
方法 - 绘制children:
dispatchDraw
方法 - 绘制装饰:
onDrawScrollBars
方法
所有的绘制过程都在draw
方法中进行。
其中,View
绘制过程的传递是通过dispatchDraw
方法实现的,dispatchDraw
方法会遍历所有的子元素并调用他们的draw
方法,完成一轮的draw
过程。
注意:
- 单一
View
需要重写onDraw
方法绘制自身。 ViewGroup
需要重写onDraw
方法绘制自身以及遍历子元素对它们进行绘制。