Android中view的显示原理之view的绘制流程

android开发中了解view的绘制流程至关重要,尤其自定义View,需要重写onMeasure,onLayout,onDraw等方法,那么view的绘制流程到底是怎么样的呢?前一篇文章(Android中view的显示原理之DecorView是如何被添加至Window中以及view绘制流程开始的地方)分析了View绘制的入口是在ViewRootImpl中的performTraversals()方法中,会依次调用performMeasure —> performLayou —>performDraw,那么今天我们继续分析view的整个绘制流程。
Android中view的显示原理之view的绘制流程_第1张图片

View绘制入口

View绘制入口是在performTraversals(),核心代码如下:

private void performTraversals() {
		***
		int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//测量
         ***
		performLayout(lp, mWidth, mHeight)//布局
		***
		performDraw();//绘制
		
}

可以看到在调用测量方法前先得到了childWidthMeasureSpec 、childHeightMeasureSpec ,通过方法名大概可以看出是子view的宽度和高度的测量规范,那么通过这个方法是怎么得到子view的测量规范的呢?这里不着急往下走了,先来介绍一个特殊的类MeasureSpec。

MeasureSpec

这个类看类名意思是测量规范,它就决定了View的测量规格。View在测量的时候会涉及到两个概念:一个是模式(SpecMode),一个是尺寸(SpecSize)。这两都被封装进了MeasureSpec中,并通过一个32位的int数值表示,其中前2位表示模式,后30位表示尺寸。通过SpecMode+SpecSize就构成了View的测量规格(即在某种模式下当view的大小)。

三种测量模式

		private static final int MODE_SHIFT = 30;
		***
         //如果父容器没有对子view有任何约束,那么它可以是任意大小 
         // UNSPECIFIED 模式一般系统内部使用
         //对应的二进制为:00000000000000000000000000000000
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

         //父容器检测出子view的大小,子view的大小就是其想要的大小
         //对应的二进制为:01000000000000000000000000000000
        public static final int EXACTLY     = 1 << MODE_SHIFT;

         //子view可以任意大小,但是不能超过父容器
         //对应二进制为:10000000000000000000000000000000
        public static final int AT_MOST     = 2 << MODE_SHIFT;
  • UNSPECIFIED
    UNSPECIFIED模式表示父容器没有对子view做任何限制,子view大小可以是任何尺寸,该模式一般是系统内部使用,实际开发中很少用到。

  • EXACTLY
    EXACTLY 模式表示子view的大小为精确值,子view的宽高布局属性值为LayoutPamras match_parent或者固定数值,子view的大小就是SpecSize。

  • AT_MOST
    AT_MOST模式表示当前子view的大小不能超过父容器的指定的大小,子view的宽高布局属性是LayoutPamras wrap_content,子view的大小可以是不超过父容器指定的大小的任意值。
    那么这些模式又是怎么确定的呢?相信童鞋肯定有这样的疑问。那么我们来分析分析源码,让你彻底理解。
    在调用 performMeasure方法前会先调用getRootMeasureSpec方法生成对应的宽高测量规格。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        //这里传入的第一个参数是windowSize也就是当前窗口宽或者高的大小,第二个参数传入的是一个int类型的数据,是当前子view 的宽或者高布局属性。
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
 			//如果当前view的宽或者高的布局属性是MATCH_PARENT,也就是填充父容器,
 			那么它的模式就是EXACTLY,它的大小就是父容器的大小。
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            //如果当前view的宽或者高的布局属性是WRAP_CONTENT,也就是包裹内容,
            那么它的模式就是AT_MOST,它的大小最大就是父容器的大小。
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            //这里就表示当前view的宽或者高的布局属性是一个确定的值,它的模式就是EXACTLY,大小就是确定值。
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

getRootMeasureSpec方法分别得到了view的宽和高的测量规格,那么这个view是哪个View呢?通过源码可知mWidth、mHeight分别对应窗口的宽和高,lp.width、lp.height分别对应DecorView的宽和高的布局属性。那么通过MeasureSpec.makeMeasureSpec就得到了DecorView宽和高的测量规格,其中makeMeasureSpec方法就是将SpecMode与SpecSize打包成一个32位的int数值,前2位表示测量模式,后30为表示测量尺寸。

测量(measure)

接下来我们就进入期待已久的 performMeasure。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;`在这里插入代码片`
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

这里调用了mView的measure方法,有木有很激动,这就是大家经常说的measure方法。那么这个mView是什么呢?我们来看看。

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ***
                }
          }
   }

它在setView中被赋值,在之前分析view绘制流程入口的时候,这个方法传进来的DecorView的实例,这里就将其看作DecorView。我们知道DecorView的数据类型是FrameLayout,先接着看 mView.measure做了什么。

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

这里进入了View的measure方法,调用了onMeasure。突然一下似乎明白了点什么,自定义View时经常需要重写的onMeasure方法出现了!DecorView是FrameLayout的子类,那么去看下FrameLayout中的onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int count = getChildCount();
	***
	//遍历子view
	for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {//测量子view的条件
            	//开始测量子view  传入的是父控件宽高的测量规格
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
              ***
            }
        }
        //这个方法很关键,讲完子view的测量,就会豁然明朗了
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
}

在FrameLayout的onMeasure中遍历了当前容器的子view,并逐个测量。在其他容器中(如:RelativeLayout,LinearLayout等)其测量流程都大致一样,先测量子view,再测量自身大小,这里就拿FrameLayout来说明测量大致流程。

//调用了ViewGroup的measureChildWithMargins方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
         //获取子view的布局属性对象
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //获取子view的宽度测量规格
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
         //获取子view的高度测量规格
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
		//开始子view的测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述代码通过传入的父容器的测量规格和子view的布局属性获取子view的测量规格,并开始子view的测量,子view的测量规格是怎么得到的呢?接着看。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
		//通过MeasureSpec获取到父容器的SpecMode和SpecSize
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
		
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        //如果父容器的测量模式是EXACTLY
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {//如果子view的布局属性值大于0,证明其大小为确定的某个值,其测量模式为EXACTLY。
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//如果子view的布局属性值为MATCH_PARENT,父容器测量模式为EXACTLY(代表着父容器的大小是确定的),那么填充父容器其大小也是确定的。
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果子view的布局属性值为WRAP_CONTENT,那么其大小不能超过父容器的大小。
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        //如果父容器的测量模式是AT_MOST
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {如果子view的布局属性值大于0,证明其大小为确定的某个值,其测量模式为EXACTLY。
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {如果子view的布局属性值为MATCH_PARENT,父容器测量模式为MATCH_PARENT(代表着父容器的大小是不确定的),要求子view的大小不能超过父容器。
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果子view的布局属性值为WRAP_CONTENT,那么其大小不能超过父容器的大小。
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        //如果父容器的测量模式是UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {//如果子view的布局属性值大于0,证明其大小为确定的某个值,其测量模式为EXACTLY。
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//子view的布局属性是MATCH_PARENT,但是父容器的大小是不可知的,所以子view的大小也是不可知的。
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子view的布局属性是包裹内容,但是其大小是不可知的,有图父容器的大小是不可知的,所以其测量模式只能是UNSPECIFIED。
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        //最后调用了MeasureSpec.makeMeasureSpec,将子view的SpecSize和SpecMode打包,生成对应的测量规格
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上述代码诠释了子view的测量规格是由父容器的测量规格和子view的布局属性共同决定的,View的测量规格有别于顶层布局视图DecorView的测量规格,这里我们总结一下View的测量规格。
Android中view的显示原理之view的绘制流程_第2张图片
回到measureChildWithMargins方法,在生成了子view的测量规格后,调用了子view的measure(childWidthMeasureSpec, childHeightMeasureSpec)。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	***
	//子view的测量又会进入到View的onMeasure方法中
	onMeasure(widthMeasureSpec, heightMeasureSpec);
	***

}

调用了View的onMeasure方法,接着走下去。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		//这里调用了setMeasuredDimension方法
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

走到这里真是万分激动啊!自定义View时重写onMeasure方法是不是会调用一个setMeasuredDimension方法!对!就是它啊!它到底做了些什么操作呢?

//在调用setMeasuredDimension前先调用了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 = specSize;
            break;
        }
        return result;
    }

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
       	***
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

这里调用了setMeasuredDimensionRaw(measuredWidth, measuredHeight)。

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

对测量后的结果进行了保存,并对对应的标志位进行了设置,然后就完了。测量就这么结束了?前面在FrameLayout的onMeasure中提到过setMeasuredDimension方法,这里就可以解释了。在测量完子view后计算总的宽度和高度(也就是测量后的宽高),再调用setMeasuredDimension对值进行保存。

布局(draw)

测量完结后会对当前view进行布局,接着看看performLayout(lp, mWidth, mHeight)。

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
		***
		final View host = mView;
		host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
		***
}

这里将mView(上面分析了是DecorView)赋值给host,并调用了host的layout,传入测量后的宽高。

		//来到View的 layout方法
	 *@param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     * 注释解释了各个参数额度含义,分对应左,上,右,下相对父容器的位置
public void layout(int l, int t, int r, int b) {
	***
	setFrame(l, t, r, b);//确定自身相对父容器的位置
	onLayout(changed, l, t, r, b);//这个方法不是在自定义View时(继承ViewGroup时,必须重写的方法吗!),
	***
}

看看setFrame做了什么。

protected boolean setFrame(int left, int top, int right, int bottom) {
	***
	mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
   	mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
	***
}

对当前view相对于父容器的位置进行了保存并设置。
在View的layout方法中还调用了onLayout(changed, l, t, r, b)方法,进去一探究竟。

/**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
     //注释的大意是:带有子类的派生类应该重写该方法,并调用子类的layout方法
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

这里onLayout没有实现是因为在View中只需要确定自身View当前在父容器中的位置,而如果是在ViewGroup中,则需要重写该方法。在FrameLayout中看看onLayout方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }


void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
				//调用了子view的layout方法
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

在ViewGroup中会遍历子view,确定每个子view在父容器中的位置。

绘制(draw)

前面的测量和布局只是确定了View的大小和其在父容器中的位置,绘制就是将当前视图渲染到可见屏幕上,具体是怎么绘制的呢?下面为你揭晓。

private void performDraw() {
	***
	draw(fullRedrawNeeded)
	***
}

private boolean draw(boolean fullRedrawNeeded) {
	***
	drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)
	***
}

performDraw调用了draw(fullRedrawNeeded),draw(fullRedrawNeeded)调用了drawSoftware(surface, mAttachInfo, xOffset, yOffset,scalingRequired, dirty, surfaceInsets)方法。

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
		***
		 mView.draw(canvas);//这里mView分析的是DecorView,这里只是分析View的绘制(mView不一定是DecorView,这里只是拿DecorView来分析)
		***
}

上述 drawSoftware中调用了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.
     */
     上述注释大意是:在指定的画布上渲染当前View及其子View,在执行渲染前当前View
     必须已经完成了完整的布局。当实现一个View时,要重写onDraw方法而不是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 //绘制View的内容
         *      4. Draw children //绘制子View
         *      5. If necessary, draw the fading edges and restore layers //如果有必要,绘制渐暗边缘以及图层的恢复
         *      6. Draw decorations (scrollbars for instance)//绘制装饰(例如滚动条)
         */
         
		 // Step 1, draw the background, if needed
		drawBackground(canvas);
		
		// skip step 2 & 5 if possible (common case)
			***
		// Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
		
		// Step 4, draw the children
        dispatchDraw(canvas);
		
		// Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
		
		// Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);
	}

通过查看源码得知View的绘制分为七个步骤:

  1. drawBackground绘制背景。
  2. 如果有必要对画布图层进行保存。
  3. onDraw绘制View的内容,这个方法在自定义View时用的特别多,因为该方法是个空实现,所以具体内容的绘制需要根据个人需求进行绘制。
  4. dispatchDraw绘制子View,该方法也是一个空实现,一般ViewGroup需要重写该方法,对内部子View进行绘制。
  5. 如果有必要绘制视图边缘以及恢复图层。
  6. onDrawForeground绘制前景,例如滚动条。
  7. drawDefaultFocusHighlight绘制默认焦点高亮。

其中onDraw方法,在不同的View中绘制需求各有不同,绘制出来的内容当然也是多种多样,在自定义View(继承View)时必须实现该方法。
dispatchDraw是对子View进行绘制,这里我们看看ViewGroup中dispatchDraw的实现是怎样的呢?

@Override
    protected void dispatchDraw(Canvas canvas) {
		***
		for (int i = 0; i < childrenCount; i++) {
				***
				drawChild(canvas, transientChild, drawingTime);
			}
		***
	}

在ViewGroup的dispatchDraw方法中调用了drawChild,顾名思义,对子View进行绘制。

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

drawChild又调用了View的draw方法,这里再给出一张draw流程的图。
Android中view的显示原理之view的绘制流程_第3张图片
至此,整个View的绘制流程就结束了。各位童鞋你明白了吗?
由于个人水平有限,文章难免出现错误,如有错误,欢迎留言,一定及时修正。

你可能感兴趣的:(android基础知识)