【 Android】 View 的绘制流程

654 阅读10分钟

View 的视图系统组成

点击 App 后~

在开发的过程中,对于界面的绘制我们实际上接触到的是写 XML 文件。

绘制绘制,我们需要知道要对哪些 View 进行绘制,因为展示在我们面前的界面不可能绘制我们写的 XML 所对应的 View,View 的绘制其实包含了很多部分。

整个 View 的视图组成参考# 通俗易懂 Android视图系统的设计与实现的第二小节,写得非常清楚。

也参考自:Android从Activity启动到View显示中间发生了什么?

image.png

image.png 参考:juejin.cn/post/707745…

布局与测量

参考扔物线自定义 View

引言:一般来说,作为开发者我们不需要去关心页面内部是如何布局的。我们只管根据业务需求,编写好 xml 布局文件,系统就会帮我们处理好这些。但是,有些时候系统自带的控件没办法满足我们的要求。

比如,要写一个正方形的 ImageView,怎么办?可能会回答将其 layout_width与 layout_height设置成一样大就好了。但是如果不能确定这两个值,而是要根据父view 动态决定呢?

又比如标签布局(可以试着自己去实现一下)

这就得自己去定义他的尺寸算法了

测量阶段

Android 里的布局,其实包含两个过程:测量过程和布局过程。

首先,界面会从最顶级的根 View 向下递归地测量出每一级、每一个子 View 的尺寸和位置。 然后就是绘制过程,每一个 View 根据自己的尺寸和位置,进行自我绘制。

具体分析就是,当布局过程到来的时候。首先,一个 View 的 measure 方法会被他的父 View 调用。这个方法的作用是让这个 View 进行自我测量,不过真正执行测量的,不是 measure 方法,而是被调用的 onMeasure()方法。

measure 方法在被调用的时候,会做一些测量的预处理工作(计算 MeasureSpec 的值),然后去调用 onMeasure 方法去进行真正的自我测量。

这个自我测量的内容包含两种情况:(有没有子 View)

  • View:没有子 View,做的事情只有一件,计算出自己的尺寸。
  • ViewGroup:调用所有子 View的 measure 方法,让他们进行自我测量。根据子 View 自我测出来的尺寸,来计算出它们的位置,并且把它们的尺寸和位置保存下来。同时,还会根据这些子 View 的尺寸和位置,最终得出自己的尺寸。

image.png

布局阶段

在测量阶段,每一个 View 或者 ViewGroup 也会被父 View 调用它的一个方法:layout 。这个方法会对 View 进行内部布局。

与 measure 一样,layout 方法也是个调度方法,他在内部会做两件事。

  1. 保存 layout 方法传入的位置和尺寸
  2. 调用 onLayout 方法,也就是调用每一个子 view 的 layout 方法

image.png

这就是内部布局,也就是根据测量结果摆放所有子 View 的意思。

image.png

layout这一步 就相当于告述子 View:给你你的坐标。你已经有了自己的尺寸了,去吧,去绘制吧。

布局过程的自定义

分为三种类型,由简入难。

一:重写布局过程的相关方法

  1. 测量过程:onMeasure()
  2. 布局过程:onLayout()
  • 具体

重写onMeasure()来修改已有的 View 的尺寸。 这一类是对于已有的 View,他已经有了自己的尺寸算法,他的 onMeasure()方法已经正确的计算出它的尺寸了。不需要重新计算一遍,根据自己的需求进行调整。

image.png

二. 重写 onMeasure()来全新计算自定义 View 的尺寸。

与第一类的区别: **需要完全计算自己的尺寸**

注意点:

- 重写 onMeasure,也不用调用 super.onMeasure();
- 计算出来的尺寸,要满足父 View 的限制;

image.png widthMeasureSpec与heightMeasureSpec包含了父 View 对子 VIew 的尺寸限制。

问题:

  • 这个限制怎么来的?

答:开发者对于子 View 的要求,也就是在 xml 布局文件中,里面的layout_打头的属性。它们是用来设置view 的位置和尺寸的,如很常用的layout_width layout_height layout_gravity layout_weight;

image.png

如图所示为很常规的xml 布局文件,比如android:text =""这些是给 view 自己处理的,规定显示什么内容。而android:layout_width这些是给父 View 处理的。

也就是说,在程序运行的时候,每一个ViewGroup会去读取它的子 View 的这些 layout_打头的属性,然后用它们去进行处理和计算。

  • 子 View 的 onMeasure()应该怎么做来让自己符合这个限制? 答:

image.png

需要先知道的是,父 View 传进来的参数,也就是宽度高度的限制,measureSpec是一个压缩数据,包含了类型与尺寸值( int 应该是前几位代表类型 mode,后几位代表尺寸值)。

限制一共有三种:

  • UNSPECIFIED:无限制,会直接把子 view 的计算结果返回。
  • AT_MOST:限制上限。父 View 限制子 View 的上限是 500.
  • EXACTLY:限制固定尺寸。

image.png

总结:

  1. 重写 onMeasure()方法把尺寸计算出来;

  2. 把计算的结果用 resolveSize()过滤一遍然后再保存;

三. 重写 onMeasure()和 onLayout() 来全新计算 ViewGroup 的内部布局。

第一步 重写 onMeasure()来计算内部布局。

1.调用每个子 View 的 measure(),让子 View 自我测量

这一步看起来很简单,就是遍历子 View,调用其 onMeasure 方法。确实是这个思路,但是别忘了,在调用子 View 的 mearsure 方法时,得传进去对子 View 的宽高限制。而 ViewGroup 如何确定自己对子 View 的宽高限制,是个麻烦事。

ViewGroup 对子 View 的宽高限制,来源于两个地方:开发者在 xml中的要求、自己的可用空间。

首先是开发者的要求,例如在 xml 中对子 View 写上 layout_width\layoutHeight.

子 View 中调用getLayoutParams获得的 LayoutParams 对象,存储了 xml 文件里layout_打头的参数的对应值。

image.png

image.png

image.png 解释上图: 首先,最简单的就是在布局里写定了值,这种直接使用该值和设置模式为 EXACTLY;

MATCH_PARENT:如果自己的模式是无限制,则子 View 也别限制了。如果不是无限制,则用自己的可用空间减去已经使用的空间。

WRAP_CONTENT:如果自己的模式是无限制,则子 View 也别限制了。如果不是无限制,则用自己的可用空间减去已经使用的空间。

2.根据子 View 给出的尺寸,得出子 View 的位置,并保存它们的位置和尺寸。

测量子 View 的过程,其实就是一个排班过程,那么在测量过程中就能够拿到每一个子 View 的位置,然后再保存下来。(这句听的云里雾里,还是不了解怎么样能拿到每一个子 VIew 的位置,只是知道调用完子 View 去测量之后,会同时存下来子 View 的位置)

尺寸简单,99%的情况每一个 View 所测得的尺寸就是他的最终尺寸,调用 getMeasureWidth就行。

关于保存子 View 位置的两点说明

  1. 不是所有的 Layout 都需要保存子 View 的位置,因为有的例如 LinearLayout,他的内容都是横向或者竖向一字排开的,可以在布局阶段实时推导出子 View 的位置。
  2. 特殊情况下对某些子 View 需要重复测量两次或多次才能得到正确的尺寸。
3. 根据子 View 的位置和尺寸来计算出自己的尺寸,并用 setMeasuredDimension()保存。

根据子 View 的排布计算出边界,这个边界就是你的尺寸了。

第二步 重写onLayout()来摆放子 View。

image.png

只需要注意,子 View 的 layout 的四个参数,是在它父 View 中的左上右下四个相对坐标。

总结:

image.png

面试回答

1.来讲讲 View 的绘制流程。

补充从 setContentView 到自己写的 xml 加入到 DecorView 的过程。


private void performTraversals() {
    //desiredWindowWidth和desiredWindowHeight是屏幕的尺寸
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();
}


private static int getRootMeaureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATRCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}


我们在写 xml 的时候,外层都有个 layout 布局,layout 也是个 ViewGroup 。DecorView 在调用performMeasure时,会将具体的测量操作让内部的 ViewGroup 执行。



private void perormMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) 
{ ... 
// 具体的测量操作分发给ViewGroup 
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
... 
    
}

// 在ViewGroup中的measureChildren()方法中遍历测量ViewGroup中所有的View
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        // 当View的可见性处于GONE状态时,不对其进行测量
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

// 测量某个指定的View
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    
    // 根据父容器的MeasureSpec和子View的LayoutParams等信息计算
    // 子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// View的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    // ViewGroup没有定义测量的具体过程,因为ViewGroup是一个
    // 抽象类,其测量过程的onMeasure方法需要各个子类去实现
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

// 不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // setMeasureDimension方法用于设置View的测量宽高
    setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = sepcSize;
            break;
    }
    return result;
}

//如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinmumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

mView为一个 ViewGroup,调用其 measure 方法会调用其 onMeasure 方法,因为其为一个 ViewGroup,所以会调用所有子 View的 measure 方法,让他们进行自我测量。

而在子 View 的自我测量中,是调用子 view 的 measure 方法,同时要传入MeasureSpec。这个MeasureSpec是子 View 在测量自己尺寸时宽度高度的限制。

这里可以引出问题2:当调用下一级 View的 measure 方法时,这个MeasureSpec的值应该怎么处理?

当子 View 调用自己的 onMeasure 方法,就开始了自我测量的过程,最后使用setMeasureDimension方法保存自己的宽度和高度。

ViewGroup 在遍历完所有子 View,让子 View 都测量出自己的尺寸时。会保存子 View 的尺寸并算出这些子 View 的位置。

juejin.cn/user/431853… 未完待续

2.MeasureSpec的值应该怎么处理

3.ViewGroup 如何算出子 View 的位置。