从源码角度分析View的工作原理

一、几个重要的概念:

1、MeasureSpec概述:
作用上简单地说就是测量View的Width/Height尺寸。一个子View的Width/Height尺寸同事受自身尺寸参数LayoutParams和父View尺寸的影响。测量过程中系统会将View的LayoutParams根据父View的MeasureSpec参数情况转换成自身的MeasureSpec,然后再根据自身的MeasureSpec测量Width/Height。
注意:这里测出来的Width/Height并不一定是最终的宽高,后面会详细解释原因。
2、MeasureSpec含义:
它是一个32位的int值,高2位为SpecMode,这个参数在我们自定义View的时候经常会用到,它代表测量模式。低30位代表SpecSize,代表SpecMode某种取值下测得的规格大小:
这里写图片描述
在测量的时候,子View的LayoutParams和父容器的MeasureSpec一起决定子View的MeasureSpec,然后再根据MeasureSpec通过onMeasure确定子View的宽高。顶级View(DecorView)与此略有不同,但不做重点分析。
3、几个参数:
int SpecMode = MeasureSpec.getMode(spec); int SpecSize = MeasueSpec.getSize(spec);
int size = SpecSize – padding。即:子View的尺寸 = 父容器的尺寸 - 父容器设置的padding。
子View的MeasureSpec创建规则如下:
这里写图片描述
表格说明:前面已经说过,子View的MeasueSpec是由父容器的MeasueSpec和自身的LayoutParams共同决定的,因此针对父容器的MeasueSpe不同和自身的LayoutParams不同,子View的MeasueSpec有多种不同的组合:
这里写图片描述
至于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。另外,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方法:

//LinearLayout的line806
measureChildBeforeLayout(child,i,widthMeasureSpec,0,heightMeasureSpec,usedHeight);
//LinearLayout的line1511/1514
void measureChildBeforeLayout(View child, int childIndex,int widthMeasureSpec, int totalWidth,
int heightMeasureSpec,int totalHeight) {
	measureChildWithMargins(child, widthMeasureSpec, totalWidth,heightMeasureSpec, totalHeight);
}

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

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<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)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方法是抽象的,protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
由继承他的子View(LinearLayout/RelativeLayout/FramLayout等)具体实现;

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

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

}

同样由继承他的子View如TextView,ImageView等具体实现。我们常用的LinearLayout的OnLayout方法如下:

protected 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.layout(left, top, left + width, top + height);// child是View.class的对象
	}

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),而childLeft += mDividerWidth(若Drawable对象为空mDividerWidth=0,否则mDividerWidth=Drawable.getIntrinsicWidth())。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);	}
. . . . . .
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值