自定义view:view的绘制流程

时间:2021-11-28 08:36:57

1、view的绘制流程

当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树的绘图流程在ViewRootImpl类的performTraversals()函数展开,该函数所做的工作可简单概况为是否需要重新计算视图大小(performMeasure)、是否需要重新安置视图的位置(performLayout)、以及是否需要重绘(而performDraw),流程图如下:

自定义view:view的绘制流程

图中host为的ViewRootImpl全局变量mView

总体来说,UI界面的绘制从开始到结束要经历几个过程:

  • 测量大小,回调 onMeasure()方法
  • 组件定位,回调 onLayout()方法
  • 组件绘制,回调 onDraw()方法

整个绘制流程函数链调用如下:

自定义view:view的绘制流程

需要说明的是,如果用户主动调用 request,只会出发 measure 和 layout 过程,而不会执行 draw 过程。接下来详细介绍各个绘制过程。

2、测量大小

performMeasure()方法负责view自身尺寸的测量。我们知道,在layout布局文件中,每一个view都必须设置layout_width和layout_height属性,属性值有三种可选模式:wrap_content、match_parent和数值,performMeasure()方法根据设置的模式计算出组件的宽度和高度。事实上,大多数情况下模式为 match_parent 和数值的时候是不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式为wrap_content的时候才需要根据内容进行尺寸的测量。

performMeasure()方法的完整代码:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

对象 mView 是 View树的根视图,performMeasure()最终调用了mView的measure()方法,我们进入该方法的源代码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ……
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ……
}

忽略了其余的代码,只剩下了onMeasure这一行,onMeasure()方法是为组件尺寸的测量预留的功能接口,当然,也定义了默认的实现,默认实现并没有太多意义,在绝大部分情况下,onMeasure()方法必须重写。

如果测量的是容器的尺寸,而容器的尺寸又依赖于子组件的大小,所以必须先测量容器中子组件的大小,不然,测量出来的宽度和高度永远为0。

measure 过程会为一个 View 及所有子节点的mMeasuredWidthmMeasuredHeight 变量赋值,该值可以通过getMeasuredWidth()getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于Measure得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次measure。比如,父视图可以先根据未给定的 尺寸去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。

2.1 measure 过程传递尺寸的两个类

  • ViewGroup.LayoutParams (View 自身的布局参数)
  • MeasureSpecs 类(父视图对子视图的测量要求)

ViewGroup.LayoutParams这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 height 和 width,你有以下选择:

  • 具体值
  • MATCH_PARENT 表示子视图希望和父视图一样大(不包含 padding 值)
  • WRAP_CONTENT 表示视图为正好能包裹其内容大小(包含 padding 值)

ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的ViewGroup.LayoutParams的子类RelativeLayoutParams。

有时我们需要使用 view.getLayoutParams()方法获取一个视图LayoutParams,然后进行强转,但由于不知道其具体类型,可能会导致强转错误。其实该方法得到的就是其所在父视图类型的LayoutParams,比如View的父控件为RelativeLayout,那么得到的 LayoutParams 类型就为 RelativeLayoutParams。

MeasureSpecs测量规格,包含测量要求和尺寸的信息,有三种模式:

  • UNSPECIFIED

    父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如 ListView、ScrollView,一般自定义 View 中用不到,
  • EXACTLY

    父视图为子视图指定一个确切的尺寸,而且无论子视图期望多大,它都必须在该指定大小的边界内,对应的属性为 match_parent 或具体值,比如 100dp,父控件可以通过MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。
  • AT_MOST

    父视图为子视图指定一个最大尺寸。子视图必须确保它自己所有子视图可以适应在该尺寸范围内,对应的属性为 wrap_content,这种模式下,父控件无法确定子 View 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。

2.3measure 核心方法

measure(int widthMeasureSpec,intheightMeasureSpec)

该方法定义在View类中,为final类型,不可被复写,但measure调用链最终会回调 View/ViewGroup 对象的onMeasure()方法,因此自定义视图时,只需要复写onMeasure()方法即可。

onMeasure(int widthMeasureSpec,intheightMeasureSpec)

该方法就是我们自定义视图中实现测量逻辑的方法,该方法的参数是父视图对子视图的width和height的测量要求。在我们自身的自定义视图中,要做的就是根据该 widthMeasureSpec和heightMeasureSpec计算视图的width和height,不同的模式处理方式不同。

setMeasuredDimension()

测量阶段终极方法,在onMeasure(intwidthMeasureSpec,intheightMeasureSpec)方法中调用,将计算得到的尺寸,传递给该方法,测量阶段即结束。该方法也是必须要调用的方法,否则会报异常。在我们在自定义视图的时候,不需要关心系统复杂的Measure过程的,只需调用setMeasuredDimension()设置根据MeasureSpec计算得到的尺寸即可,你可以参考ViewPagerIndicator 的 onMeasure 方法。

3、确定子view的位置

performLayout()方法用于确定子组件的位置,所以,该方法只针对 ViewGroup 容器类。作为容器,必须为容器中的子View精确定义位置和大小。该方法的源码如下:

private void performLayout(WindowManager.LayoutParams lp,int desiredWindowWidth, int desiredWindowHeight){
    ……
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ……
    for (int i = 0; i < numValidRequests; ++i) {
        final View view = validLayoutRequesters.get(i);
        view.requestLayout();
    }
}

代码中的 host 是 View树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0,0),其大小默认会填满mContentParent容器。我们重点来看一下 layout()方法的源码:

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;
    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags &PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        ……
    }
    ……
}

在 layout()方法中,在定位之前如果需要重新测量view的大小,则先调用onMeasure()方法,接下来执行setOpticalFrame()或setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用,该方法是空方法。

onLayout()方法在这里的作用是当前组件为ViewGroup时,负责定位ViewGroup中的子组件,这其实是一个递归的过程,如果子view也是一个ViewGroup,该ViewGroup依然要负责他的子vew的定位,依此类推,直到所有的view都定位完成为止。也就是说,从最顶层的DecorView开始定位,像多米诺骨牌一样,从上往下驱动,最后每一个view都放到了他应该出现的位置上。onLayout()方法和上节的onMeasure()方法一样,是为开发人员预留的功能扩展接口,自定义ViewGroup时,该方法必须重写。

首先要明确的是,子视图的具体位置都是相对于父视图而言的。View的onLayout方法为空实现,而 ViewGroup的onLayout为abstract,因此,如果自定义的View 要继承 ViewGroup 时,必须实现onLayout 函数。

在 layout 过程中,子视图会调用getMeasuredWidth()和getMeasuredHeight()方法获取到measure过程得到的mMeasuredWidth和mMeasuredHeight,作为自己的width和height。然后调用每一个子视图的layout(l,t,r,b)函数,来确定每个子视图在父视图中的位置。

4、绘制组件

performDraw()方法执行view的绘制功能,view的绘制是一个十分复杂的过程,不仅仅绘制view本身,还要绘制背景、滚动条,好消息是每个view只需要负责自身的绘制,而且一般来说,ViewGroup不需要绘制。

和measure和layout一样,draw过程也是在ViewRoot的performTraversals()的内部发起的,其调用顺序在measure()和layout()之后,同样的,performTraversals()发起的draw过程最终会调用到mView的draw()函数,这里的mView对于Actiity来说就是PhoneWindow.DecorView。

下面将转到mView.draw(),之前提到mView.draw()调用的就是View.java的默认实现,View类中的draw函数体现了View绘制的核心流程,因此我们下面重点来看下View.java中draw的调用流程:

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)
         */  

        // Step 1, draw the background, if needed
    ...
        background.draw(canvas);
    ...
        // skip step 2 & 5 if possible (common case)
    ...
        // Step 2, save the canvas' layers
    ...
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;  

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
    ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);  

        // Step 4, draw the children
        dispatchDraw(canvas);  

        // Step 5, draw the fade effect and restore layers  

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }
    ...
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

对于View.java和ViewGroup.java,onDraw()默认都是空实现,因为具体View本身长什么样子是由View的设计者来决定的,默认不显示任何东西。

View.java中dispatchDraw()默认为空实现,因为其不包含子视图,而ViewGroup重载了dispatchDraw()来对其子视图进行绘制,通常应用程序不应该对dispatchDraw()进行重载,其默认实现体现了View系统绘制的流程。那么,接下来我们继续分析下ViewGroup中dispatchDraw()的具体流程:

@Override
protected void dispatchDraw(Canvas canvas) {
    ...  

    if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    } else {
        for (int i = 0; i < count; i++) {
            final View child = children[getChildDrawingOrder(count, i)];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
    }
     ......
} 

dispatchDraw()的核心代码就是通过for循环调用drawChild()对ViewGroup的每个子视图进行绘制,上述代码中如果FLAG_USE_CHILD_DRAWING_ORDER为true,则子视图的绘制顺序通过getChildDrawingOrder来决定,默认的绘制顺序即是子视图加入ViewGroup的顺序,而我们可以重载getChildDrawingOrder函数来更改默认的绘制顺序,这会影响到子视图之间的重叠关系。

drawChild()的核心过程就是为子视图分配合适的cavas剪切区,剪切区的大小正是由layout过程决定的,而剪切区的位置取决于滚动值以及子视图当前的动画。设置完剪切区后就会调用子视图的draw()函数进行具体的绘制,如果子视图的包含SKIP_DRAW标识,那么仅调用dispatchDraw(),即跳过子视图本身的绘制,但要绘制视图可能包含的字视图。完成了dispatchDraw()过程后,View系统会调用onDrawScrollBars()来绘制滚动条。

view的绘制最终是通过Canvas类完成的,该类定义了若干个绘制图形的方法,通过Paint类配置绘制参数,便能绘制出各种图案效果。有时候为了提高绘图的性能,使用了Surface技术,Surface提供了一套双缓存机制,能大大加快绘图效率,而我们绘图时需要的 Canvas 对象也由是 Surface创建的。

View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事:

  • 绘制背景:background.draw(canvas)
  • 绘制自己:onDraw(canvas)
  • 绘制子视图:dispatchDraw(canvas)
  • 绘制滚动条:onDrawScrollBars(canvas)

组件的绘制也是一个递归的过程,说到底Activity的UI界面的根一定是容器,根容器绘制结束后开始绘制子组件,子组件如果是容器继续往下递归绘制,否则将子组件绘制出来……直到所有的组件正确绘制为止。