android View的方法,再次回顾 Android View 核心知识与原理

原标题:再次回顾 Android View 核心知识与原理

本文作者

作者: 灯不利多

https://www.jianshu.com/p/08d0e7d966ed

最近重新看了一下任大佬的《Android 开发艺术探索》,写了篇笔记,分享给大家。

1

ViewRootImpl 与 DecorView

ba9ee4a47fe905b462962285608a0bd8.png

接下来的讲解的源码版本为 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 的大致流程,可用下图表示。

5b0a6daf71adb82c9e0c5cb9fbbcb1ed.png

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 布局的内容如下。

f0121f0d9e349a597b3bf6df9ea473c1.png

我们现在能感知到的视图层级是下面这样的。

9373e0735d21a9080357beba7ff6b3f0.png

当我们在 MainActivity 中调用父类的 setContentView 后,AppCompatActivity 会调用 AppCompatDelegateImpl 的 setContentView 方法,AppCompatDelegateImpl 在这个方法中会把 RelativeLayout 添加到 id 为 content 的 ViewGroup 中。

65912d29a5ca1eedc4550596c8cf1b60.png

其中 ContentFrameLayout 也就是 id 为 content 的 ViewGroup 。

50e58e32bbf12cff7a0cabc6900d7d02.png

ensureSubDecor 方法会在 subDecor 没有初始化时用 createSubDecor 方法创建 subDecor ,createSubDecor 方法会调用 Window 的 setContetnView 方法,把 abc_screen_toolbar 布局设为 Window 的内容视图,而这里的 mHasActionBar 只有在 feature 为 FEATURE_SUPPORT_ACTION_BAR 时才会为 true。

abc_screen_toolbar 布局的内容如下。

76da10fdb12368e7b1f90d2d9a6cac0f.png

把 RelativeLayout 放到 mSubDecor 中后,视图层级就变成下面这样了。

72cf8b16dd80091e66570c76fd55341b.png

Window 的实现类为 PhoneWindow,在 PhoneWindow 的 setContentView 方法中,会调用 installDecor 方法创建 DecorView ,然后调用 LayoutInflate 的 inflate 方法把 ActionBarOverlayLayout 加入到 DecorView 中。

192f31397a60fa249355a64147a29680.png

在 installDecor 方法中,会调用 generateLayout 方法生成 mContentParent。

在 generateLayout 方法中,会根据不同的 feature 来生成不同的 DecorView,比如没有设定任何 feature 时,对应的 DecorView 的布局就是 screen_simple 。

screen_simple 布局的实现如下。

7ca507e15efb020140734423c236b7dc.png

image

前面的布局加入到 screen_simple 中后,视图层级就是下面这样的。

b58ebe2c44c7847013139da3a5c278f2.png

这里的 action_mode_bar_stub 是用来显示 ActionMode 的,而 FrameLayout 就是 ID_ANDROID_CONTENT 对应的 ViewGroup。

到这里好像还是少了点什么,状态栏哪去了?

69d2c8d05706ee970f10ef8b800b2d30.png

根据 Layout Inspector 的分析,LinearLayout 下面还有一个 id 为 statusBarBackground 的 View ,根据这个 id 在 DecorView 中找到了对应的 mStatusColorViewState 。

420af99f4f49b58a3e73c6367b3bdcc8.png

而在 DecorView 的 updateColorViewInt 方法中,则把状态栏通过 addView 方法添加到了 DecorView 中。

该方法的调用时序图如下。

06bda49524df4ab3e604d4c841b8e329.png

也就是完整的 DecorView 视图层次如下。

18d74961fc1d46ac7cecf5d17c90b2d8.png

上图对应的 View 树如下。

f77259e9294ef97f1099415389999377.png

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 中定义了下面三种测量规格。

68def188575c274f2bdf0e9ff2a11233.png

无限制 UNSPECIFIED 表示父 View 对子 View 的大小不做限制;

精确 EXATCTLY 父 View 计算好了子 View 具体的宽高,子 View 的最终大小就是 SpecSize 指定的值;

最多 AT_MOST 父 View 指定了一个可用大小,View 的大小不能大于这个值;

MeasureSpec 用来打包 SpecMode 和 SpecSize 的方法是 makeMeasureSpec ,代码如下。

172dc42a6931ed5d22fdc1fe35816947.png

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 的大小为屏幕尺寸。

65fa60566ae8e7210dd15c9900f07fef.png

measureHierarchy 方法是用来设定子 View ,也就是 DecorView 的大小的。

720382784640795153011911fdebcc50.png

measureHierarchy 中的的 childWidthMeasureSpec 和 childHeightMeasureSpec 就是 DecorView 的测量规格 MeasureSpec。

3c9c2c508774618f66a579bc21d363c8.png

通过上面代码可以看出,DecorView 会根据 LayoutParams 中的宽高来设定宽高测量规格。

MATCH_PARENT 精确模式,DecorView 大小就是窗口大小;

WRAP_CONTENT 最大模式,大小不定,但是不能超过窗口大小;

固定大小 精确模式,大小为 LayoutParams 中指定的大小;

对于普通 View 来说,View 的 measure 过程由 ViewGroup 传递而来,而 ViewGroup 是在 measureChildWithMargins 方法中确定子 View 的测量规格的。

924d60aa10b349456a2ed8a2a93b93cd.png

下面是 ViewGroup 的 getChildMeasureSpec 方法获取子 View 的测量规格的方式。

1a91bd9870414d8970d64278d53a93f1.png

其中一段代码如下。

f997a869395ba276f52226478d5f3a6f.png

上面这段代码中的 size 是去掉了 padding 后的 size。

这里要注意的是,不是所有 ViewGroup 都会用这样的方式决定子 View 的测量规格,比如 RelativeLayout 用的就是不一样的测量规格。

4

View 测量过程

对于 ViewGroup,除了要完成自己的测量,还要遍历调用子元素的 measure 方法,而 View 只需要通过 measure 方法就能确定测量规格。

View 的测量过程由 View 的 measure 方法完成,measure 方法是一个 final 类型的方法,子类不能重写。

View 的 measure 方法会调用 onMeasure 方法,这个方法我们是可以重写的,onMeasure 的实现如下。

e404ae2490761a29e9a1ea07cd05e7da.png

widthMeasureSpec 和 heightMeasureSpec 是从父 View 传过来的宽高测量规格,getDefaultSize 方法是用来获取默认宽高的,getDefaultSize 的实现如下。

38480b2564d32ec33e41cc5ee73b4305.png

从 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 的布局是下面这样的。

d76adf6f560cb8f78b107ba4ede9477e.png

那么 MyView 测量后的大小就是 600 ,这个 600 是 dp 换算为 px 后的值。

2c8bce7b02281fb53e84a55b4cbc83f6.png

ViewGroup 的 SpecSize 是自身剩余的空间大小,也就是默认子 View 的宽/高为父 View 的剩余控件大小,相当于为宽/高设定的 wrap_content 无效,变成了 match_parent 。

如果我们不想让自定义 View 在宽/高设为 wrap_content 时与父 View 的大小一致,那我们可以像下面这样设定自己的计算好的默认宽/高。

bdcd32051139c5e99044dcbf81d858c8.png

下面来看下 ViewGroup 的测量过程。

ViewGroup 是一个抽象类,没有定义测量的的具体过程,具体的测量过程需要子类实现,下面以 LinearLayout 为例,看一下它的 onMeasure 方法的实现。

304c84bd9fc1d1b0eb9bcf4122748c35.png

LinearLayout 会根据我们设定的方向设定子 View 的测量规格,下面来看下 measureVertical 的实现。

8dffd2f6231b1b2b8fa31fe713bf5167.png

在 measureVertical 方法中,把每一个子元素都传给了 measureChildBeforeLayout ,而 measureChildBeforeLayout 只是调用了 ViewGroup 的 measureChildWithMargin 方法。

cbae3b9d275edcd46b99b7ca71436c9c.png

5

View 放置过程

layout 方法的作用是 ViewGroup 用于确定子元素的位置,当 ViewGroup 的位置确定后,会在 onLayout 方法中遍历所有子 View 并调用子 View 的 layout 方法。

layout 方法用于确定 View 自己的位置,而 onLayout 方法则用于确定所有子元素的位置,View 的 layout 方法的实现如下。

249358ba024b5ceee0367df8ec7f144c.png

View 的 layout 方法首先会通过 setFrame 方法设定 View 的边框,也就是 mLeft、mRight、mTop 和 mBottom 四个顶点的值,这时 View 在父 View 中的位置就确定了。

14aa76d3394996f708a0fc5fc37a6e38.png

设定了四个顶点后,layout 方法就会调用 onLayout 方法确定子 View 的位置,View 和 ViewGroup 都没有实现 onLayout 方法,下面以 LinearLayout 为例,看下 LinearLayout 的 onLayout 方法的实现。

LinearLayout 的 onLayout 方法会根据不同的排列方向调用不同的放置方法,当方向为 VERTICAL 时,对应的放置方法为 layoutVertical ,下面来看下 layoutVertical 方法的实现。

7ebea590c297d7f8e076515999e93f9a.png

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 步是不执行的。

4ebf355b8dccb180c4bfb5d7bfff8acc.png

下面来看下绘制相关方法的实现。

82a871b049828219229c56b846d200df.png

drawBackground 方法首先会通过 Drawable 的 setBounds 方法设置背景绘制的范围,然后如果我们调用过 scrollTo 方法,那么 drawBackground 就会把画布平移到指定位置后再绘制。

View 和 ViewGroup 没有实现 onDraw 方法,接下来就是 dispatchDraw 方法,View 没有实现这个方法,下面来看下 ViewGroup 的 dispatchDraw 方法的实现。

57e595f1be3b5eb6f0078da54b750952.png

在 ViewGroup 的 dispatchDraw 方法中,首先会调用 buildOrderedChildList 方法获取子 View 列表,然后遍历子 View ,通过 drawChild 方法调用每一个子 View 的 draw 方法。

而第 6 步 drawForegounrd 只是获取 foreground 对应的 Drawable 并调用它的 draw 方法。

7

总结

根据前面讲解的内容,从 ViewRootImpl 的 performTraversals 方法开始,大致的方法调用时序图如下。

22ea1b14c1ab7cb74ce8f7e5bc61910a.png

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 开发艺术探索》

setContentView背后的故事返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值