View 的绘制原理

基本概念

ViewRoot

ViewRoot对应于ViewRootImpl类,它是连接WindowManagerDecorView的纽带。View的三大流程都是通过ViewRoot来完成的。

ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时也会创建ViewRootImpl对象,并将ViewRootImpl对象与DecorView建立关联。

View的三大绘制流程

View的绘制流程主要有measurelayoutdraw过程。

  1. measure:用来确定View的测量宽高。
  2. layout:用来确定View的最终宽高以及四个顶点的位置。
  3. draw:将View绘制在屏幕上。

View的绘制流程由ViewRootperformTraversals方法开始。

performTraversals方法会依次调用performMeasureperformLayoutperformDraw方法。这三个方法会分别完成顶层Viewmeasurelayoutdraw过程。

其中、performMeasure方法会调用其中的measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure的流程就从父容器传递到了子元素中,这样就完成了一轮measure过程。不断的对子元素进行measure过程。如此反复便完成了对View树的遍历。

其中,performMeasure方法位于ViewRootImpl中的measureHierarchy方法中(hierarchyn.层次结构)。

performLayout方法、performDraw方法的流程与之同理,需要注意一点的是,draw流程的传递是通过draw方法中的dispatchDraw实现的,不过并无本质的区别。

DecorView

DecorView作为顶层View,继承自FrameLayout。一般情况下它的内部都会包含一个LinearLayout。而LinearLayout中有上下两个部分,分别为标题栏和内容栏。我们平时设置指定布局文件的方法setContentView,就是指的是内容栏中的布局。

img

通过源码,我们可以得知,DecorView 其实是一个 LinearLayoutView 层的事件都先通过 DecorView,然后才传递给我们的 View

MeasureSpec

spec 英[spek]n.规格 vt.按特定标准设计并制造

MeasureSpec参与了Viewmeasure过程。在测量过程中,系统将ViewLayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的测量宽/高。

MeasureSpec代表一个32位的int值,高两位代表 测量模式SpecMode,低30位代表 该测量模式下的规格大小SpecSize

MeasureSpecSpecModeSpecSize之间可以通过位运算互相求出。

每个Viewmeasure过程之前都会先计算其MeasureSpec的值,然后再对其进行measure

注意View的宽和高各有一个MeasureSpec值,分别为widthMeasureSpecheightMeasureSpec

SpecMode的三种状态

  1. UNSPECIFIED

    父容器不对View做任何的限制,要多大给多大。这种情况一般用于系统的内部,表示一种测量方式的状态。

  2. EXACTLY

    父容器已经测出了View所需要的精确大小,此时的大小就是View的最终大小。

    它对应于LayoutParamsmatch_parent和具体的数值这两种模式。

  3. AT_MOST

    父容器指定了一个可用的大小,View的大小不能大于这个值。

    它对应于LayoutParams中的wrap_content

MeasureSpecLayoutParams的对应关系

对于DecorView和普通View来说,MeasureSpec的转化过程略有不同。

对于DecorView

DecorViewMeasureSpec由窗口的大小和DecorView自身的LayoutParams共同决定的。

通过在ViewRootImpl中的measureHierarchy方法中,调用getRootMeasureSpec方法,获得DecorViewMeasureSpec

其中getRootMeasureSpec方法中,根据自身LayoutParams的不同大小,共有三种情况:

  1. LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
  2. LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  3. 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通View

普通ViewMeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定的。

通过在父容器的measureChildWithMargins中的getChildMeasureSpec方法获得ViewMeasureSpec

其中getChildMeasureSpec方法的参数中使用到了父容器的MeasureSpec。在这个方法中通过switchif语句,对ViewMeasureSpec进行了决定。结果如下:

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 < UNSPECIFIEDchildMeasureSpec = max(childLayoutParams , parentMeasureSpec)。(实际上并无级别之分

通过上面的表格,可以更加直观的看出:普通ViewMeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定的。


View的工作流程

measure过程

measure过程分为两种情况:

  1. Viewmeasure过程:只需要测量自身即可。
  2. ViewGroupmeasure过程:则除了完成自己的测量外,还需要遍历去调用所有子元素的measure过程。

以下对这两种情况分别讨论。

Viewmeasure过程

Viewmeasure方法是final类型的方法,不可以重写,measure方法里面调用了onMeasure方法。

onMeasure方法里面也只调用了一个setMeasuredDimension方法来设置View的宽/高的测量值。

1
2
3
4
5
6
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMininumWidth(),
widthMeasureSpec),
getDefaultSize(getSuggestedMininumHeight(),
heightMeasureSpec));
}

而在setMeasureDimension方法的参数中,则使用了getDefaultSize的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = Measurespec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specsize;
break;
}
return result;
}

对于AT_MOSTEXACTLY的情况

通过代码可以看出,最终返回的值就是传入的MeasureSpec的中View测量后的大小(View最终的大小是在layout阶段确定的,但是几乎所有情况下,View的测量大小和最终大小都是相同的)。

对于UNSPECIFIED的情况

getDefaultSize方法返回的值是getsuggestedMininumWidth方法和getsuggestedMininumHeight方法决定的。

这里只讨论getsuggestedMininumWidth方法的逻辑,另一个方法同理:

  1. 如果View没有设置背景,那么此方法的返回值就是android:minWidth这个属性所指定的值,这个值默认为0。
  2. 如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。

对于自定义View的情况

对于直接继承View的自定义View来说,需要重写onMeasure方法并设置在wrap_content情况时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent

因为当自定义View处于wrap_content时,它的SpecModeAT_MOST模式,由前文可知,当父容器无论为AT_MOST还是ECACTLY模式时,自定义View都为AT_MOST模式且尺寸大小为parentSize,也就是父容器的剩余空间,即自定义View的大小变得与父容器的剩余空间大小一致,显然不是我们需要的。

解决方法:

解决方法也是非常简单的。我们只需要在onMeasure方法中对

  1. width方向为AT_MOST模式
  2. height方向为AT_MOST模式
  3. width方向以及height方向都为AT_MOST模式
  4. 其他的情况

这四个状态单独判断,分别使用setMeasuredDimension方法直接为自定义View设置我们需要的值即可。

源码中对于TextViewImageView等控件的wrap_content也做了特殊的处理。

ViewGroupmeasure过程

对于ViewGroup来说,处理要完成它自己的measure过程,还需要遍历去调用所有子元素的measure方法,对子元素也进行measure过程。

由于ViewGroup是一个抽象类,不能重写ViewonMeasure方法,但是他提供了一个measureChildren的方法:

1
2
3
4
5
6
7
8
9
10
protected void measureChildren(int widthMeasureSepc, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; i++) {
final View child = children[i];
if (...) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

从代码中,可以清晰的看出,measureChildren方法遍历了所有的子元素,并对他们使用了measureChild方法。

measureChild方法中,通过

  1. 参数child获得了child.LayoutParams
  2. measureChild方法中的getChildMeasureSpec方法配合参数中父容器的两个MeasureSpec值,得到子元素的两个MeasureSpec值。

接着将子元素的两个MeasureSpec值传递给child.measure方法来进行测量,到此便结束了一轮的measure过程。

注意:不同的ViewGroup有着不同的布局特性,不易写出通用的供ViewGroup使用的onMeasure方法。故设置成抽象类,需要测量过程中的各个子类(如LinearLayoutRelativeLayout等)自己去具体实现onMeasure方法。

Activity启动时获得一个View的宽/高信息的方法

由于Activity的启动和View的测量过程并不是同步进行的,因此无法保证在Activity启动时某个View已经测量完毕了。所以我们在Activity启动的时候,如果直接通过getMeasuredWidth/getMeasuredHeight方法获得的值可能为0。

以下有四种方法可以解决这个问题(详细代码见《Android开发艺术探索》190页)。

  1. Activity/View#onWindowFocusChanged

    ActivityonResumeonPause方法启动时,onWindowFocusChanged方法也会同时被调用,可以重写里面的内容,使之计算View的宽高。

    注意onWindowFocusChanged方法会在View已经初始化完毕后才开始调用。

  2. view.post(runnable)

    通过post可以将一个runnable投递都消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。

  3. ViewTreeObserver

    ViewTreeObserver中的众多回调可以完成这个功能。

    比如OnGlobalLayoutListener接口,当View树的状态发生改变或者View树内部的View的可见性发生改变的时候,onGlobalLayout方法将被回调,此时就可以获取View的宽高了。

  4. view.measure(int widthMeasureSpec, int heightMeasureSpec)

    可以主动调用该方法开始计算view的宽高。

    1. match_parent

      由于正常的measure过程中,我们需要父容器的MeasureSpec的值,而此时父容器并没开始计算,我们无从得知这两个值的大小。故我们无法对match_parentview计算其宽高。

    2. dp/px

      因为有精确的数值了,所以我们可以直接使用这个精确的数值通过MeasureSpec.makeMeasureSpec方法构建viewMeasureSpec

    3. wrap_content

      直接将MeasureSpec.makeMeasureSpec方法中参数的值设置为(1<<30)-1即可,这是View理论上可以支持的最大值,所以这样构建MeasureSpec是合理的。

layout过程

layout的作用是ViewGroup用来确定子元素的位置。当调用一个Viewlayout方法时,它会在layout方法中通过setFrame方法确定自身的位置,然后调用onLayout方法确定子元素的位置。

onLayout方法中,会遍历所有子元素,对它们计算各自的位置后,调用子元素的layout方法,完成一轮layout过程。

View中实现了layout方法,但是由于onLayout的实现与不同View各自的布局有关,所以源码中仅给出了一个空onLayout方法。需要每个ViewViewGroup自己去重写。

注意:单一View一般不需要重写onLayout方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>

* 为视图及其所有子体指定大小和位置
*
* 这是布局机制的第二阶段。
* (第一个是测量)。在此阶段中,每个父级调用其所有子级上的layout来定位它们。
* 这通常使用存储在方法pass()中的子测量值来完成。
*
* 派生类不应重写此方法。
* 具有子级的派生类应重写onLayout。在该方法中,他们应该对每个子对象调用布局。
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

draw过程

draw过程就比较简单,它的作用是将View绘制到屏幕上面。

View的绘制过程遵循以下的4步:

  1. 绘制背景:background.draw(canvas)
  2. 绘制自己:onDraw方法
  3. 绘制children:dispatchDraw方法
  4. 绘制装饰:onDrawScrollBars方法

所有的绘制过程都在draw方法中进行。

其中,View绘制过程的传递是通过dispatchDraw方法实现的,dispatchDraw方法会遍历所有的子元素并调用他们的draw方法,完成一轮的draw过程。

注意

  1. 单一View需要重写onDraw方法绘制自身。
  2. ViewGroup需要重写onDraw方法绘制自身以及遍历子元素对它们进行绘制。

View的工作流程图


View 的绘制原理
https://luoyuy.top/posts/bd6906438953/
作者
LuoYu-Ying
发布于
2022年4月18日
许可协议