自定义View学习笔记03—View的工作原理简介

这篇博客有点长,但我想讲得还是比较清晰的,希望能坚持看完。

一、几个重要的概念:
1、MeasureSpec概述:

作用上简单地说就是测量View的Width/Height尺寸。一个子View的Width/Height尺寸同事受自身尺寸参数LayoutParams和父View尺寸的影响。测量过程中系统会将View的LayoutParams根据父View的MeasureSpec参数情况转换成自身的MeasureSpec,然后再根据自身的MeasureSpec测量Width/Height。

注意:这里测出来的Width/Height并不一定是最终的宽高,后面会详细解释原因。

2、MeasureSpec含义:

它是一个31位的int值,高2位为SpecMode,这个参数在我们自定义View的时候经常会用到,它代表测量模式。低30位代表SpecSize,代表SpecMode某种取值下测得的规格大小,具体如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uPWM5Vws-1589614544813)(https://img-blog.csdn.net/20171225144806785?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFveXVlZ29uZ3pp/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
在测量的时候,子View的LayoutParams和父容器的MeasureSpec一起决定子View的MeasureSpec,然后再根据MeasureSpec通过onMeasure确定子View的宽高。顶级View(DecorView)(根View)与此略有不同,但不做重点分析。

3、几个参数:
int SpecMode = MeasureSpec.getMode(spec); int SpecSize = MeasueSpec.getSize(spec);
int size = SpecSize – padding。
即:子View的尺寸 = 父容器的尺寸 - padding。子View的MeasureSpec创建规则如下:

自定义View学习笔记03—View的工作原理简介_第1张图片
表格说明:前面已经说过,子View的MeasueSpec是由父容器的MeasueSpec和自身的LayoutParams共同决定的,因此针对父容器的MeasueSpe不同和自身的LayoutParams不同,子View的MeasueSpec有多种不同的组合:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bHlE8T5y-1589614544816)(https://img-blog.csdn.net/20171225145446265?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGFveXVlZ29uZ3pp/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
至于UNSPECIFED模式,主要用于系统级别的多次Measue,我们不需要关注这个。

二、View的工作流程

主要是指View的measure测量View的宽/高、layout确定View的最终宽/高和四个顶点的位置、draw将View绘制到屏幕上去这三大流程。这里我们先讲measure流程,layout流程和draw流程这两个我们后面讲。

measure():
完成View的测量,他是一个final类型的方法,这表示它不能被复写和重写。他调用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: 
			break;
		case MeasureSpec.EXACTLY: 
			result = specSize; 
			break;
	}
	return result;
 }

简单的理解:getDefaultSize()返回的大小就是specSize,因为UNSPECIFIED模式我们通常用不上,AT_MOST模式下什么也没有做,EXACTLY模式下result=specSize。而这个specSize就是View测量后的大小,但不一定是最终的大小(虽然大概率是最终大小,但小部分情况下不是)。

再来看看getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的源码:

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

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

public int getMinimumWidth() {
	final int intrinsicWidth = getIntrinsicWidth();
	return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

public int getMinimumHeight() {
	final int intrinsicHeight = getIntrinsicHeight();
	return intrinsicHeight > 0 ? intrinsicHeight : 0;
}

getSuggestedMinimumWidth/Height中的mMinWidth/mMinHeight由android:minWidth/minHeight在xml中设置,如果没有设置这个参数,则默认为0.

getMinimumWidth/Height返回的就是背景中Drawable的原始宽高,如果Drawable的原始宽高大于0,则返回具体的值,否则返回0.

结论:直接继承View的自定义控件需要重写OnMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content的时候相当于使用match_parent。因为如果View在布局中使用wrap_content,他的SpecMode是AT_MOST模式,此时他的宽高就是SepcSize。查前面的表可知,在AT_MOST模式下,SepcSize的大小就是parentSize。亦即父类的SpecSize-padding。这就相当于使用match_parent。解决方法:重写onMeasure方法。具体如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
	int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
	if(widthSpecMode == MeasureSpec.AT_MOST && 
		heightSpecMode == MeasureSpec.AT_MOST){
	    setMeasuredDimension(width, height);
	}else if(widthSpecMode == MeasureSpec.AT_MOST){
	    setMeasuredDimension(width, heightMeasureSpec);
	}else if(heightSpecMode == MeasureSpec.AT_MOST){
	    setMeasuredDimension(widthMeasureSpec, height);
	}
}

针对自定义View,我们只需要给View制定一个默认的内部宽高(文中的width,height),并在wrap_content时设置该参数即可,对于非wrap_content情况,我们沿用系统的测量值即可,亦即:android:layout_width和android:layout_height两个属性,哪一个被设置为wrap_content,我们就在自定义的代码中使用对应的width/height。至于具体怎么指控,则没有固定的套路和规则。

三、ViewGroup的工作流程:

ViewGroup的工作流程与View稍有不同,它除了完成对自己的measure以外,还会去对自己所有的子View进行measure。这里我们先讲measure流程,layout流程和draw流程这两个我们后面讲。另外,ViewGroup是一个抽象类,他没有重写View的OnMeasure方法,而是额外提供了一个measureChildren的方法专门用来测量子View:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
private 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);
}

意思很显然,就是在measureChild里面,去除子View的LayoutParams,然后getChildMeasureSpec方法创建子View的measureSpec并将其传给child的measure方法进行测量getChildMeasureSpec与前面的getSuggestedMinimumHeight/getSuggestedMinimumWidth方法原理相同,再此不累述。就这样,ViewGroup通过for循环完成对其自身子View的measure工作。

事实上ViewGroup并没有具体定义其测量过程,测量过程的onMeasure方法由继承他的子View(LinearLayout、RelativeLayout等)去具体实现,因为不同的子View有不同的布局特征,测量过程完全不同,无法给出统一的模式。

四、LinearLayout的Measure过程分析
LinearLayout的Measure方法很简单:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	if (mOrientation == VERTICAL) {
		measureVertical(widthMeasureSpec, heightMeasureSpec);
	} else {
		measureHorizontal(widthMeasureSpec, heightMeasureSpec);
	}
}

对于LinearLayout布局,只有两种方式要么是vertical竖直方向,要么是horizontal水平方向,因此就处理两个方向上的测量。

对于measureVertical和measureHorizontal方法都很长,几百行,在此不做展示。核心思路是:在两个方法里面会遍历所有的子View并对每一个子View执行measureChildBeforeLayout方法:

measureChildBeforeLayout(child,i,widthMeasureSpec,0,heightMeasureSpec,usedHeight);//LinearLayout.class的806行

void measureChildBeforeLayout(View child, int childIndex,int widthMeasureSpec, int totalWidth,int heightMeasureSpec,int totalHeight) {

	measureChildWithMargins(child, widthMeasureSpec, totalWidth,heightMeasureSpec, totalHeight);
}//LinearLayout.class的1511/1514行

measureChildBeforeLayout方法里面会调用父类ViewGroup的measureChildWithMargins方法,获取子View的childWidthMeasureSpec和childHeightMeasureSpec参数,然后在调用父类View的measure方法进行测量,并调用onMeasure方法,至此又回到我们最开始的分析:

private 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在父类View/ViewGroup里,child是View的对象:View child=new View();
	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureVertical方法里面有一个用于记录累计高度/宽度的参数:mTotalLength,每测量一个子View的高度,就对它执行一次加法,每次增加的高度包括子View的高度及其marginTop/Bottom以及父容器的paddingTop/Bottom。完成对子View的测量后,View还要对自身进行测量。
最终测量完成后,通过getMeasuredWdith/Height方法拿到View的测量宽高。但某些极端情况下,View的measure测量要反复执行数次才能最终确定,因此我们自定义的时候最好是在OnLayout方法里面去获取View的宽高。
RelativeLayout的测量过程与此类似,但更复杂,在此不累述。

五、获取View宽高的几种方法

A、在Activity中复写onWindowFocusChanged方法并获取宽高:

public void onWindowFocusChanged(boolean hasFocus) {
	super.onWindowFocusChanged(hasFocus);
	if(hasFocus){
		int width = mIvBitMap.getMeasuredWidth();
		int height = mIvBitMap.getMeasuredHeight();
	}
}

B、调用View.post/postDelayed方法。后者线程安全。

mIvBitMap.postDelayed(new Runnable() {
	@Override
	public void run() {
		int widthPost = mIvBitMap.getMeasuredWidth();
		int heightPost = mIvBitMap.getMeasuredHeight();
	}
}, 1000);

C、使用ViewTreeObserver的回调接口OnGlobalLayoutListener接口就是其中之一。
D、通过view.measure方法手动测量,这种方法很复杂。

以上四种方法,最常用的是A和B两种方法,尤其是方法B。C、D两种方法几乎不用,在此不多说。

六、layout过程:
layout的作用是用来确定子View在ViewGroup中的位置。当ViewGroup的位置确定后,ViewGroup会调用OnLayout方法遍历他的所有子View,并调用其layout方法(layout方法里面调用了OnLayout方法)。layout方法确定View自身的位置,onLayout方法会确定所有子View的位置。ViewGroup的layout方法调用的是父类View的,View的layout方法具体实现如下:

public void layout(int l, int t, int r, int b) {
	if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
	    onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
	    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}
	int oldL = mLeft;	
	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);
	    if (shouldDrawRoundScrollbar()) {
			if(mRoundScrollbarRenderer == null) {
	    		mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);			
	    	}
	    } else {			
		    mRoundScrollbarRenderer = null;	    
		}
	    mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
	    ListenerInfo li = mListenerInfo;
	    if (li != null && li.mOnLayoutChangeListeners != null) {
			ArrayList listenersCopy =			(ArrayList)li.mOnLayoutChangeListeners.clone();
			int numListeners = listenersCopy.size();
			for (int i = 0; i < numListeners; ++i) {
	    		listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
			}
	    }
	}
	mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;		
	mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
	if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
	    mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
	    notifyEnterOrExitForAutoFillIfNeeded(true);
	}
}

大致流程是:
1、通过setFrame方法设定view的四个顶点的位置,即初始化mLeft、mRight、mTop、mBottom,确定了这四个值,就确定了View在父容器中的位置。
2、调用onLayout方法让父容器确定子View的位置。

值得注意的是:
1、ViewGroup的onLayout方法是抽象的:

private abstract void onLayout(boolean changed, int l, int t, int r, int b);

由继承他的子View(LinearLayout/RelativeLayout/FramLayout等)具体实现;

2、View的onLayout方法是空的,什么也没做:

private void onLayout(boolean changed,int left,int top,int right,int bottom){   

}

同样由继承他的子View如TextView,ImageView等具体实现。

我们常用的LinearLayout的OnLayout方法如下:

private void onLayout(boolean changed,int l,int t,int r,int b){
	if (mOrientation == VERTICAL) {		
		layoutVertical(l, t, r, b);
	} else {		
		layoutHorizontal(l, t, r, b);	
	}
}

很明显的,OnLayout依照Vertical和Horizontal两种布局方式分别布局。这里仍以layoutVertical方法作分析:

void layoutVertical(int left, int top, int right, int bottom) {
	………
	for (int i = 0; i < count; i++) {
		final View child = getVirtualChildAt(i);
		if (child == null) {
		childTop += measureNullChild(i);
		}
		if (child.getVisibility() != GONE) {
			final int childWidth = child.getMeasuredWidth();
			final int childHeight = child.getMeasuredHeight();
			final LinearLayout.LayoutParams lp =
			(LinearLayout.LayoutParams) child.getLayoutParams();
			int gravity = lp.gravity;
			if (gravity < 0) {	gravity = minorGravity;	}
			final int layoutDirection = getLayoutDirection();
			final int absoluteGravity = Gravity.getAbsoluteGravity(
								       gravity,layoutDirection);
			switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
				case Gravity.CENTER_HORIZONTAL:
					childLeft=paddingLeft+((childSpace-childWidth)/2)+
					lp.leftMargin-lp.rightMargin;
				break;
				case Gravity.RIGHT:		
					childLeft=childRight-childWidth-lp.rightMargin;	
				break;
				case Gravity.LEFT:
				default:	
					childLeft = paddingLeft + lp.leftMargin;		
				break;
			}
			if (hasDividerBeforeChildAt(i)) {	
				childTop += mDividerHeight;	
			}
			childTop += lp.topMargin;		
			setChildFrame(child,childLeft,childTop+getLocationOffset(child),
						  childWid,childHei);
			childTop += childHeight + lp.bottomMargin + 
							getNextLocationOffset(child);
			i += getChildrenSkipCount(child, i);

		}
	}
}

private void setChildFrame(View child,int left,int top,int width,int height){
	// child是View.class的对象
	child.layout(left, top, left + width, top + height);
}

layoutVertical方法会通过for循环遍历childView,并调用setChildFrame方法给对应的子View指定具体的位置,从setChildFrame的源码及参数来看,指定子View的位置时并不是直接指定View的四个顶点,而是指定ChildLife和childTop两个顶点,然后分别加上测得子View的宽高,得到另外两个顶点。每调用一次setChildFrame方法,childTop就会增大,这使得后面循环到的子View会被放在垂直方向靠后的位置。这正好符合了LinearLayout布局的Vertical属性的定义。而setChildFrame方法内部则是通过第一个参数child调用子View的layout方法,而该方法依然是父类View.class的,layout方法前面已经有分析,在此不累述。如此循环,直至完成整个ViewTree的layout过程。

疑问:View的测量宽高和最终宽高的区别——>>>>>
这个问题可以更具体的表述为——>>View的getMeasuredWidth/Height方法与View的getWidth/Height方法的区别:
我们先分别看他们的实现方式:

public final int getWidth() {	
	return mRight - mLeft;		
}
public final int getHeight() {	
	return mBottom - mTop;		
}

mLeft 、mRight、mBottom、mTop四个参数在View.class的setFrame方法中被赋值,而setFrame方法被layout方法和setOpticalFrame方法同时调用,传入setFrame方法的参数正是left,right,top,bottom。而layout方法被子类LinearLayout的setChildFrame(View child, int l, int t, int w, int h)方法以child.layout(l,t,r,b)的形式调用,其中child是View的对象,然后layoutHorizontal方法和layoutVertical方法调用setChildFrame方法,最终这两个方法被onLayout方法调用。onLayout方法复写自父类View.class。

再细说说参数l和childWidth的由来:

l = childLeft + getLocationOffset(child),
//若Drawable对象为空mDividerWidth=0,否则
//mDividerWidth = Drawable.getIntrinsicWidth()
childLeft += mDividerWidth。
childWidth = child.getMeasuredWidth()。

方法child.getMeasuredWidth来自于父类View.class。我们可以看看这个方法的实现:

public final int getMeasuredWidth() {        
	return mMeasuredWidth & MEASURED_SIZE_MASK;    
}

其中mMeasuredWidth在View.class的setMeasuredDimensionRaw方法中被赋值:

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

该方法被setMeasuredDimension(int measuredWidth, int measuredHeight)方法调用,而onMeasure (int widthMeasureSpec, int heightMeasureSpec)方法又调用了setMeasuredDimension方法,而widthMeasureSpec和heightMeasureSpec两个参数恰好是measure测量方法运行之后获得的包含具体的宽高尺寸和测量模式的数据,至此我们又回到了前面“二、View的工作流程”小节,并清晰地解释了getWidth方法中参数mRight和mLeft的来历,同时讲解了getMeasuredWidth()方法的调用。参数mBottom和mTop与此类似,不累述。

简单的说就是:getMeasuredWidth/Height获得的是measure测量方法运行过后的宽高。getWidth/Height方法获得的是layout布局方法运行过程中的宽高。二者的获得只是在时间上有些不同,但我们通常可以认为二者是相等的。极少数情况下二者有出入。尤其是在layout过程中对l,t,r,b四个参数做了处理的时候。

七、draw的过程
Draw的过程比前两个都要简单,它主要是将布局视图绘制到屏幕上,遵循以下几个步骤:

1. Draw the background
//Step 1, draw the background, if needed
	if (!dirtyOpaque) {    
		drawBackground(canvas);
	}
2. If necessary, save the canvas' layers to prepare for fading
// 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) {
3. Draw view's content
// Step 3, draw the content
	if (!dirtyOpaque) {	
		onDraw(canvas);	
	}
4. Draw children
// Step 4, draw the children
	dispatchDraw(canvas);		
	drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
	if (mOverlay != null && !mOverlay.isEmpty()) {
	    mOverlay.getOverlayView().dispatchDraw(canvas);
	}
5. If necessary, draw the fading edges and restore layers
6. Draw decorations (scrollbars for instance)
// Step 6, draw decorations (foreground, scrollbars)
	onDrawForeground(canvas);
// Step 7, draw the default focus highlight
	drawDefaultFocusHighlight(canvas);
	if (debugDraw()) {    
		debugDrawFocus(canvas);	
	}
	. . . . . .

自定义View几个关键步骤总结:

自定义View学习笔记03—View的工作原理简介_第2张图片

你可能感兴趣的:(自定义View)