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 |
EXACTLYchildSize |
EXACTLYchildSize |
EXACTLYchildSize |
match_parent |
EXACTLYparentSize |
AT_MOSTparentSize |
UNSPECIFIED0 |
wrap_content |
AT_MOSTparentSize |
AT_MOSTparentSize |
UNSPECIFIED0 |
上图可以简单记为:除了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也已经初始化好了。ViewTreeObserverViewTreeObserver中的众多回调可以完成这个功能。比如
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方法绘制自身以及遍历子元素对它们进行绘制。
View的工作流程图
