美文网首页
Android View 的工作流程和原理

Android View 的工作流程和原理

作者: Joe_2e0c | 来源:发表于2019-05-27 10:31 被阅读0次

    performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理,performLayout 和 performDraw 的传递流程和 performMeasure 是类似的,唯一不同的是,performDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

    接下来结合源码来分析这三个过程。

    Measure 测量过程

    这里分两种情况,View 的测量过程和 ViewGroup 的测量过程。

    View 的测量过程

    View 的 测量过程由其 measure 方法来完成,源码如下:

    可以看到 measure 方法是一个 final 类型的方法,这意味着子类不能重写此方法。

    在 13 行 measure 中会调用 onMeasure 方法,这个方法是测量的主要方法,继续看 onMeasure 的实现

    setMeasuredDimension 方法的作用是设置 View 宽和高的测量值,我们主要看 getDefaultSize 方法

    是如何生成测量的尺寸。

    可以看到要得到测量的尺寸需要用到 MeasureSpec,MeasureSpec 是什么鬼呢,敲黑板了,重点来了。

    MeasureSpec 决定了 View 的测量过程。确切来说,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格。

    来看 MeasureSpec 类的实现

    可以看出 MeasureSpec 中有两个主要的值,SpecMode 和 SpecSize, SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下的规格大小。

    MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize

    SpecMode 有三种模式:

    UNSPECIFIED

    不限制:父容器不对 View 有任何限制,要多大给多大,这种情况比较少见,一般不会用到。

    EXACTLY

    限制固定值:父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。

    AT_MOST

    限制上限:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

    MeasureSpec 中三个主要的方法来处理 SpecMode 和 SpecSize

    makeMeasureSpec 打包 SpecMode 和 SpecSize

    getMode 解析出 SpecMode

    getSize 解析出 SpecSize

    不知道童鞋们之前有没有注意到 onMeasure 有两个参数 widthMeasureSpec 和 heightMeasureSpec,那这两个值从哪来的呢,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小,但是最外层的根视图 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是从哪里得到的呢?这就需要去分析 ViewRoot 中的源码了,在 performTraversals 方法中调了 measureHierarchy 方法来创建 MeasureSpec 源码如下:

    里面调用了 getRootMeasureSpec 方法生成 MeasureSpec,继续查看 getRootMeasureSpec 源码

    通过上述代码,DecorView 的 MeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的 LayoutParams 中的宽和高的参数来划分。

    LayoutParams.MATCH_PARENT:限制固定值,大小就是窗口的大小 windowSize

    LayoutParams.WRAP_CONTENT:限制上限,大小不定,但是不能超过窗口的大小 windowSize

    固定大小:限制固定值,大小为 LayoutParams 中指定的大小 rootDimension

    对于 DecorView 而言, rootDimension 的值为 lp.width 和 lp.height 也就是屏幕的宽和高,所以说 根视图 DecorView 的大小默认总是会充满全屏的。那么我们使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 产生过程又是怎么样的呢,在 ViewGroup 的测量过程中会具体介绍。

    先回头看 getDefaultSize 方法:

    现在理解起来是不是很简单呢,如果 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,这也是系统默认的行为。之后会在 onMeasure 方法中调用 setMeasuredDimension 方法来设定测量出的大小,这样 View 的 measure 过程就结束了,接下来看 ViewGroup 的 measure 过程。

    ViewGroup 的测量过程

    ViewGroup中定义了一个 measureChildren 方法来去测量子视图的大小,如下所示

    从上述代码来看,除了完成自己的 measure 过程以外,还会遍历去所有在页面显示的子元素,

    然后逐个调用 measureChild 方法来测量相应子视图的大小

    measureChild 的实现如下

    measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。

    那么 ViewGroup 是如何创建来创建子元素的 MeasureSpec 呢,我们继续看 getChildMeasureSpec 方法源码:

    上面的代码理解起来很简单,为了更清晰地理解 getChildMeasureSpec 的逻辑,这里提供一个表,表中对 getChildMeasureSpec 的工作原理进行了梳理,表中的 parentSize 是指父容器中目前可使用的大小,childSize 是子 View 的 LayoutParams 获取的值,从 measureChild 方法中可看出

    表如下:

    通过上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。

    至此,View 和 ViewGroup 的测量过程就告一段落了。来个小结。

    MeasureSpec 的模式和生成规则

    MeasureSpec 中 specMode 有三种模式:

    UNSPECIFIED

    不限制:父容器不对 View 有任何限制,要多大给多大,这种情况比较少见,一般不会用到。

    EXACTLY

    限制固定值:父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值这两种模式。

    AT_MOST

    限制上限:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

    生成规则:

    对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。

    对于不同 ViewGroup 中的不同 View 生成规则参照上表。

    MeasureSpec 测量过程:

    measure 过程主要就是从顶层父 View 向子 View 递归调用 view.measure 方法,measure 中调 onMeasure 方法的过程。

    说人话呢就是,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在 XML 文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

    那么测量过后,怎么获取 View 的测量结果呢

    一般情况下 View 测量大小和最终大小是一样的,我们可以使用 getMeasuredWidth 方法和 getMeasuredHeight 方法来获取视图测量出的宽高,但是必须在 setMeasuredDimension 之后调用,否则调用这两个方法得到的值都会是0。为什么要说是一般情况下是一样的呢,在下文介绍 Layout 中会具体介绍。

    Layout 布局过程

    测量结束后,视图的大小就已经测量好了,接下来就是 Layout 布局的过程。上文说过 ViewRoot 的 performTraversals 方法会在 measure 结束后,执行 performLayout 方法,performLayout 方法则会调用 layout 方法开始布局,代码如下

    View 类中 layout 方法实现如下:

    layout 方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的,然后会调用 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了,接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似

    onLayout 源码如下:

    纳尼,怎么是个空方法,没错,就是一个空方法,因为 onLayout 过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置,我们继续看 ViewGroup 中的 onLayout 方法

    可以看到,ViewGroup 中的 onLayout 方法竟然是一个抽象方法,这就意味着所有 ViewGroup 的子类都必须重写这个方法。像 LinearLayout、RelativeLayout 等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。所以呢我们如果要自定义 ViewGroup 那么就要重写 onLayout 方法。

    xml 中使用

    显示效果如下:

    不知道童鞋们发现了没,我给自定义的 ViewGroup 设置了背景色,看效果貌似占满全屏了,可是我在 xml 中设置的 wrap_content 啊,这是什么情况,我们回头看看 ViewGroup 中 View 的 MeasureSpec 的创建规则

    从表中可看出因为 ViewGroup 的父布局设置的 match_parent 也就是限制固定值模式,而 ViewGroup 设置的 wrap_content,那么最后 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那么如果我们给 ViewGroup 设置固定值就会使用 我们设置的值,来改下代码。

    效果如下:

    表中的其他情况,建议童鞋们自己写下代码,会理解的更好。

    之前说过,一般情况下 View 测量大小和最终大小是一样的,为什么呢,因为最终大小在 onLayout 中确定,我们来改下代码:

    显示效果

    没错,onLayout 就是这么任性,所以要获取 View 的真实大小最好在 onLayout 之后获取。那么如何来获取 view 的真实大小呢,可以通过下面的代码来获取

    打印如下:

    可以看到实际高度和测试的高度是不一样的,因为我们在 onLayout 中做了修改。

    因为 View 的绘制过程和 Activity 的生命周期是不同步的,所以我们可能在 onCreate 中获取不到值。这里提供几种方法来获取

    1.Activity 的 onWindowFocusChanged 方法

    2.view.post(runnable) 也就是我上面使用的方法

    3.ViewTreeObserver 这里童鞋们搜索下就可以找到使用方法,篇幅较长就不举例子了

    Draw 绘制过程

    确定了 View 的大小和位置后,那就要开始绘制了,Draw 过程就比较简单,它的作用是将 View 绘制到屏幕上面。View 的绘制过程遵循如下几步:

    绘制背景 background.draw (canvas)

    绘制自己(onDraw)

    绘制 children(dispatchDraw)

    绘制装饰(onDrawScrollBars)

    View 的绘制过程的传递是通过 dispatchDraw 实现的,dispatchdraw 会遍历调用所有子元素的 draw 方法,如此 draw 事件就一层一层的传递下去。和 Layout 一样 View 是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制,重写 onDraw 方法。具体可参考 TextView 或者 ImageView 的源码。

    相关文章

      网友评论

          本文标题:Android View 的工作流程和原理

          本文链接:https://www.haomeiwen.com/subject/pcvfuqtx.html