View 的绘制过程

1,282 阅读9分钟

写在开头

从一开始学习安卓就想知道,为什么我在xml布局文件里声明了一个button之后,运行的时候就能按照我声明的参数在指定位置按照指定大小给我绘制出来,一开始想一口吃成个胖子,看了网上的文章有了初步的理解,然而。。。忘得真的太快了,现在来亲自梳理一遍view的显示过程,包括view的测量、布局和绘制。

View和ViewGroup简介

在安卓中,显示的视图可以分为两类,view和viewgroup,而view是所有控件的基类,viewgroup也是继承自view的,不同的是viewgroup可以容纳子view,而view不能再容纳任何子view。而我们平时在布局文件里使用的绝大多数控件,都是view类和viewgroup的子类。
viewgroup的子类:各种layout布局,比如linerlayout,framelayout,relativelayout等等。
view的子类:各种控件,比如textview,button等等。
这样以来,布局文件里的内容就形成了一棵树状结构,即我们经常谈起的view树:

为了方便描述,下文用view指代控件(不能包含子view),用viewgroup指代容器(可以包含子view)

View的显示过程

view的显示必须经过测量、布局和绘制三个阶段,分别对应onMeasure()、onLayout()和onDraw()三个回调方法。作用分别如下:
onMeasure()——经过测量初步确定view的宽和高(至于为什么是初步确定,后面再讲解)
onLayout()———根据onMeasure()中初步确定的宽高,对view进行布局即确定当前的view应该放在屏幕的哪个位置,(这一步有可能对初步确定的宽和高重新赋值)
onDraw()———调用绘图api(canvas.drawXX()方法)在该位置绘制view,至此,view便显示在屏幕上了

1.view的测量

首先需要明确一点,所有子view的测量都是在直接父viewgroup的控制下完成的,什么意思?比如现在有如下布局文件:

根布局linearlayout具有两个直接子view——framelayout和button,所以framelayout和button的测量是由linearlayout控制完成的,那么怎么控制并完成的呢?
首先,viewgroup自身的measure()方法被调用,在进行一些处理之后,measure()方法内部调用onMeasure()方法,onMeasure()方法循环遍历自己的直接子view,并且调用每个子view的measure()方法(不是onMeasrue()),FrameLayout中该方法部分实现如下:

if (count > 1) {
       for (int i = 0; i < count; i++) {
           final View child = mMatchParentChildren.get(i);
           //获取子view在xml文件中设置的布局参数
           final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
           //根据子view的布局参数确定子view的width,不进行合法性检查
           final int childWidthMeasureSpec;
           if (lp.width == LayoutParams.MATCH_PARENT) {
               final int width = Math.max(0, getMeasuredWidth()
                       - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                       - lp.leftMargin - lp.rightMargin);
               childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                       width, MeasureSpec.EXACTLY);
           } else {
               childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                       getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                       lp.leftMargin + lp.rightMargin,
                       lp.width);
           }
           //根据子view的布局参数确定子view的height,不进行合法性检查
           final int childHeightMeasureSpec;
           if (lp.height == LayoutParams.MATCH_PARENT) {
               final int height = Math.max(0, getMeasuredHeight()
                       - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                       - lp.topMargin - lp.bottomMargin);
               childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                       height, MeasureSpec.EXACTLY);
           } else {
               childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                       getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                       lp.topMargin + lp.bottomMargin,
                       lp.height);
           }
           //调用子view的measure()方法
           child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
       }
   }

可以看到最终父viewgroup调用了每个直接子view的measure()方法,而为什么不是调用子view的onMeasure()方法呢?我理解的原因主要有两个:

  • onMeasure()方法为protected类型,只能在子类内部访问,不能在外部访问。
  • measure()方法还会进行进一步的复杂处理,包括传入参数的优化等,执行这些代码之后,才会调用onMeasure()方法。

这是view.measure()方法中调用onMeasure()的部分:

if (cacheIndex < 0 || sIgnoreMeasureCache) {
           onMeasure(widthMeasureSpec, heightMeasureSpec);
           mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
       } 

下面是View.onMeasure()的默认实现:

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

通过 setMeasuredDimension方法设定该view的初步测量值。
最内层的getSuggestedMinimumWidth()将返回父view能容纳的最大值。
getDefaultSize方法用于返回默认的大小,实现如下:

 public static int getDefaultSize(int size, int measureSpec) {
    //将result赋值为父视图的最大值
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    //未确定的时候赋值为size
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    //wrap_content的时候不进行赋值操作,即默认为size的大小
    case MeasureSpec.AT_MOST:
    //指定为精确值
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

分析以上方法的执行可以得出结论,默认的View.onMeasure()方法不支持wrap_content属性,即使在xml中指定为wrap_content也会默认设置为充满父view所以,在自定义view的时候,要想你的view支持wrap_content属性,必须覆盖view的onMeasure()方法并加入wrap_content的相关逻辑。view.measure为final方法,不能覆盖

这样,view的测量阶段就完成了。

view的布局

同view的测量类似,这一阶段viewgroup自身的layout()方法被调用,确定自己的位置之后,layout()方法内部调用onlayout(),onLayout()调用layoutChildren()方法来对所有直接子view进行布局,FrameLayout中的具体实现:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false );
}

layoutChildren()方法的部分实现:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    //获取直接子view数量
    final int count = getChildCount();
    此处代码有省略...
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            //获取子view在xml布局文件中定义的属性
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            此处代码省略...(省略的代码主要用于各种判断逻辑,用来确定布局位置)
            //调用子view的layout方法
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

可以看到和测量过程类似,都是通过循环取得每一个子view,再获得子view在xml文件中定义的各种属性,加以判断,最后调用子view的对应方法,不同的是,测量过程更关心view的宽和高,所以主要判断大小相关的属性。而布局过程更关心view的位置,所以判断的属性大都是view的位置相关属性,比如centerinparent是否为true等等。
在view的layout()方法又会调用onLayout()方法:

  public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

//调用setframe或者setOpticalFrame方法来设置该自己相对于父viewgroup的坐标(左上和右下)
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //调用子view的onLayout()方法
        onLayout(changed, l, t, r, b);
        ...
}

View类的onLayout()方法默认实现为空:

 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

再来总结一下layout阶段的工作:

  1. 父viewproup的layout方法被调用,首先调用setframe或者setOpticalFrame设置自己的位置,然后调用onLayout()方法循环遍历子view并调用子view.layout()
  2. 子view的layout()方法通过调用内部setframe或者setOpticalFrame方法来设置该view相对于父viewgroup的坐标
  3. 子view.layout()方法调用自己的onLayout()方法

由于view不能再包含子view,而且view的位置在view类内部的layout()方法中已经通过setframe或者setOpticalFrame确定,所以继承view类来自定义view的时候,一般不用覆盖onLayout()方法
到此,view的位置也已经确定好了。

view的绘制

这一阶段父viewproup调用子视图(可能是viewgroup也可能是view)的draw()方法,draw()方法在View类进行了实现,部分实现如下:

public void draw(Canvas canvas) {
    ...
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */
    ...
    if (!verticalEdges && !horizontalEdges) {
        // 绘制自己
        if (!dirtyOpaque) onDraw(canvas);
        // 绘制子视图
        dispatchDraw(canvas);
        ...
}

可以看到draw()方法先调用自己的onDraw()方法绘制自己,然后调用 dispatchDraw(canvas)来遍历得到所有子视图(可能是viewgroup也可能是view)并调用子视图的draw()方法,draw()方法的关键实现已在上面代码中说明。
可以看到

  1. 假如子视图是一个view,调用了子视图的onDraw()方法之后,dispantchDraw()虽然得到了调用,但是方法中不会再进行绘制子视图操作,因为view不可能在包含子视图。
  2. 假如子视图是viewgroup,若该viewgroup没有指定background属性,那么该viewgroup的OnDraw()方法不会被调用(由于if(!dirtyOpaque)),但是dispatchDraw()方法始终会被调用,因为viewgroup一般都包含有子视图。

至此,view的绘制过程也结束了,view最终成为了我们看到的样子。

view显示过程缩略概括

  1. viewgroup和view的测量:
    首先,最外层viewgroup的measure()方法得到调用,measure()方法进行了一些处理之后,其内部又调用了onMeasure()方法,在onMeasure()方法中除了进行自身的测量并调用setMeasuredDimension()设置自身大小之外,若该视图下包括了子视图,还要遍历调用所有子视图的measure()方法。
    所以viewgroup的onMeasure()方法除了初步测量自身的大小,还要初步测量子view的大小,并调用子.view的measure()方法传入初步测量的高和宽,由子view的onMeasure()方法再次确定合适的大小
  2. viewgroup和view的布局:
    最外层viewgroup的layout()被调用,该方法设置了自己的位置之后,调用自身onLayout()遍历所有子视图并调用子视图的layout()方法。
    ViewGroup类的onLayout()方法是抽象方法,自定义viewgroup时必须实现,而View类的onLayout()方法默认实现为空
  3. viewgroup和view的绘制
    最外层viewgroup的draw()被调用,viewgroup若没有设置background属性则不会执行自己的onDraw(),继续执行dispatchDraw()遍历绘制所有子view。

拓展

  1. 既然measure、layout和draw都是从最外层viewgroup开始的,那么是谁调用了最外层viewgroup的measure()、layout()和draw()方法呢?
    答案是viewRoot的performTraversals()方法:

    private void performTraversals() {
    
        ............
        //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
         int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
         int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    
          // Ask host how big it wants to be
          //执行测量操作
          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
        ........................
        //执行布局操作
         performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    
        .......................
        //执行绘制操作
        performDraw();
    
    }
    

    这里给出一张安卓UI的架构图:

    至于Activity,Window,DecorView 和 ViewRoot 的关系和职责,可以参考以下链接:
    点我点我

  2. view.getMeasuredWidth()和view.getWidth()的区别
    view.getMeasuredWidth()——获得在onMeasure()方法中调用setMeasuredDimension()之后的view宽度,执行setMeasuredDimension()之后mMeasuredWidth才被赋值,进而onLayout阶段可以调用getMeasuredWidth()获得初步测量的宽度
  • view.getWidth()——获得在onLayout()方法之后的view宽度
    在重写了onLayout方法之后,二者的返回值视重写的逻辑而定有可能不同,比如某个view在xml中指定宽度为1000dp,那么测量阶段的值就是1000dp,而假如你在layout()里加入判断认为该值过大,然后在该方法中向的setframe()传递了一个新的宽度值,getWidth()方法就将返回该值,这只是举的特殊情况,大多数情况下二者还是一样的
支持原创技术分享,您的鼓励将是我最大的动力! 赏 DistanceLin Alipay

支付宝打赏