Android中View的绘制机制

在生活中,我们绘制一幅图,想象一下我们的绘制流程是怎么样的?

  1. 我要确定我绘制的这幅图大概有多大
  2. 我要确定我绘制的这幅图在什么位置
  3. 我要怎么利用画笔把这幅图绘制出来

在Android中我们绘制一个View通常也是这样的流程

  1. 先确定View的大小(measure)
  2. 再确定View的位置(layout)
  3. 最后确定View的样子(draw)

在Android中View视图结构自上而下是树形结构,ViewGroup->…->View
最顶层的View是DecorView(它继承自FrameLayout是一个ViewGroup)
Android控件树
所以View的绘制起点就是DecorView开始,自上而下依次遍历它的子View,完成测量、布局、绘制流程

ViewRootImp#performMeasure->ViewGroup#measure->…->View#measure

ViewRootImp#performLayout->ViewGroup#layout->…->View#layout

ViewRootImp#performDraw->ViewGroup#draw->…->View#draw

1. 测量流程

测量流程的目的就是决定自身的大小,先看下View#measure方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec){
	...// 省略若干
	onMeasure(widthMeasureSpec,heightMeasureSpec);
}

View#measure方法是一个final方法,不可以被重写,可以把它理解为一个模版方法,里面做了一些基础操作,然后公开了一个onMeasure方法给我们,在onMeasure方法中,我们可以实现自身的测量,那我们重点关注下onMeasure方法,先理解它的两个参数widthMeasureSpec和heightMeasureSpec

  • widthMeasureSpec 宽度测量模式

是一个32位的int值,高两位代表测量模式,低30位代表View的尺寸大小
这个参数是谁传来的?前面我们分析过View的测量是一个自上而下的过程ViewGroup->View,所以这个参数是它的直接父容器传递过来的.那么问题又来了,我测量自己,父容器传过来这个参数有啥用?

当我们的View处在一个ViewGroup中的时候,我们的View大小是要受到ViewGroup大小的约束的.你的宽高总不能比你的父容器还要大吧!所以这个参数就是父容器传递给子View的一个约束(或者理解为建议尺寸)

  • heightMeasureSpec 高度测量模式 同上不再介绍

  • MeasureSpec的三种测量模式

  1. MeasureSpec.EXACTLY 精确模式
  2. MeasureSpec.AT_MOST 至多模式,不超过父容器大小
  3. MeasureSpec.UNSPECIFIED 不确定,父容器不对子View的大小做限制,一般用在ScrollView、ListView等中

那么父容器给我的这个建议宽高是不是我想要的呢?接下来我们分析下父容器如何制作这个参数的?

阅读ViewGroup#getChildMeasureSpec

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        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
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.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
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 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) {
                // 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
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 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) {
                // 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
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
  • spec参数:这是ViewGroup的父亲给它的,我们暂时不看,我们只考虑当前这一级,当前的ViewGroup如何制作子View的测量规格的
  • padding参数:这个是父亲需要保留的空间,比如padding、margin (这些空间除外,剩余的才能给子View)
  • childDimension参数:这个是子View的宽高尺寸,比如具体数值、match_parent、wrap_content

阅读以上制作流程可知

  1. 当我们子View是具体的宽高时,父容器建议的宽高就是我们设置的尺寸
  2. 当我们子View时match_parent或者wrap_content时,父容器建议的宽高就是父亲剩余的尺寸

所以

  1. 当我们的View宽高设置的具体的一个大小时,父容器给我们的建议宽高就是我们想要的
  2. 当我们的View宽高设置的match_parent时,父容器给我们的建议宽高也是我们想要的
  3. 当我们的View宽高设置的wrap_parent时,父容器给我们的建议宽高并不是我们想要的

所以当我们的View的宽高需要支持wrap_content时,我们需要自己进行测量,针对View我们可以根据它的内容来得到自己所需要的宽度,针对ViewGroup我们先测量完它的所有子View,然后再得到自己的宽高,注意测量完自身的宽高之后,还要和父容器建议的大小进行比较,不能超过父容器大小哦,这里可以利用View#resolveSize方法进行协调(一个工具方法,不用我们自己进行判断了)得到最终的宽高之后,调用setMeasuredDimension进行设置,之后就可以通过getMeasureWidth和getMeasureHeight获取到测量的宽高了

再分析下,我们不重写onMeasure方法,看看View的默认实现

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

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 int getSuggestedMinimumWidth() {
	// 如果设置了background,取background和minWidth最大值,否则取minWidth
	return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

可以看到当我们子View设置了match_parent和wrap_content时,默认的onMeasure方法测量的大小都是父容器剩余的大小,match_parent为父容器剩余大小,那是合理的,wrap_content为剩余大小,不符合我们预期,所以我们要重写onMeasure自己实现自身的测量

View视图根部DecorView的测量,从前面的分析得知,测量规格都是由上一个父容器传递过来的,那么测量的起点是从DecorView开始的,DecorView的宽度测量规格和高度测量规格是谁传递的呢?阅读ViewRootImpl源码得知在ViewRootImpl#performTraversals方法中

private void performTraversals() {
  	// 省略若干代码...		
	int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
	int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
	performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

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;
}

可以得知,是通过getRootMeasureSpec构造的,其中mWidth和mHeight是当前窗口大小,前面计算得知,lp.width和lp.height
都是match_parent,所以最终得到的MeasureSpec中测量模式是MeasureSpec.EXACTLY,大小为窗口大小

2.布局流程

布局流程的目的是确定自身的位置,我们知道作为子View,它都是需要被添加到ViewGroup容器中的,那么它的位置其实是它的父容器决定的,父容器传递给四个顶点的位置,从而摆放它自己,那么我们来看下View#layout方法

public void layout(int l, int t, int r, int b) {
	setFrame(l, t, r, b);
	onLayout(changed, l, t, r, b);
}
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);
}

可以分析出通过调用setFrame()方法来赋值mLeft、mTop、mRight、mBottom,然后通过mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom)确定自身的摆放位置,mLeft、mTop、mRight、mBottom这几个参数值不去介绍,可以参考View视图坐标系,那么之后就可以getLeft…获取了,同样的这里layout方法我们也不需要修改,这里父类默认实现的一些基础操作,同样的它也暴露了onLayout方法给我们实现,接下来分析下onLayout方法,此方法是用来摆放内部子View的位置的,对于View来说,它内部没有子View了,所以该方法我们一般不需要重写,但是对于ViewGroup,它是用来装载View的,所以必须实现onLayout方法,在内部调用每个子View的layout方法,传入四个顶点位置,然后让子View摆放自己在合适的位置。所以我们经常使用的布局容器如LinearLayout、RelativeLayout方法,都是实现了onLayout方法,定义了子View的摆放规则的。

3.绘制流程

绘制流程的目的是确定自身的样子(长啥样),作为ViewGroup它一般是用来装载子View的,所以它一般不需要绘制什么,在它的内部会遍历所有的子View,调用其draw方法完成子View自身的绘制。想象一下,子View都是放在容器里的,如果容器需要一个背景图案啥的,我们还是需要重写ViewGroup的onDraw方法完成绘制的。这里我们就先不展开讨论了,我们来分析下View#draw方法

public void draw(Canvas canvas) {

         // Step 1, 绘制背景
         drawBackground(canvas);

         // Step 2, 绘制主体内容
         onDraw(canvas);

         // Step 3, 绘制子View
         dispatchDraw(canvas);

         // Step 4, 绘制滑动边缘渐变和滑动条
         	...

         // Step 5, 绘制前景
         onDrawForeground(canvas);
}

从draw方法中可以看出来一个View的绘制顺序,理解绘制顺序非常重要,因为我们最终展示的视图,它就是由一层一层的图形叠加来的,这几个方法具体逻辑可以查看源码,我们主要分析下Step 2和Step 3, Step 2是调用onDraw绘制自身,我们看下这个onDraw方法

protected void onDraw(Canvas canvas) {

}

它是一个空实现,所以我们需要实现它,在其内部完成自己的绘制逻辑
接着再分析dispatchDraw()方法

protected void dispatchDraw(Canvas canvas) {

}

它内部也是一个空实现,因为对于View来说,它内部没有子View了,它只要负责绘制自己就行了,但是ViewGroup必须实现它,在其内部遍历子View调用其draw方法完成子View的绘制,这里可以查看源码ViewGroup的dispatchDraw实现

具体谈谈如何绘制?

我们知道生活中,我们要绘制一个图形,我总得有个坐标系吧,比如小时候在黑板画画,黑板就是坐标系,我们可以画在黑板的任意位置,同样的绘制View也是这个道理,首先我们得找到绘制的坐标系,其实View就是一块矩形区域,View绘制的坐标系原点就是View的左上角,原点向右的方向为X轴正方向,原点向下的方向为Y轴正方向。知道了坐标原点和坐标系,我们就可以进行绘制各种各样的图形了

绘制相关的API

Canvas#XXX 画布,其内部包含有很多API,比如绘制线、点、矩形、等…我们的图形就是绘制在画布上。

Paint 画笔

具体详细的用法可以查看API手册,这里不详细介绍了

画布的操作(裁剪、位置变化)

clipRect 裁剪一块区域,我们绘制的图形只在这一个范围内可见
translate、rotate、scale 位置变换,相当于改变坐标系的位置

save 保存当前的画布设置
restore 恢复之前的画布设置
可以理解save-restore 为一个绘制图层

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值