View的绘制流程

引言

从事android开发已经有些年头了,可能很多人跟我一样,开发时间越长越发现移动开发的局限性。也常常会生出学习其他语言、从事其它岗位的念头~~咳咳,好像跑题了。这篇文章的目的主要是为了让大家更清楚的了解View,如果有什么不正确的认识,希望大家多多指正。后续我可能会记录下自己学习其它语言的过程,希望与君共勉,毕竟一个不想当厨子的医生不是一个好裁缝嘛!

View 绘制机制

当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,只有经过measure、layout、draw三个流程才能最终把View绘制出来。performTraversals()依次调用performMeasure()performLayout()performDraw()三个方法,分别完成顶级View的绘制。其中performMeasure()会调用measure()measure()中又调用onMeasure(),实现对其所有子元素的measure过程,这样就完成了一次measure过程;接着子元素会重复父容器的measure过程,如此反复至完成整个View树的遍历(layout和draw同理)。

绘制流程
ViewRootImpl
DecorView
View
performTraversals()
performMeasure()
measure()-onMeasure()
measure()-onMeasure()
performLayout()
layout()-onLayout()
layout()-onLayout()
performDraw()
draw()-onDraw()
draw()-onDraw()

View树的遍历

DevorView
ViewGroup
ViewGroup
View
View
View
View

树的遍历是有序的,由父视图到子视图,每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。

measure 过程由measure(int, int)方法发起,从上到下有序的测量 View,在 measure 过程的最后,每个视图存储了自己的尺寸大小和测量规格。 layout 过程由layout(int, int, int, int)方法发起,也是自上而下进行遍历。在该过程中,每个父视图会根据 measure 过程得到的尺寸来摆放自己的子视图。
measure 过程会为一个 View 及所有子节点的 mMeasuredWidthmMeasuredHeight 变量赋值,该值可以通过 getMeasuredWidth()getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于 measure 得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次 measure。比如,父视图可以先根据未给定的 dimension 去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。

measure过程中传递尺寸的相关类

  • ViewGroup.LayoutParams (View 自身的布局参数)

    • 这个类我们很常见,就是用来指定视图的高度和宽度等参数。对于每个视图的 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 的尺寸,只能由子控件自己根据需求去计算自己的尺寸,这种模式就是我们自定义视图需要实现测量逻辑的情况。

measure的核心方法

  • measure(int widthMeasureSpec, int heightMeasureSpec)

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

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)

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

  • setMeasuredDimension()

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


有点兄弟估计对MeasureSpec还不熟悉,到底这是个什么玩意,且看下文:

MeasureSpec是一个32位的int值,前2位表示SpecMode测量模式,后30位表示SpecSize测量大小;在一个View控件的measure过程中,系统会将这个View的LayoutParams结合父容器的MeasureSpec生成一个MeasureSpec,这个MeasureSpec即规定好了如何去测量这个View的规格大小。MeasureSpec与LyaoutParams的关系即:ChildMeasureSpec = ParentMeasureSpec + ChildLayoutParams

SpecMode的三种测量模式:

  • UNSPECIFIED:不确定模式,父控件不会对子控件有任何约束;
  • EXACTLY:精确模式,父容器知道View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值;对应于LyaoutParams中的match_parent或具体数值。
  • AT_MOST:最多模式,父容器指定了一个可用的大小SpecSize,View的大小不能超过这个值,View的最终大小要看View的具体实现;对应于LyaoutParams中的wrap_content。

言归正传,我们继续来讨论View的绘制。上面说到了measure方法,下面我们继续来看看measure的过程。

measure过程分为两种情况:
第一种情况是只有一个View,那么直接通过measure()方法完成测量;
第二情况是ViewGroup,除了完成自身的测量之外,还要遍历去调用子元素的measure()方法。

View的measure

View的测量过程首先会调用View的measure()方法,而measure()方法又会调用onMeasure()方法实现具体的测量。onMeasure()
主要通过setMeasuredDimension()方法来设置View宽高的测量值(这里的测量值并不是最终的宽高大小,最终大小要在layout阶段确定的,不过一般来说View的测量大小和最终大小是一致的)。
而View的实际测量宽高是通过getDefaultSize()方法来获取的(返回值实际上就是View的SpecSize),该方法的主要逻辑是:根据传进来的View的MeasureSpec,获取对应的SpecMode值和SpecSize值,并判断SpecMode三种测量模式下对应的View的SpecSize的取值。在这里主要关注EXACTLYAT_MOST两种模式,这两种模式下都是直接返回View的SpecSize值,这个SpecSize就是View的测量宽高大小。
如果是getDefaultSize()方法里面是UNSPECIFIED测量模式的话,则会使用getSuggestedMinimumWidth()getSuggestedMinimumHeight()提供的默认大小,那么默认大小是多少呢?通过getSuggestedMinimumWidth()方法可以看到:如果View没有设置背景,那么View的测量宽度等于XML布局文件中android:minWidth属性指定的值,如果没有指定则默认为0;如果View设置了背景,那么View的测量宽度等于android:minWidth属性指定的值与背景图Drawable的原始宽度(若无原始宽度则默认为0)两者中的最大值。

alt view的measure过程

ViewGroup的measure

ViewGroup的测量过程除了完成自身的测量之外,还会遍历去调用子View的measure()方法。ViewGroup是一个抽象类,没有重写View的onMeasure()方法,所以需要子类去实现onMeasure()方法规定具体的测量规则。ViewGroup子类复写onMeasure()方法一般有如下三个步骤:
1、遍历所有子View并测量其宽高,直接调用ViewGroup的measureChildren()方法;
2、合并计算所有子View测量的宽高,最终得到父View的实际测量宽高;
3、存储父View实际测量宽高值;
ViewGroup中提供了measureChildren()方法,该方法主要遍历所有的子View并调用其measureChild()方法,measureChild()主要的逻辑是:

取出子View的LayoutParams参数,结合传进来的父View的MeasureSpec参数,通过getChildMeasureSpec()来计算并创建子View的MeasureSpec,而getChildMeasureSpec()方法主要获取父View测量规格中的SpecMode值和SpecSize值,并根据三种SpecMode模式结合子View的LayoutParams参数计算出子View的SpecMode值和SpecSize值,并通过makeMeasureSpec()方法创建对应的MeasureSpec测量规格,然后再把MeasureSpec传递给子View的measure()方法进行测量。如此递归下去遍历所有的子View并测量子View的宽高从而得出ViewGroup的实际测量大小。

alt ViewGroup的measure过程
下面我们取 ViewGroup 的 measureChildren(int widthMeasureSpec, int heightMeasureSpec) 方法对复合 View 的 Measure 流程做一个源码分析:

    /**
     * 请求所有子 View 去 measure 自己,要考虑的部分有对子 View 的测绘要求 MeasureSpec 以及其自身的 padding
     * 这里跳过所有为 GONE 状态的子 View,最繁重的工作是在 getChildMeasureSpec 方法中处理的
     *
     * @param widthMeasureSpec  对该 View 的 width 测绘要求
     * @param heightMeasureSpec 对该 View 的 height 测绘要求
     */
    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];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();//获取 Child 的 LayoutParams

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

   /** 
     * 该方法是 measureChildren 中最繁重的部分,为每一个 ChildView 计算出自己的 MeasureSpec。
     * 目标是将 ChildView 的 MeasureSpec 和 LayoutParams 结合起来去得到一个最合适的结果。
     *
     * @param spec 对该 View 的测绘要求
     * @param padding 当前 View 在当前唯独上的 paddingand,也有可能含有 margins
     *
     * @param childDimension 在当前维度上(height 或 width)的具体指
     * @return 子视图的 MeasureSpec 
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

            .........

        // 根据获取到的子视图的测量要求和大小创建子视图的 MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
    }

   /**
     *
     * 用于获取 View 最终的大小,父视图提供了宽、高的约束信息
     * 一个 View 的真正的测量工作是在 onMeasure(int, int) 中,由该方法调用。
     * 因此,只有 onMeasure(int, int) 可以而且必须被子类复写
     *
     * @param widthMeasureSpec 在水平方向上,父视图指定的的 Measure 要求
     * @param heightMeasureSpec 在竖直方向上,控件上父视图指定的 Measure 要求
     *
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
      ...

      onMeasure(widthMeasureSpec, heightMeasureSpec);

      ...
    }

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

Layout相关概念和核心方法

首先要明确的是,子视图的具体位置都是相对于父视图而言的。
View 的 onLayout 方法为空实现,而 ViewGroup 的 onLayout ()为 abstract 的,因此,如果自定义的 View 要继承 ViewGroup 时,必须实现 onLayout() 函数。
在 layout 过程中,子视图会调用getMeasuredWidth()getMeasuredHeight()方法获取到 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight,作为自己的 width 和 height。然后调用每一个子视图的layout(l, t, r, b)函数,来确定每个子视图在父视图中的位置。
View的Layout过程主要是确定View的四个顶点位置,从而确定其在容器中的位置,具体的layout过程和measure过程大致相似。

View的layout过程:

alt View的layout过程

ViewGroup的layout过程:

alt ViewGroup的layout过程
下面我们取LinearLayout的layout做源码分析:

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

    /**
     * 遍历所有的子 View,为其设置相对父视图的坐标
     */
    void layoutVertical(int left, int top, int right, int bottom) {
    for (int i = 0; i < count; i++) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    childTop += measureNullChild(i);
                } else if (child.getVisibility() != GONE) {//不需要立即展示的 View 设置为 GONE 可加快绘制
                    final int childWidth = child.getMeasuredWidth();//measure 过程确定的 Width
                    final int childHeight = child.getMeasuredHeight();//measure 过程确定的 height

                    ...确定 childLeft、childTop 的值

                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
                }
            }
    }

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }    

    View.java
    public void layout(int l, int t, int r, int b) {
        ...
        setFrame(l, t, r, b)
    }

    /**
     * 为该子 View 设置相对其父视图上的坐标
     */
     protected boolean setFrame(int left, int top, int right, int bottom) {
         ...
     }

Draw的相关概念和核心方法

Draw过程主要是绘制View的过程,也分为单一View的绘制和ViewGroup的绘制。
View的draw过程都是从调用draw()方法开始的,该方法主要完成如下工作流程:
(1) drawBackground():绘制背景;
(2) 保存当前的canvas层(不是必须的);
(3)onDraw():绘制View的内容,这是一个空实现,需要子View根据要绘制的颜色、线条等样式去具体实现,所以要在子View里重写该方法;
(4)dispatchDraw():对所有子View进行绘制;单一View的dispatchDraw()方法是一个空方法,因为单一View没有子View,不需要实现dispatchDraw ()方法,而ViewGroup就不一样了,它实现了dispatchDraw()方法去遍历所有子View进行绘制;
(5)onDrawForeground():绘制装饰,比如滚动条;

View的Draw过程:

alt View的Draw过程
View.draw(Canvas) 源码分析:

/**
     * Manually render this view (and all of its children) to the given Canvas.
     * The view must have already done a full layout before this function is
     * called.  When implementing a view, implement
     * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
     * If you do need to override this method, call the superclass version.
     *
     * @param canvas The Canvas to which the View is rendered.  
     *
     * 根据给定的 Canvas 自动渲染 View(包括其所有子 View)。在调用该方法之前必须要完成 layout。当你自定义 view 的时候,
     * 应该去是实现 onDraw(Canvas) 方法,而不是 draw(canvas) 方法。如果你确实需要复写该方法,请记得先调用父类的方法。
     */
    public void draw(Canvas canvas) {

        / * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background if need
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children (dispatchDraw)
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

     // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

         // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

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

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // we're done...
            return;
        }

        // Step 2, save the canvas' layers
        ...

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

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

*由上面的处理过程,我们也可以得出一些优化的小技巧:当不需要绘制 Layer 的时候第二步和第五步会跳过。因此在绘制的时候,能省的 layer 尽可省,可以提高绘制效率。

ViewGroup的Draw过程:

alt ViewGroup的Draw过程
ViewGroup.dispatchDraw() 源码分析:

dispatchDraw(Canvas canvas){

...

 if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {//处理 ChildView 的动画
     final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {//只绘制 Visible 状态的布局,因此可以通过延时加载来提高效率
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);// 添加布局变化的动画
                    bindLayoutAnimation(child);//为 Child 绑定动画
                    if (cache) {
                        child.setDrawingCacheEnabled(true);
                        if (buildCache) {
                            child.buildDrawingCache(true);
                        }
                    }
                }
            }

     final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }

    controller.start();// 启动 View 的动画
}

 // 绘制 ChildView
 for (int i = 0; i < childrenCount; i++) {
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }

...

}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
}

/**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     * This draw() method is an implementation detail and is not intended to be overridden or
     * to be called from anywhere else other than ViewGroup.drawChild().
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
    }

  • drawChild(canvas, this, drawingTime)
    直接调用了 View 的child.draw(canvas, this,drawingTime)方法,文档中也说明了,除了被ViewGroup.drawChild()方法外,你不应该在其它任何地方去复写或调用该方法,它属于 ViewGroup。而View.draw(Canvas)方法是我们自定义控件中可以复写的方法,具体可以参考上述对view.draw(Canvas)的说明。从参数中可以看到,child.draw(canvas, this, drawingTime)肯定是处理了和父视图相关的逻辑,但 View 的最终绘制,还是 View.draw(Canvas)方法。
  • invalidate()
    请求重绘 View 树,即 draw 过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些调用了invalidate()方法的 View。不会经过measure和layout过程,只会调用draw过程
  • requestLayout()
    当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法,在自定义的视图中,如果某些情况下希望重新测量尺寸大小,应该手动去调用该方法,它会触发measure()和layout()过程,但不会进行 draw

参考资料:

how-android-draws
https://www.jianshu.com/p/1dab927b2f36
https://www.jianshu.com/p/a5ea8174d912

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值