
虽然都说“面试造火箭,上班拧螺丝”,但是多看看源码和实现原理总归是好的。View 作为 Android 的基石之一,其绘制流程是非常重要的。小弟不才,在这里记录一下我对 View 绘制流程 的理解,未来等我理顺了可能还会写一篇事件分发机制的理解。本文会从基础的知识储备讲到各个绘制环节,文章包含的内容比较多,让我们开始吧!
基础知识储备
我们先来看几张图,首先是 Activity 视图架构图
Activity
Activity 不负责控制视图,它只负责控制生命周期并处理消息事件。控制视图的工作是由 Window 来完成的。一个 Activity 包含一个 Window ,Window 才是真正代表一个窗口,Activity 可以看作是这个窗口的控制器,负责视图的添加和现实,并通过其他回调方法,与 Window 及 View 进行交互
Window
Window 是视图的容器,其中持有一个 DecorView,DecorView 是 View 的根布局。Window 是一个抽象类,实际上 Activity 持有的是 PhoneWindow
这个子类。PhoneWindow
中有一个内部类 DecorView
,通过创建 DecorView
来加载 Activity 中编写的 XML 布局。Window 通过 WindowManager 将 DecorView
加载到其中,并将 DecorView
交给 ViewRoot,进行视图绘制以及其他交互。
DecorView
DecorView
是 FrameLayout
的子类,它是 Android 视图树的根节点视图。其作为顶级 View,一般情况下内部包含一个垂直布局的 LinearLayout,这里简单来说有两个部分:一是标题栏;二是内容栏, 通过 setContentView 设置到其中,一般通过代码获取的 content
就是这里的 id 为 content 的 FrameLayout
(这里 LinearLayout 中具体有些什么还是取决于系统版本)。
ViewRoot
ViewRoot 的作用非常重要,这里的 View 绘制 和后面会提到的 事件分发机制 都是经由它来执行或者传递的。
ViewRoot 对应 ViewRootImpl
类,它是连接 WindowManagerService
和 DecorView
的纽带,View 绘制的三大流程,即测量(measure),布局(layout),绘制(draw)都是由 ViewRoot 来完成。
ViewRoot 不属于视图树的一部分。从源码上看,其既不是 View
的子类,也不是 View
的父类,但是它实现了 ViewParent
接口,所以还是可以把它看作是 View 的父视图。RootView 中存在一个内部类变量继承了 Handler
类,因此可以接收并分发事件,Android 中所有的触摸、按键、界面刷新事件都是由它进行分发的。
DecorView 的源码我们暂且不细说了(我不会说是我还没看明白)。
这里最后提一下 ViewRoot 对点击事件的分发,整个过程大致是:
硬件 → ViewRootImpl → DecorView → PhoneWindow → Activity
再往下就是 Android 的事件分发机制了,我们会在下篇文章中再来细说这部分。
小结
Activity 是控制器,负责控制 View 的生命周期并处理事件;Window 是 VIew 的容器;DecorView 是顶级View;ViewRoot 是连接器,连接用户与软件,是实现交互的桥梁。
绘制的基础框架
View 绘制的顺序是 DecorView
→ ViewGroup
* N → View
,按照这个顺序依次往下,进行 measure(测量) → layout(布局) → draw(绘制)
Measure 流程
测量每个组件的大小
在 measure()
方法中对租价尺寸进行一下逻辑处理后,再调用 onMeasure()
方法通过 setMeasuredDimension()
设定 View 的尺寸信息,完成 View 的测量工作。
1 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { |
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
看起来测量流程非常简单,但是 widthMeasureSpec
和 heightMeasureSpec
这两个重要的参数是怎么获得的?
MeasureSpec
首先 MeasureSpec 是一种测量规格,它是一个32位的 int 值。
它由占2个字节的测量模式 mode 作为 32 和 31 位,加上后30位的 size 表示具体的测量大小。
测量模式分为三类:
- UNSPECIFIED :不对 View 进行任何限制,要多大给多大,一般用于系统内部
- EXACTLY:对应 LayoutParams 中的 match_parent 和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
- AT_MOST :对应 LayoutParams 中的 wrap_content。View 的大小不能大于父容器的大小。
对于 DecorView 是通过屏幕大小和自身的布局参数 LayoutParams 确定其大小。
对于其他 View 包括 ViewGroup 则是通过父布局的 MeasureSpec 和自身 LayoutParams 确定,这部分比较复杂,简单来说就是设定为 wrap_content 时 View 的大小不能超过父布局,因此需要重写 onMeasure()
方法处理这个情况。
更具体的解析可以看看大佬的文章
测量流程图(我真的不想画图了,那天有空了再复刻一个自己版本,就借用大佬的图吧):
这里要注意的是:ViewGroup 的测量是依赖于 View 的测量的,因为 ViewGroup 中是没有 onMeasure()
方法的,它的 onMeasure()
是一个抽象方法,需要每个子 View 根据自己的布局特性去实现。
Layout 流程
测量完 View 后,就需要将其 布局 到 Window 上,Layout 主要是通过确定上下左右四个点来确定位置。
Layout 也是从父容器到子组件的顺序,不同的是 ViewGroup 先在 layout()
中确定自己的布局,然后在 onLayout()
方法中再调用子 View 的 layout()
方法,让子 View Layout 出来。在 Measure 过程中,就如上面一节提到的,ViewGroup 一般是先测量子 View 的大小,然后再确定自身的大小。
1 | public void layout(int l, int t, int r, int b) { |
其中 onLayout()
是一个空方法,只有当前 View 有子 View (即 ViewGroup)时才要实现这个方法,特别是自定义 View 时需要通过重写这个方法实现特定的效果。
布局流程图:
Draw 流程
绘制的过程如下:
- 绘制背景
drawBackground(canvas)
- 绘制自己
onDraw(canvas)
- 绘制 Children
dispatchDraw(canvas)
- 绘制装饰
onDrawForeground(canvas)
1 | public void draw(Canvas canvas) { |
所有的视图最终都会调用 View 的这个 draw(canvas)
方法绘制,在自定义 View 时,应该复写 onDraw(canvas)
方法实现绘制,如果说要复写这个方法应该先调用 super.draw(canvas)
完成系统的绘制,然后再进行自定义的 draw 方法来绘制。
更具体的源码分析可以看看这篇文章
绘制流程图:
自定义 View
我们在自定义 View 的时候需要根据自定义不同的 View 来分别实现不同的方法:onMeasure()
, onLayout()
, onDraw()
。
onMeasure()
- 对于View,一般重写此方法,针对 wrap_content 情况,规定 View 默认的大小值,避免和 match_parent 情况一致。
- 对于 ViewGroup,若不重写,就会执行和单一 View 中相同逻辑,不会测量子 View。一般会重写
onMeasure()
方法,循环测量子 View。
onLayout()
- 对于 View,不需要实现该方法。
- 对于 ViewGroup 必须实现,该方法是个抽象方法,实现该方法,来对子 View 进行布局。
onDraw()
无论 View 或是 ViewGroup 都需要实现该方法进行绘制。
参考链接:
- Post title:View 的绘制流程
- Post author:Lynch Lee
- Create time:2021-05-11 16:40:38
- Post link:https://neeomaclynch.github.io/2021/05/11/View-的绘制流程/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.