一、几个重要的概念:
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); }
. . . . . .