View的工作原理


本篇是个人在阅读安卓开发艺术探索一书后的知识归纳以及分享自己的一些理解所写的博客,在于以后可以方便的进行复习相关知识,毕竟这些基础知识是多多去理解的。

一、初始ViewRoot和DectorView

ViewRoot对应于ViewRootImpl类对象,它是连接WindowManager和DectorView的纽带,View的三大流程均是通过ViewRoot来完成的。首先在ActivityThread中,当Activity对象被创建完后,会将DectorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DectorView建立关联。

View的绘制流程就是从ViewRoot的performTraversals方法开始的,经过measure、layout、draw三个过程才能最终将一个View绘制出来,meaure确定测量宽高,layout确定最终宽高以及View在父容器中四个顶点的位置,draw负责将View绘制在屏幕上。performTraversals流程如下:
在这里插入图片描述
首先performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw三个流程,在performMeasure中调用measure方法,在measure方法中又调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这时measure流程就从父容器传递到了子元素中,这就完成了依次measure过程。接着子元素重复父容器的measure过程,如此反复完成整个View树的遍历。performLayout和performDraw的传递过程类似,不过draw过程是通过draw方法中的dispatchDraw来实现的(具体看后面draw过程)。

measure过程确定了View的测量宽高,大部分情况就是最终宽高,具体后续分析,通过getMeasuredWidth和getMeasuredHeight获取;layout过程确定了View的最终宽高以及四个顶点的位置,通过getTop、getBottom、getLeft、getRight获取四个顶点坐标,并通过getWidth和getHeight方法获取最终宽高;draw过程将View绘制到屏幕上。

DecorView是一个FrameLayout,一般包含一个LinearLayout,其中又有两部分,一个是标题栏,下方式内容栏。Activity中setContentView就是设置内容栏的布局。
在这里插入图片描述

二、理解MeasureSpec

首先了解一下MeasureSpec,从源码可知MeasureSpec参与了View的measure过程,MeasureSpec很大程度决定了一个View的尺寸,MeasureSpec的创建过程受父容器的MeasureSpec和自身LayoutParams影响。

2.1MeasureSpec
MeasureSpec是一个32位int值,高2位代表SpecMode测量模式,低30位代表SpecSize规格大小。具体可看源码:

private static final int MODE_SHIFT = 30;
//3左移30位,也就是最高2位11,用来后续进行与运算获取值
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//分别为3种SpecMode模式,0、1、2左移30位
public static final int UNSPECIFIED = 0 << MODE_SHIFT:
public static final int EXACTLY = 1 << MODE_SHIFT:
public static final int AT_MOST= 2 << MODE_SHIFT:

public static int makeMeasureSpec(int size , int mode) {
	if(sUseBrokenMakeMeasureSpec) {
		//打包SpecSize + SpecMode
		return size + mode;
	} else {
		return (size & ~MODE_MASK) | (mode & MODE_MASK);
	}
}

public static int getMode(int measureSpec) {
	//通过与运算获取SpecMode,解包
	return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
	//通过与运算获取SpecSize,解包
	return (measureSpec & ~MODE_MASK);
}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多对象内存分配,提供了打包和解包方法。SpecMode(高2位)和SpecSize(后30位)也是int值,一组SpecMode和SpecSize可以打包为一个MeasureSpec,一个MeasureSpec可以通过解包得到SpecMode和SpecSize,从源码中也可以看出来。

SpecMode有三种,如下:

  • UNSPECIFIED:父容器不对View有任何限制,一般用于系统内部。
  • EXACTLY:父容器已经检测出View所需要的精确大小,View的大小就是SpecSize。对应于LayoutParams中的match_parent和具体数值这两种模式。
  • AT_MOST:父容器指定一个可用大小即SpecSize,View的大小不能大于这个值。对应于LayoutParams中的wrap_content。

2.2MeausreSpec和LayoutParams的对应关系
DecorView的MeasureSpec由窗口尺寸和自身LayoutParams决定。
View的MeasureSpec是由LayoutParams和父容器的MeasureSpec一起决定的(从源码可看出),MeasureSpec一旦确定后,onMeasure方法中就可以确定View的测量宽高。
普通View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildWithMargins方法:

public void  measureChildWithMargins(View child,
									 int parentWidthMeasureSpec, int widthUsed
									 int parentHeightMeasureSpec, int heightUsed) {
	....
	//通过getChildMeasureSpec获取子元素的MeasureSpec,通过这个方法可以看出MeasureSpec由什么决定
	final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
	mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
	final int childHeightMeasureSpec = ...;.
	//获取MeasureSpec后调用子元素的measure方法来完成对子元素的测量
	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

通过getChildMeasureSpec获取子元素的MeasureSpec,通过这个方法我们就可以看出MeasureSpec由父容器的MeasureSpec和自身LayoutParams决定:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
	//通过父容器MeasureSpec获取父容器的SpecMode和SpecSize
	int specMode = MeasureSpec.getSpecMode(spec);
	int specSize = MeasureSpec.getSpecSize(spec);
	
	//具体可使用大小
	int size = Math.max(0, specSize - padding);
	//子元素SpecSzie和SpecMode
	int resultSize = 0;
	int resultMode = 0;

	switch(specMode) { 
		 //父SpecMode为EXACTLY时
		case MeasureSpec.EXACTLY: 
			if(childDimension >= 0 ) {  //子自身LayoutParams为具体值
				resultSize = childDimension;
				resultMode = MeasureSpec.EXACTLY;
			} else if(childDimension == LayoutParams.MATCH_PARENT )  { //子自身LayoutParams为match_parent
				resultSize = size;
				resultMode = MeasureSpec.EXACTLY;
			} else if(childDimension == LayoutParams.WRAP_CONTENT )  { //子自身LayoutParams为wrap_content
				resultSize = size;
				resultMode = MeasureSpec.AT_,MOST;
			} 
			break;
			
		//父SpecMode为AT_MOST时
		case MeasureSpec.EXACTLY: 
			if(childDimension >= 0 ) {  //子自身LayoutParams为具体值
				resultSize = childDimension;
				resultMode = MeasureSpec.EXACTLY;
			} else if(childDimension == LayoutParams.MATCH_PARENT )  { //子自身LayoutParams为match_parent
				resultSize = size;
				resultMode = MeasureSpec.AT_MOST;
			} else if(childDimension == LayoutParams.WRAP_CONTENT )  { //子自身LayoutParams为wrap_content
				resultSize = size;
				resultMode = MeasureSpec.AT_,MOST;
			} 
			break;

		//父SpecMode为UNSPECIFIED时
		case MeasureSpec.EXACTLY: 
			if(childDimension >= 0 ) {  //子自身LayoutParams为具体值
				resultSize = childDimension;
				resultMode = MeasureSpec.EXACTLY;
			} else if(childDimension == LayoutParams.MATCH_PARENT )  { //子自身LayoutParams为match_parent
				resultSize = 0;
				resultMode = MeasureSpec.UNSPECIFIED;
			} else if(childDimension == LayoutParams.WRAP_CONTENT )  { //子自身LayoutParams为wrap_content
				resultSize = 0;
				resultMode = MeasureSpec.UNSPECIFIED;
			} 
			break;
	}
}

通过上述方法便知道View的MeasureSpec是通过自身LayoutParams和父容器的MeasureSpec共同决定的,简单用文字说明一下,当View采用固定宽高也就是具体值的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式EXACTLY并且大小为LayoutParams设置的大小。当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式且大小是父容器剩余空间;如果父容器是最大模式,那么View也是最大模式且大小不会超过父容器剩余空间。当View的宽/高是wrap_content时,无论父容器是什么模式,View的模式总是最大化且大小不能超过父容器剩余空间。上述解释不考虑UNSPECIFIED模式。这些都是从源码可以看出来的,下面用一张表来总结:
在这里插入图片描述

三、View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,measure过程确定了View的测量宽高,layout过程确定了View的最终宽高以及四个顶点的位置,draw过程负责将View绘制到屏幕上。

3.1measure过程
measure过程需要分情况来看,如果是一个原始的View,是需要通过measure方法就完成了其测量过程;如果是一个ViewGroup,除了完成自己的测量过程,还会遍历所有子元素并调用子元素的measure方法,各个子元素再递归的去执行这个流程;

3.1.1view的measure过程
view的measure过程由measure方法完成,其次measure方法是一个final类型,这代表不能被重写,在measure方法中会调用onMeasure方法,我们来看看onMeasure方法:

public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	//设置测量宽高
	setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
		getDefaultSize(getSuggesttedMinimumHeight(), heightMeasureSpec));
}

onMeasure方法很简单,就是调用setMeasureDimension方法设置View的宽高的测量值,值则由getDefaultSize提供,看看这个方法:

public static int getDefaultSize(int size, int measureSpec) {
	int result = size;
	//获取SpecMode以及SpecSize
	int specMode = MeasureSpec.getSpecMode(measureSpec);
	int specSize = MeasureSpec.getSpecSize(measureSpec);

	switch (specMode) {
		//测量模式为UNSPECIFIED时值为getSuggestedMinimum..()方法的返回值
		case MeasureSpec.UNSPECIFIED:
			result = size;
			break;
		//AT_MOST和EXACTLY模式,值都为specSize
		case MeasureSpec.AT_MOST:
		case MeasureSpec.EXACTLY:
			result = specSize
			break;
	}
	return result;
}

getDefaultSize方法也很简单,传进去getSuggestedMinimumWidth/Height以及measureSpec,如果是UNSPECIFIED返回的是getSuggestedMinimumWidth/Height,一般用于系统内部测量;而无论是AT_MOST还是EXACTLY测量模式返回的值都是measureSpec的specSize,这个specSize就是测量宽/高,所以可以得出结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小;否则在布局中使用wrap_content就等同于match_parent(从源码以及分析可以看出来),宽高等于specSize,而specSize为父容器剩余空间;如何重写,看看下面:

public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super(widthMeasureSpec, heightMeasureSpec);
	int widthSpecMode = MeasureSpec.getSpecMode(widthMeasureSpec);
	int widthSpecSize = MeasureSpec.getSpecSize(widthMeasureSpec);
	int heightSpecMode = MeasureSpec.getSpecMode(heightMeasureSpec);
	int heightSpecSize = MeasureSpec.getSpecSize(heightMeasureSpec);
	//直接判断,如果是wrap_content,specMode为AT_MOST,直接设置自身大小,其他则使用specSize
	if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MesureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, mHeight);
	} else if (widthSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(mWidth, heightSpecSize);
	} else if (heightSpecMode == MeasureSpec.AT_MOST) {
		setMeasuredDimension(widthSpecSize, mHeight);
	}
}

解释一下,我们只需要提供一个mWidth、mHeight在wrap_content时设置相应宽/高即可。对于非wrap_conten情况使用系统的测量值就可以了。

3.1.2ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程,还需要遍历去调用所有子元素的measure方法,各个子元素再递归执行这个过程。但ViewGroup是个抽象类,它没有重写View的onMeasure方法定义具体的测量过程,这是因为不同的ViewGroup子类有不同的布局特性,其测量细节也各不相同,所以其测量过程onMeasure方法需要各个子类去具体实现;但他提供了一个measureChildren方法用于遍历并调用子元素measure方法,看看源码:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
	final int size = mChildrenCount;
	final View[] children = mChildren;
	//遍历所有子元素,调用measureChild方法,measureChild方法其实只是取出子元素LayoutParamsr
	//然后根据父容器MeasureSpc获得子元素MeasureSpec然后调用子元素measure方法
	for(int i = 0; i < size; i++) {
		final View child = children[i];
		if(child.mViewFlags & VISIBILITY_MASK) != GONE) {
			measureChild(child, widthMeasureSpec, heightMeasureSpec);
		}
	}
}

measureChildren方法遍历所有子元素,调用measureChild方法,传入该子元素以及父容器MeasureSpec,看看measureChild方法:

protected void measureChild(View child, int parentWidthMeasureSpec, 
							int parentHeightMeasureSpec) {
	//取出子元素LayoutParams
	final LayoutParams lp = child.getLayoutParams();
	
	//调用getChildMeasureSpec方法通过父容器measureSpec和自身LayoutParams获取measureSpec
	//该方法之前分析过,可看上面
	final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
		mPaddingLeft + mPaddingRight, lp.width);
	final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
		mPaddingLeft + mPaddingRight, lp.height);
		
	//获取measureSpec后直接调用子元素的measure方法
	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

总的分析一遍,ViewGroup是个抽象类,没有重写onMeasure方法,因为不同子类有着不同的布局特性,其测量细节也各不相同,所以具体测量过程需要交给子类去完成,一般在onMeasure方法里面调用方法去遍历所有子元素并调用其measure方法,子元素递归这一个过程,直到完成所有measure过程。同样ViewGroup提供了meausreChildren方法用于遍历并调用所有子元素的measure方法,在measureChildren方法中遍历了所有子元素,并调用measureChild方法,在measureChild方法中根据父容器measureSpec和自身LayoutParams获取子元素measureSpec,接着measureChild方法中继续调用子元素的measure方法来进行测量。

3.1.3在Activity中获取View的宽高
我们要在Activity启动时就去获取某个View的测量宽高,实际上在onCreate、onStart、onResume方法中均无法正确得到这个View的宽高信息,这时因为View的measure过程和Activity的生命周期方法并不是同步实行的,所以无法保证在Activity执行相应生命周期函数时某个View的measure过程已经完成了,如果View还没由测量完毕,获得的值就是0,有四种方法来解决这个问题:

  1. onWindowFocusChanged: View已经初始化完毕,宽/高已经准备好了,这时获取是没有问题的。
  2. view.post(runnable):通过post将一个runnable投递到消息队列尾部,等待Lopper调用时,View已经准备好了。
  3. ViewTreeObserver:使用其OnGlobalLayoutListener接口,当View树状态发生改变或View树内部View可见性发生改变时,onGlobalLayout方法被回调。
  4. View.measure(int widthMeasureSpec, int heightMeasureSpec):手动对View进行measure得到宽高,比较复杂。

3.2Layout过程
首先调用layout方法确定View本身的位置,通过onLayout方法确定所有子元素的位置。layout方法中,首先会通过setFrame方法来设定View的四个顶点的位置,这四个顶点一旦确定,View在父容器中的位置就确定了;接着调用onLayout方法确定子元素的位置,同样onLayout方法的具体实现与布局相关,View和ViewGroup均没有真正实现,需要自己去实现,一般就是遍历所有子元素并调用其layout方法完成传递,看看Layout源码:

public void layout(int l, int t, int r, int b) {
	if ((mPrivateFlag3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0 ) {
		onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
		mPrivateFlag3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}

	int oldL = mLeft;
	int ondT = mTop;
	int oldB = mBottom;
	int oldR = mRight;

	boolean changed = isLayoutModeOptical(mParent) ? 
			setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
	if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
		onLayout(changed, l, t, r, b);
		...
	}
	...
}

3.2draw过程
drea过程比较简单,作用就是将View绘制到屏幕上。一般有以下几步:

  1. 绘制背景:drawBackground(canvas)。
  2. 绘制自己:onDraw(canvas)。
  3. 绘制children:dispatchDraw(canvas)。
  4. 绘制装饰:onDrawScrollBars(canvas)。
    看看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;

	int saveCount;

	if (!dirtyOpaque) {
		//绘制背景
		drawBackground(canvas);
	}

	final int viewFlags = mViewFlags;
	boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
	boolean verticalEdges = (viewFlags &FADING_EDGE_VERTICAL) != 0;
	if (!verticalEdges && !horizontalEdges) {
		//绘制自身
		if(!dirtyOpaque) onDraw(canvas);

		//绘制children
		dispatchDraw(canvas);

		//绘制装饰
		onDrawScrollBars(canvas);

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

View绘制过程的传递是通过dispatchDraw来实现的(绘制子元素),dispatchDrawo方法会遍历并调用所有子元素的draw方法,如此draw事件就一层层传递下去了。View还有一个方法setWillNotDraw,看看源码:

public void setWillNotDraw(boolean willNotDraw) {
	setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

如果一个View不需要绘制任何内容,设置这个标记位为true,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但ViewGroup默认启用这个优化标记位,所以如果我们要一个ViewGroup通过onDraw方法绘制内容时,我们要显示关闭WILL_NOT_DRAW这个标记位。

四、自定义View

4.1自定义View分类
一般有四类:

  1. 继承View重写onDraw方法:常用在实现一些不规则的效果的控件(我写过的一个钟表控件就是继承View重写onDraw方法)。需要自己支持wrap_content(之间讲过很简单重写onMeasure方法,在wrap_contnet时显示指定一个值即可),padding也要自己处理。
  2. 继承ViewGroup派生特殊的Layout:实现一些自定义布局(我写过的一个流式布局就是这个,流式布局就是历史记录那样的,这行不够换下行显示,重写onLayout方法)。一般需要合适的处理ViewGroup的测量、布局两个过程,并同时处理子元素的测量和布局过程。
  3. 继承特定的View:一般用于拓展已有的View的功能。
  4. 继承特定的ViewGroup:一般用于将几种View组合在一起的时候(自定义组合控件,同样很常见的一个例子,就是制作一个特别的按钮,上方是ImageView,下方是TextView就可以用这种方式,只需代码引入布局简单实现)。

4.2自定义View须知

  1. 让View支持wrap_content:在measure过程中分析过源码,View使用wrap_content和match_parent效果是一样的,所以直接继承View或ViewGroup时要重写onMeasure方法来处理wrap_content。
  2. 支持padding:直接继承View的控件如果不在draw方法中处理padding,那么padding属性也是无法起作用的。
  3. 尽量不要再View中使用Handler,没必要:View内部本身提供了post系列方法,完全可以替代Handler的作用。
  4. View中如果有线程或动画,要记得及时停止,onDetachedFromWindow是个好时机:当包含此View的Activity退出或当前View被remove时,View的onDetachedFromWindow会调用,这时可以停止动画和线程。如果不及时处理,可能会造成内存泄漏。
  5. View带有滑动嵌套时,要处理好滑动冲突:可看上一节博客如何处理滑动冲突,一般就外部拦截法和内部拦截法。

具体自定义控件的过程,从我写过的几个来看,一般就是先在values文件夹中创建一个attrs.xml文件,在里面创建自定义属性;在自定义控件的代码中声明这些属性,创建几个构造函数(基本是一样定式的),通过TyepdArray获取这些属性并初始化,接下来就根据自己的需求重写各函数,一般创建不规则的控件就重写onDraw方法即可,创建新布局重写onLayot方法。其他拓展控件功能或自定义组合控件比较简单。具体还是要自己去写几个自定义控件,多写写就可以熟悉了。
下面的给几个其他博客,是一些例子大家可以去跟着自己练练,个人感觉有一定难度且可以了解整个自定义控件的过程:

继承View的一个圆形时钟控件:作者:Hello_code,Android自定义View之时钟
继承ViewGroup实现流式布局:作者:沐左,自定义控件 - 流式布局(FlowLayout)
继承特定的ViewGroup的自定义组合控件,大家可以自己去写一个按钮,上方ImageView,下方TextView,比较简单。同样继承特定的View拓展功能也比较简单,就不赘述了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值