自由笔记-AndroidView模块之View绘制流程分析

View绘制流程:

起始点为ViewRootImp的performTraversals方法。在该方法中调动这3个方法来触发以下3个流程

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

performDraw();

 

一、 measure()过程

主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth)

每个View的控件的实际宽高都是由父视图和本身视图决定的。

ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法.

如果一个View是ViewGroup,它还有子View,那么它会调用measureChildWithMargins()去测量子View。在该方法里会调用

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

 

在View的onMeasure()方法里面进行实际测量并设定View的宽高,通过调用 setMeasuredDimension(h , l)方法设定

 

简单来说,在最顶层的ViewRoot调用measure方法,然后会触发回调onMeasure方法,在onMeasure方法中又会触发调用子View的measure方法来一层层的往下测量。

 

measure—>onMeasure->measure......

 

二、layout布局过程

 

该过程和measure过程一样,也是从ViewRoot发起,逐层下发,通过layout触发onLayout方法,在onLayout方法里面触发子控件的layout方法。

1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法

(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;

2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。

只有ViewGroup需要重写Onlayout方法,去实现它里面的子控件的摆放

 

layout->onLayout->layout......

 

三、draw绘制流程

该过程和measure过程也类似。从ViewRoot的draw方法开始,逐层递归往下,通过draw方法触发onDraw方法,但是对于ViewGroup有点区别,draw方法主要流程如下:

1、绘制该View的背景

2、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)

3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)

4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)

值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类

函数实现具体的功能。

5、dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个

地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能

实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。

6、绘制滚动条,前台等

 

draw->drawBackground->onDraw->dispatchDraw->onDrawForeground

在ViewGroup中,系统已经为我们实现好了dispatchDraw方法,它会判断哪些子控件需要重新绘制,在该方法中调用drawChild然后在调用子控件的draw方法,进入到以这个子控件为root的绘制流程

我们需要重写onDraw方法来完成我们控件的绘制。

 

 

 

四、触发重新绘制的三个函数:invalidate(),requsetLaytout()以及requestFocus(),这三个函数最终会调用到ViewRoot中的schedulTraversale()方法,该函数然后发起一个异步消息,消息处理中调用

performTraverser()方法对整个View进行遍历。

performTraverser方法中分别调用performMeasure,performLayout,performDraw

 

invalidate:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”

视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。

一般引起invalidate()操作的函数如下:

1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。

2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。

3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。

4、setEnabled()方法 :请求重新draw(),但不会重新绘制任何视图包括该调用者本身。

 

requestLayout:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。

 

requestFocus:请求View树的draw()过程,但只绘制“需要重绘”的视图。

 

View的测量过程:

1、MeasureSpec :测量规格类,其中高2位代表测量模式,低30位代表大小,其中,测量模式有以下三种

private static final int MODE_SHIFT = 30;

private static final int MODE_MASK = 0x3 << MODE_SHIFT;

 

/**

* Measure specification mode: The parent has not imposed any constraint

* on the child. It can be whatever size it wants.

*/

public static final int UNSPECIFIED = 0 << MODE_SHIFT; //父容器不对View有任何限制,要多大给多大,一般用于系统内部。

 

/**

* Measure specification mode: The parent has determined an exact size

* for the child. The child is going to be given those bounds regardless

* of how big it wants to be.

*/

public static final int EXACTLY = 1 << MODE_SHIFT;//精准模式,父容器检查出子View的精确大小,一般对应于指定dp或者match_parent(当父容器的测量模式为EXACTLY的时候)

 

/**

* Measure specification mode: The child can be as large as it wants up

* to the specified size.

*/

public static final int AT_MOST = 2 << MODE_SHIFT;//最大空间模式,父容器指定了一个大小SpecSize,子View不能够超过这个大小,具体是多少看子View的实现,一般对应于wrap_content

 

 

2、MeasureSpec和LayoutParams的关系

首先是根View DecorView的MeasureSpec的创建过程,参看ViewRootImp的measureHierarchy方法

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,

final Resources res, final int desiredWindowWidth, final int desiredWindowHeight);

这里面host是DecorView,lp是window的layoutParams,desiredWindowWidth和desiredWindowHeight是屏幕的宽高。

 

接着我们看getRootMeasureSpec这个方法,它在measureHierarchy方法里面执行:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);其中这个lp.width是布局的宽高,他有以下三个值

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;

}

从代码可以看出,DecorView的测量规格,根据width的取值,按照以下规则确定:

ViewGroup.LayoutParams.MATCH_PARENT:精准模式,就是屏幕的宽高

ViewGroup.LayoutParams.WRAP_CONTENT:最大模式,只要不超过窗口大小就行

固定大小,写死dp值的时候,也是精准模式,大小为LayoutParams里面指定的大小

 

在绘制入口performTraversals()方法初始阶段复制为WindowManager.LayoutParams lp = mWindowAttributes;

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

WindowManager.LayoutParams 继承自ViewGroup.LayoutParams

public LayoutParams() {

super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

type = TYPE_APPLICATION;

format = PixelFormat.OPAQUE;

}

所以DecorView默认宽高是填满窗口,是精准模式。当根View的测量规格确认之后,我们就会进行它的子View的测量规格

 

3、子View的MeasureSpec确定

子View的MeasureSpec是根据父view的MeasureSpec确认的,在我们确认了顶级View的MeasureSpec之后,我们开始确认子View的MeasureSpec

一般来讲,布局都是一个容器,需要测量子View,DecorView其实是一个FrameLayout,打开源码看它的onMeasure方法,会调用 super.onMeasure(widthMeasureSpec, heightMeasureSpec);

进入到FrameLayout的onMeasure方法里面,进行子View的测量。这里强调一下,帧布局和线性布局都会调用ViewGroup的measureChildWithMargins方法

去测量子View,和相对布局复杂一些,因为它的各个子View会相互依赖。而在ViewGroup里面也有measureChildren的方法去循环调用测量子View的方法

protected void measureChild(View child, int parentWidthMeasureSpec,

int parentHeightMeasureSpec) {

final LayoutParams lp = child.getLayoutParams();

 

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight, lp.width);

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom, lp.height);

 

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

这里我们就以帧布局来分析

protected void measureChildWithMargins(View child,

int parentWidthMeasureSpec, int widthUsed,

int parentHeightMeasureSpec, int heightUsed) {

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

 

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,

mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin

+ widthUsed, lp.width);

final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,

mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin

+ heightUsed, lp.height);

 

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

从代码可以看出,会调用getChildMeasureSpec来确认子控件的MeasureSpec。入参中,第一个是父控件的测量规格,第二个是父控件已经使用的距离,

包括父View的大小,内边距和外边距,第三个是父控件的宽度。接下来根据源码来分析子View的测量规格的确认。以宽度为例

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

//首先获取到父控件的测量模式和测量大小

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

//判断子View还是否有会只的距离,没有的话就取0

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) {//如果子控件的宽度大于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) {//如果子控件的宽度大于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 //如果父控件是未确认大小模式

/**

* Always return a size of 0 for MeasureSpec values with a mode of UNSPECIFIED

*/

//View.sUseZeroUnspecifiedMeasureSpec 说明下这个变量,从注释中可以看出,如果是UNSPECIFIED,那么它会永远返回0,但是在安卓M(6.0)之后

//这个值会返回true,官方注释是说,在安卓6.0之后,可以有一个hints的size对于UNSPECIFIED的MeasureSpecs,在滚动的容器里面,左边的

//子控件会知道父控件的期望size,例如,list items可以是他们父控件的三分之一的大小

static boolean sUseZeroUnspecifiedMeasureSpec = false;

case MeasureSpec.UNSPECIFIED:

if (childDimension >= 0) {

// Child wants a specific size... let him have it //如果子控件的宽度大于0,那么子控件的大小就是设定的大小,模式也为精准模式

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;

}

//如果子控件不是确认的大小,那么6.0之前,子View的size会是0,模式是UNSPECIFIED,6.0之后子View的size是父控件的确认size,模式是UNSPECIFIED

break;

}

//noinspection ResourceType

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

4、子控件的测量

当View的测量规格确认之后,就会进入到View的measure方法然后进入onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

通过setMeasuredDimension会设定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://如果是不确认模式,大小为size,getSuggestedMinimumWidth方法的返回值

result = size;

break;

case MeasureSpec.AT_MOST:

case MeasureSpec.EXACTLY:

result = specSize;//如果是另外两种模式,就将之前的测量规格的值设定给最终的值。实际情况下,每一个控件都会自己重写自己的onMeasure方法,根据内容重新设定宽高的值

break;

}

return result;

}

这里在说下 getSuggestedMinimumWidth

protected int getSuggestedMinimumWidth() {

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

如果没有设定背景,那么宽度就是mMinWidth,对应android:mMinWidth属性的值,没有设定就是0;如果有设定背景,那么获取背景的最小宽度和mMinWidth比较取最大值

public int getMinimumWidth() {

final int intrinsicWidth = getIntrinsicWidth();

return intrinsicWidth > 0 ? intrinsicWidth : 0;

}

该方法返回背景Drawable的原始宽度,这里并不是所有Drawable会有原始宽度BitmapDrawable有值,ShapeDrawable是0

 

我们可以看出,如果子控件是warp_content模式,那么它的测量大小是parenSize和AT_MOST,这样和mathc_content是一样的,

实际上,在具体的控件的onMeasure方法,最后的大小取值不一定就是parenSize。这里我们以TextView为例,来看看它的onMeasure方法

int widthMode = MeasureSpec.getMode(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width;

int height;

首先取出测量模式和宽高,在定义实际的宽高变量

if (widthMode == MeasureSpec.EXACTLY) {

// Parent has told us how big to be. So be it.如果是精准模式,实际宽就是测量的宽

width = widthSize;

}

如果不是精准模式而是最大填充模式,根据text的内容,计算出实际的大小然后和测量的大小比较取最小的那个

if (widthMode == MeasureSpec.AT_MOST) {

width = Math.min(widthSize, width);

}

高度也是一样的代码,然后就设定最终测量结果setMeasuredDimension(width, height);

所以,在我们自定义控件的时候,如果要使得warp_content生效,我们必须对onMeasure方法进行重写,参考TextView,定义好实际宽度高度

对于AT_MOST模式,我们要判断是使用之前测量规格里面的大小还是使用实际宽度大小。

 

5、获取控件的宽高

通过源码我们知道,在onResume方法执行之后,视图才开始进行测量,所以在onResume方法获取不到View的宽高值,所以需要通过以下途径获取

@Override

public void onWindowFocusChanged(boolean hasFocus) {

super.onWindowFocusChanged(hasFocus);

}

 

view.post(runnable)

 

ViewTreeObserver observer = view.getViewTreeObserver();

observer.addOnGlobalLayoutListener(

 

//在这里面要记得移除掉这个监听器

view.getViewTreeObserver().removeGlobalOnLayoutListener(this)

);

 

View的Layout过程

layout过程比测量过程简单的多,主要首先会通过setFrame方法来确认View的四个顶点的位置,即mLeft,mRight,mTop,mBottom,确认好四个值之后会开始

执行layout方法。

public final int getWidth() {

return mRight - mLeft;

}

public final int getMeasuredWidth() {

return mMeasuredWidth & MEASURED_SIZE_MASK;

}

正常情况下,在layout过程,会调用setFrame方法来确认四个顶点的位子,而传入的值,一般都是left,left+mMeasuredWidth。所以在最终的情况下

getWidth会和getMeasuredWidth一样。

 

View的draw过程

1、绘制背景 background.draw(canvas)

2、绘制自己 onDraw

3、绘制children(dispatchDraw)

4、绘制装饰 onDrawScrollBars

5、通知WMS绘制完成mWindowSession.finishDrawing(mWindow);

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值