本文作者
作者: 灯不利多
最近重新看了一下任大佬的《Android 开发艺术探索》,写了篇笔记,分享给大家。
1
ViewRootImpl 与 DecorView
接下来的讲解的源码版本为 Android 10 。
ViewRootImpl 是连接 WindowManager 和 DecorView 的纽带,测量、放置和绘制三大流程都是通过 ViewRootImpl 实现的。
在 ActivityThread 的 handleResumeActivity 方法中,会调用 WindowManager 的 addView 方法,而具体添加 DecorView 的操作是在 WindowManagerGlobal 中。
在 WindowManagerGlobal 的 addView 方法中,会把 DecorView 添加到 Window 中,同时会创建 ViewRootImpl ,并调用 ViewRootImpl 的 setView 方法 把 ViewRootImpl 和 DecorView 关联起来。
View 的绘制流程是从 ViewRootImpl 的 performTraversals 方法开始的,它经过测量(measure)、放置(layout)和绘制(draw)三个过程才能把一个 View 绘制出来,measure 方法用于测量 View 的宽高,layout 用于确定 View 在父容器中的放置位置,draw 负责做具体的绘制操作。
针对 performTraversals 的大致流程,可用下图表示。
View 绘制主要的三个方法就是 onMeasure、 onLayout、onDraw,这三个方法要解决的问题就是画多大、在哪画、画什么。
ViewRootImpl 的 performTraversal 方法会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成 DecorView 的测量、放置和绘制三大流程。
performMeasure 方法会调用 DecorView 的 measure 方法,在 measure 方法中又会调用自己的 onMeasure 方法。
DecorView 的 onMeasure 方法会调用父类 FrameLayout 的 onMeasure 方法,在 FrameLayout 的 onMeasure 方法中,会调用子元素的 onMeasure 方法测量子元素的宽高,接着子元素会重复父容器的 measure 过程,如此反复完成整个 View 树的遍历。
而 performLayout 和 performDraw 的执行流程与 performMeasure 是类似的。
measure 过程决定了 View 的宽高,layout 过程决定了 View 的四个顶点的坐标和实际的 View 宽高,draw 过程则决定了 View 的具体绘制操作,只有 draw 方法完成后 View 的内容才会在屏幕上展示。
1.1 Activity 视图层级结构
假如我们有一个继承了 AppCompatActivity 的 MainActivity,并且 activity_main 布局的内容如下。
我们现在能感知到的视图层级是下面这样的。
当我们在 MainActivity 中调用父类的 setContentView 后,AppCompatActivity 会调用 AppCompatDelegateImpl 的 setContentView 方法,AppCompatDelegateImpl 在这个方法中会把 RelativeLayout 添加到 id 为 content 的 ViewGroup 中。
其中 ContentFrameLayout 也就是 id 为 content 的 ViewGroup 。
ensureSubDecor 方法会在 subDecor 没有初始化时用 createSubDecor 方法创建 subDecor ,createSubDecor 方法会调用 Window 的 setContetnView 方法,把 abc_screen_toolbar 布局设为 Window 的内容视图,而这里的 mHasActionBar 只有在 feature 为 FEATURE_SUPPORT_ACTION_BAR 时才会为 true。
abc_screen_toolbar 布局的内容如下。
把 RelativeLayout 放到 mSubDecor 中后,视图层级就变成下面这样了。
Window 的实现类为 PhoneWindow,在 PhoneWindow 的 setContentView 方法中,会调用 installDecor 方法创建 DecorView ,然后调用 LayoutInflate 的 inflate 方法把 ActionBarOverlayLayout 加入到 DecorView 中。
在 installDecor 方法中,会调用 generateLayout 方法生成 mContentParent。
在 generateLayout 方法中,会根据不同的 feature 来生成不同的 DecorView,比如没有设定任何 feature 时,对应的 DecorView 的布局就是 screen_simple 。
screen_simple 布局的实现如下。
image
前面的布局加入到 screen_simple 中后,视图层级就是下面这样的。
这里的 action_mode_bar_stub 是用来显示 ActionMode 的,而 FrameLayout 就是 ID_ANDROID_CONTENT 对应的 ViewGroup。
到这里好像还是少了点什么,状态栏哪去了?
根据 Layout Inspector 的分析,LinearLayout 下面还有一个 id 为 statusBarBackground 的 View ,根据这个 id 在 DecorView 中找到了对应的 mStatusColorViewState 。
而在 DecorView 的 updateColorViewInt 方法中,则把状态栏通过 addView 方法添加到了 DecorView 中。
该方法的调用时序图如下。
也就是完整的 DecorView 视图层次如下。
上图对应的 View 树如下。
2
测量规格 MeasureSpec
按注释来说,MeasureSpec 封装了从父 View 传给子 View 的布局要求,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,具体的尺寸会受到父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程。
在测量过程中,系统会把 View 的 LayoutParams 根据父容器设定的规则转换为对应的 MeasureSpec,然后再根据这个 MeasureSpec 测量出 View 的宽高。
要注意的是,这里的说的宽高是测量宽高,不一定是 View 的最终宽高,原因后面会讲到。
MeasureSpec 代表一个 32 位 int 值,高 2 位代表测量模式 SpecMode,低 30 位代表规格大小 SpecSize,MeasureSpec 通过把 SpecMode 和 SpecSize 打包成一个 int 值避免过多的对象内存分配,
MeasureSpec 中定义了下面三种测量规格。
无限制 UNSPECIFIED 表示父 View 对子 View 的大小不做限制;
精确 EXATCTLY 父 View 计算好了子 View 具体的宽高,子 View 的最终大小就是 SpecSize 指定的值;
最多 AT_MOST 父 View 指定了一个可用大小,View 的大小不能大于这个值;
MeasureSpec 用来打包 SpecMode 和 SpecSize 的方法是 makeMeasureSpec ,代码如下。
3
MeasureSpec 与 LayoutParams 的关系
在 View 测量时,系统会把 LayoutParams 在父 View 的约束下,转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 确定 View 测量后的宽高,要靠 LayoutParams 和父 View 一起才能决定子 View 的 测量模式。
DecorView 的测量规格由窗口的尺寸和其 LayoutParams 共同确定,而普通 View 的测量规格由父 View 的 MeasureSpec 和自身的 LayoutParams 决定,MeasureSpec 确定后,就可以在 onMeasure 方法中确定 View 的测量宽高。
在 ViewRootImpl 的 performTraversals 方法中,有一段调用 measureHierarchy 方法的代码,也就是传给 measureHierarchy 的大小为屏幕尺寸。
measureHierarchy 方法是用来设定子 View ,也就是 DecorView 的大小的。
measureHierarchy 中的的 childWidthMeasureSpec 和 childHeightMeasureSpec 就是 DecorView 的测量规格 MeasureSpec。
通过上面代码可以看出,DecorView 会根据 LayoutParams 中的宽高来设定宽高测量规格。
MATCH_PARENT 精确模式,DecorView 大小就是窗口大小;
WRAP_CONTENT 最大模式,大小不定,但是不能超过窗口大小;
固定大小 精确模式,大小为 LayoutParams 中指定的大小;
对于普通 View 来说,View 的 measure 过程由 ViewGroup 传递而来,而 ViewGroup 是在 measureChildWithMargins 方法中确定子 View 的测量规格的。
下面是 ViewGroup 的 getChildMeasureSpec 方法获取子 View 的测量规格的方式。
其中一段代码如下。
上面这段代码中的 size 是去掉了 padding 后的 size。
这里要注意的是,不是所有 ViewGroup 都会用这样的方式决定子 View 的测量规格,比如 RelativeLayout 用的就是不一样的测量规格。
4
View 测量过程
对于 ViewGroup,除了要完成自己的测量,还要遍历调用子元素的 measure 方法,而 View 只需要通过 measure 方法就能确定测量规格。
View 的测量过程由 View 的 measure 方法完成,measure 方法是一个 final 类型的方法,子类不能重写。
View 的 measure 方法会调用 onMeasure 方法,这个方法我们是可以重写的,onMeasure 的实现如下。
widthMeasureSpec 和 heightMeasureSpec 是从父 View 传过来的宽高测量规格,getDefaultSize 方法是用来获取默认宽高的,getDefaultSize 的实现如下。
从 getDefaultSize 方法中可以看出,当测量模式为 UNSPECIFIED 时,宽/高就是最小宽/高,当测量模式为 AT_MOST 或 EXACTLY 时,宽/高就是 ViewGroup 指定的 SpecSize。
View 的宽/高由 specSize 决定,直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则咋布局中使用 wrap_content 相当于使用 match_parent 。
从前面的代码可以了解到,如果 View 在布局中使用 wrap_content,那么它的 specMode 是 AT_MOST 模式,这时它的宽/高为 specSize ,这时 View 的 specSize 为 ViewGroup 的 specSize。
比如 activity_main 的布局是下面这样的。
那么 MyView 测量后的大小就是 600 ,这个 600 是 dp 换算为 px 后的值。
ViewGroup 的 SpecSize 是自身剩余的空间大小,也就是默认子 View 的宽/高为父 View 的剩余控件大小,相当于为宽/高设定的 wrap_content 无效,变成了 match_parent 。
如果我们不想让自定义 View 在宽/高设为 wrap_content 时与父 View 的大小一致,那我们可以像下面这样设定自己的计算好的默认宽/高。
下面来看下 ViewGroup 的测量过程。
ViewGroup 是一个抽象类,没有定义测量的的具体过程,具体的测量过程需要子类实现,下面以 LinearLayout 为例,看一下它的 onMeasure 方法的实现。
LinearLayout 会根据我们设定的方向设定子 View 的测量规格,下面来看下 measureVertical 的实现。
在 measureVertical 方法中,把每一个子元素都传给了 measureChildBeforeLayout ,而 measureChildBeforeLayout 只是调用了 ViewGroup 的 measureChildWithMargin 方法。
5
View 放置过程
layout 方法的作用是 ViewGroup 用于确定子元素的位置,当 ViewGroup 的位置确定后,会在 onLayout 方法中遍历所有子 View 并调用子 View 的 layout 方法。
layout 方法用于确定 View 自己的位置,而 onLayout 方法则用于确定所有子元素的位置,View 的 layout 方法的实现如下。
View 的 layout 方法首先会通过 setFrame 方法设定 View 的边框,也就是 mLeft、mRight、mTop 和 mBottom 四个顶点的值,这时 View 在父 View 中的位置就确定了。
设定了四个顶点后,layout 方法就会调用 onLayout 方法确定子 View 的位置,View 和 ViewGroup 都没有实现 onLayout 方法,下面以 LinearLayout 为例,看下 LinearLayout 的 onLayout 方法的实现。
LinearLayout 的 onLayout 方法会根据不同的排列方向调用不同的放置方法,当方向为 VERTICAL 时,对应的放置方法为 layoutVertical ,下面来看下 layoutVertical 方法的实现。
layoutVertical 方法会遍历所有子 View 并调用 setChildFrame 方法指定子 View 的边框(frame)在哪个位置,而 setChildFrame 方法只是简单调用了 子 View 的 layout 方法。
childTop 的值会逐渐增加,下一个子 View 的 top 为上一个子 View 的 bottom,也就是排列方向为 VERTICAL 的 LinearLayout 的特性。
6
View 绘制过程
View 绘制分为下面 6 步:
绘制背景
保存 Canvas 图层为后续淡出做准备(可选)
绘制 View 的内容
绘制子 View (dispatchDraw)
绘制淡出边缘并恢复 Canvas 图层(可选)
绘制装饰(比如 foreground 和 scrollbar)
一般情况下第 2 步和第 5 步是不执行的。
下面来看下绘制相关方法的实现。
drawBackground 方法首先会通过 Drawable 的 setBounds 方法设置背景绘制的范围,然后如果我们调用过 scrollTo 方法,那么 drawBackground 就会把画布平移到指定位置后再绘制。
View 和 ViewGroup 没有实现 onDraw 方法,接下来就是 dispatchDraw 方法,View 没有实现这个方法,下面来看下 ViewGroup 的 dispatchDraw 方法的实现。
在 ViewGroup 的 dispatchDraw 方法中,首先会调用 buildOrderedChildList 方法获取子 View 列表,然后遍历子 View ,通过 drawChild 方法调用每一个子 View 的 draw 方法。
而第 6 步 drawForegounrd 只是获取 foreground 对应的 Drawable 并调用它的 draw 方法。
7
总结
根据前面讲解的内容,从 ViewRootImpl 的 performTraversals 方法开始,大致的方法调用时序图如下。
View 绘制的三大过程分别是测量、放置和绘制,对应的的三个方法为 onMeasure 、onLayout 和 onDraw 。
测量过程中最重要的就是理解 MeasureSpec 以及自定义 View 时要重写 onMeasure 方法设置默认宽高。
MeasureSpec 由测量模式 SpecMode 和 SpecSize 组成,SpecMode 分为待定(UNSPECIFIED)、精确(EXACTLY)和最大(AT_MOST)。
放置过程中最关键的方法就是 setFrame ,这个方法会把父 View 在 onLayout 方法中计算好的四个顶点的值赋值给 mTop、mLeft 、mRight 和 mBottom 。
绘制过程的 draw 方法中主要的 4 个绘制步骤为:绘制背景、绘制 View 内容、绘制子 View 内容以及绘制装饰。
参考资料
《Android 开发艺术探索》