Android View的绘制流程知识点总结

Android View的绘制流程知识点总结

前言

本文属于《Android开发艺术探索》(以下简称“艺术探索”)第四章——View的工作原理读书笔记。

同样,还是博客园那位博主的文章:Android View绘制13问13答。他的 Andoird N问N答 系列文章总结的非常好。同时,在第三章读书笔记中提到的CSDN博主废墟的树,这篇从ViewRootImpl类分析View绘制的流程,把绘制的三大流程讲的非常清楚。感谢前辈们的分享。

1.绘制流程从哪里开始

以Activity为例,下图是相关方法的调用流程:



可以看到,最终performTraversals()方法触发了View的绘制。该方法内部,依次调用了performMeasure(),performLayout(),performDraw(),将View的measure,layout,draw过程,从顶层View分发了下去。

上面体现了Activity中View的绘制过程是如何被触发的,其实通过阅读《艺术探索》第8章——理解Window和WindowManager,可以知道,Dialog,PopupWindow中View的绘制过程也是一样的,只是触发的方式不同。例如Dialog中,是调用dialog.show()时,触发了WindowManagerImpl的addView()(上图步骤2),后面的流程就一样了。

2.绘制流程第一步——measure

什么是MeasureSpec

MeasureSpec是一个specSize和specMode信息的32位int值,其中高两位表示specMode,低30位表示specSize。specMode指测量模式,specSize指在某种测量模式下的规格大小。specMode包括:

  • UNSPECIFIED
  • EXACTLY
  • AT_MOST

View需要MeasureSpec信息,来确定自己能显示多大。顶层View的MeasureSpec由ViewRootImpl#getRootMeasureSpec()方法获得,子View的MeasureSpec,由父容器和其自身的LayoutParams共同决定,通过ViewGroup提供的getChildMeasureSpec()方法获得。

getRootMeasureSpec()源码如下:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_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;
    }

getChildMeasureSpec()方法较长,《艺术探索》中的一图以表格形式呈现了该方法:



View的measure过程

父容器调用子View的measure方法把上一步获得的MeasureSpec信息传递过去,子View的measure方法调用View#onMeasure(),onMeasure调用setMeasuredDimension()设置自身大小。该过程如图:

getSuggestedMinimumHeight(),getSuggestedMinimumWidth():

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

作用一目了然。getDefaultSize()方法也很简单:

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 = specSize;
            break;
        }
        return result;
    }

由前面的分析可知,当View的宽高属性为wrap_content时,其父View通过getChildMeasureSpec方法确定的其测量模式为AT_MOST。再由getDefaultSize()可知,其最终的宽高会被设置为specSize,即父View所剩空间的大小,这也就是为什么自定义View不对AT_MOST模式做处理,其wrap_content和match_parent效果一样。

ViewGroup的measure过程

ViewGroup的measure()和onMeasure()方法是从View继承过来的,没有做任何重写(measure方法是final限定,不能重写)。其onMeasure()方法由各个子类各自重写,实现自己的需求。ViewGroup的子类在onMeasure()中做的事其实都差不多:

  1. 遍历子View
  2. 调用measureChild*让子View确定自己的大小
  3. 根据所有子View的大小确定自己的大小

2步骤中*是个通配符,意思是measureChild一类的方法,如measureChildHorizontal,measureChildWithMargins。这些方法内部会调用getChildMeasureSpec确定子View的测量模式,会调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)触发子View的测量。

3.绘制流程第二步——layout

View源码中,layout方法中会先调用setFrame给自身的left,top,right,bottom属性赋值,至此自己在父View中的位置就确定了。然后会调用onLayout方法,该方法在View中是一个空实现,具体的实现由其子View(一般指ViewGroup)实现,如LinearLayout。

4.绘制流程第三步——draw

绘制流程经过ViewRootImpl中的performMeasure(),performLayout(),现在到了perfromDraw()。performDraw经过如下图所示的方法调用,触发了顶层View的draw方法:


drawSoftware()方法内部调用了mView.draw(canvas),mView就是“1.绘制流程从哪里开始”第4步setView()所设置的根View。View的draw方法源码中的注释具有参考价值,如下:

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * 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
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

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

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

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

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

可以看到一般情况下View的draw流程分为四步:

  1. 绘制背景
  2. 绘制自身内容(onDraw)
  3. 遍历子View,调用其draw方法把绘制过程分发下去(dispatchDraw)
  4. 绘制装饰(onDrawForeground)

其中步骤二在自定义View的时候经常需要去实现,以绘制自己想要的效果。步骤三dispatchDraw在View中是个空实现。ViewGroup实现了dispatchDraw(),其中调用了ViewGroup#drawChild方法,而drawChild()仅仅是调用了child.draw():

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

自定义View

自定义View的几种方式

1.继承自View,重写onDraw方法

场景:显示的内容需要高度定制,如图表

2.继承自ViewGroup

场景:流式布局等,对于内容布局由特殊要求的。通过重写onLayout方法达到目的。

3.继承自特定的View(如TextView,ImageView)

场景:需要扩展系统控件功能,如圆角ImageView

4.继承自特定的ViewGroup(如LinearLayout)

场景:不需要自定义布局方式,只需要将常用的View组合起来,比如app设置界面,常常每一个item都是左一个图标,中间一行文字,右边一个箭头,这是就可以继承自LinearLayout,默认水平方向布局,暴露出setIcon,setText等自定义的方法设置图标,文字即可。

自定义View注意事项

  1. 让View支持wrap_conent
  2. 让View支持padding
  3. 尽量避免使用Handler,一般都可以用View自带的post方法代替
  4. 在onDeatchFromWindow时,停止View的动画或线程(如果有的话)
  5. 如果存在嵌套滑动,处理好滑动冲突

总结

没有总结。。以上只是看完《艺术探索》用自己的理解写读书笔记加强记忆而已。各路大神总结的很清楚,需要时查阅他们博客就行了,比如要点提炼|开发艺术之View

发布于 2018-09-20