View绘制流程重要知识点梳理

210 阅读6分钟

在开始之前,先来一张图片梳理思路。(估计只有我自己看得懂/(ㄒoㄒ)/~~

一,measure过程

measure()过程由measure(int, int)方法发起,从上到下有序测量View,

measure过程会为一个View及其所有子View的mMeasuredWidth和mMeasuredHeight变量赋值

1.储备知识

在查看view.measure()的源码之前,我们需要了解MeasureSpec和LayoutParams这两个类。

1.1MeasureSpecs

表示测量规格,包含测量模式和大小。有三种模式

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,
  • EXACTLY:父容器已经检测出View所需要的精确大小,此时View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。
  • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,这是我们自定义View时需要处理的情况,它对应于LayoutParams中的wrap_content

1.2LayoutParams

在实际代码中,我们可以给View设置LayoutParams。在父View的measure的过程中,系统会根据子View的LayoutParams和父容器的约束计算出子View本身的MeasureSpec。这里的父容器的约束,存在两种情况:

  • 第一种是对DecorView,所谓的父容器的约束就是窗口的尺寸
  • 第二种是对普通的View,所谓的父容器的约束就是父容器的MeasureSpec

这张表是对getChildMeasureSpec()这个方法的工作原理的梳理,

原图来自:简书

2.measure过程

View 的 measure 是final,所以 View 的子类(比如 ViewGroup / 自定义控件)是不能 override 这个方法的。所以 ViewGroup 没有实现 measure 方法,也就是说,它使用的是父类 View 的 measure 。

View的measure过程

View类中默认的onMeasure()方法,只会测量自身的尺寸,并调用setMeasuredDimension()保存尺寸。其中调用了getDefaultSize()方法。

protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

如果View在布局中使用wrap_content,那么它的specMode就是AT_MOST模式,从getDefaultSize()方法来看,那么最后View的宽/高就是specSize,也就是父容器当前剩余的空间大小。这种效果和在布局中使用match_parent完全一致。

因此,直接继承View的自定义控件需要重写onMeasure()方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。

那么举个例子?

在这种情况下,我们只需要指定一个默认的wrap_content下的mWidth,mHeight,然后通过resolveSize()方法,使其符合父View的限制。

最后调用setMeasuredDimension()方法来保存最后的宽高就行了。

类似这样:

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = ---;
        int height = ---;
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程外,还会去调用所有子View的measure()方法。

需要注意的是,ViewGroup是一个抽象类,它没有重写View的onMeasure()方法,它只是提供了measureChildren,measureChild,measureChildWithMargins等测量子View相关的方法。

在自定义ViewGroup中,关键在于,根据需求覆写onMeasure()从而实现你的子View测量逻辑。

在覆写onMeasure()过程中,可以使用ViewGroup提供的几个通用的测量下层View的相关方法:getChildMeasureSpec(),measureChild(),measureChildWithMargins(),measureChildren()

getChildMeasureSpec()

传给子View的measure()方法中的measureSpec值,就是通过getChildMeasureSpec()这个方法计算出来的。这个方法一般在父ViewGroup的onMeasure()方法内部被调用。在其内部,通过父视图的MeasureSpec和子View的LayoutParams,计算出子View的MeasureSpec

二,layout过程

layout过程就是摆放View的位置。

与 Measure 过程不同的是,layout 调用时,layout 中先 setFrame 确定当前 View 的位置,再 onLayout 遍历下层 View 并确定位置。

View 中的 layout() 是可以被覆写的。 ViewGroup 中的 layout() 是 final 的,不能被覆写。

在完成 layout 之后,可以使用 getWidth 和 getHeight,获得正常尺寸,这个尺寸是通过计算摆放完的控件坐标得来。getWidth = right - left

View 的 onLayout 方法为空实现,而 ViewGroup 的 onLayout 为 abstract 的,因此,如果自定义的 View 要继承 ViewGroup 时,必须实现 onLayout 函数。

ViewGroup的实现类

自定义View继承ViewGroup时,我们必须实现onLayout()方法。

那么如何写实现onLayout()呢?

根据这个 ViewGroup 子类的摆放逻辑,当前 ViewGroup 剩余空间,onMeasure 过程中得到的下层 View 的相关尺寸,LayoutParams 的 margin,gravity 等,计算出每个下层 View 应处的左上右下位置(可能已经在 onMeasure 时已经一起测量出来,并且保存在某个变量中),最终调用每个下层 View 的 layout(),形成递归。

三,draw过程

View 中的draw() 方法不是 final 的,可以被覆写。

ViewGroup 没有draw()实现。它使用的是父类 View 的 draw 。

主要就是调用了下面几个方法:

  1. drawBackground() 背景(不能覆写)
  2. onDraw() 当前 View
  3. dispatchDraw() 下层 View
  4. onDrawForeground() 滑动边缘渐变提示和滚动条,前景。如果覆写,需要 minSdk>=23

不同于 measurelayout ,遍历下层 View 这个步骤,不是在 onDraw 中,而是在 draw 中。并且有专门的 dispatchDraw方法遍历下层 View 。

onDraw

实现当前 View 自身内容的绘制(包括 padding 的处理)。

View 和 ViewGroup 的 onDraw(canvas) 都是空实现。

ViewGroup 的 draw() 默认不会调用 onDraw() 方法。因为正常来说,ViewGroup 是一个 View 容器,自身不会有具体画面。

如果需要回调 onDraw() 方法,在构造函数中调用 setWillNotDraw(false) 即可。(但是,如果你继承的是比如 ScrollView 这种 ViewGroup ,它已经调用过 setWillNotDraw(false) 了)

draw() 过程中,某些情况下,比如只是前景状态改变,系统会做相应优化,跳过 onDraw()

四,其他方法

requestLayout

requestLayout 意味着视图的大小已经改变,整个 View 树将会重新测量,重新布局,可能会重新绘制

因此,为了确保重新布局会导致重画,那么你应该在 requestLayout 配一个 invalidate。

invalidate

invalidate 请求把 View 重新 draw 一下。

invadite() 必须在主线程中调用。

postInvalidate() 只有视图被添加到窗口的时候才会继续执行,也就是 attachInfo 不为 null 的时候。内部是由 Handler 的消息机制实现的,所以在任何线程都可以调用,但实时性没有 invadite() 强。一般保险起见,会使用 postInvalidate() 来刷新界面。

resolveSize()

这里的第一个参数width,和height,所代表的就是你希望在MeasureSpec的SpecMode为AT_MOST的情况下,view的最后的宽高。但这里有个例外,在AT_MOST情况下,width或height如果大于父布局所能允许的最大宽高的话,还是会使用父布局所允许的最大宽高,而非你传入的width和height。相较于自己手动用条件语句判断SpecMode,这个方法可以减少一些样板代码。

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = ---;
        int height = ---;
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

五,总结

以上就是View绘制流程的一些需要注意的点了。如果有什么写的不对的或不好的地方,欢迎评论。

参考:

Android View 测量布局绘制过程

carson_ho的View绘制流程系列

codekk:公共技术点之View绘制流程