View的工作流程
View的工作流程主要指:measure、layout、draw这三大流程。
View的绘制是从上往下一层层迭代下来的。DecorView–>ViewGroup(—>ViewGroup)–>View ,按照这个流程从上往下,依次measure(测量),layout(布局),draw(绘制)。
为方便笔记,上面参考于此链接
Measure过程
一.View的measure过程
View的measure过程由其measure方法来完成。measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会调用onMeasure方法,因此只需要看onMeasure的实现即可:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看出上述代码,通过setMeasuredDimension在设置View的宽/高,它的参数又调用了getDefauSize方法,所以我们看getDefauSize这个方法:
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:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
1.可以看出,它是根据measureSpec分解出specSize和specMode两个值,但是又看了一下case的值,UNSPECIFIED这个我们前面说过,一般不管,所以我们你只需要看AT_MOST和EXACTLY,然后他们的处理方式一样,返回了specSize的值,而这个specSize就是View测量后的大小。
2.为什么说是View测量后的值?是因为Veiw的最终大小是在layout阶段确定的,所以这里要加以区分。但其实几乎所有情况下View的测量大小和最终大小是相等的。后面会说特殊情况。
3.还有一个结论:直接继承View的自定义控件需要重写onMeasure方法中的warp_content,否则使用wrap_content和使用match_partent的效果是一样的,这个从上面方法和前面的表可以看出。
解决方案:
protected void onMeasure(int widthMeasureSpec , int heightMeasureSpec){ super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
在上面代码中,我们只需要在View指定为warp_content时设置一个默认内部宽/高(mWidth,mHeight)。至于这个默认宽/高大小如何指定,没有固定的依据,需要灵活指定。可以查看TextView和ImageView源码查看它们的解决方式。由于getDefaultSize的参数又是getSuggestedMinimumWidth()方法返回的,所以看一下这个方法:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
很好理解,这里就是看View是否有设置Background,如果设置了,就选mMinHeight和 mBackground.getMinimumHeight()中最大的那个值返回,mMinHeight其实就是android:minHeight这个属性所指的值,如果不指定该属性,则默认为0。
*举个场景例子:当时写计算器的时候,给Button设置背景后导致Button之间默认的空隙没有了,注意是默认的,自己设置的不影响。*
而后者getMinimHeight这个方法又返回的是什么呢?看下面代码:
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
可以看出,如果intrinsicHeight > 0就返回intrinsicHeight,否则返回0,intrinsicHeight就是Drawable的原始高度,前提是这个Drawable有原始高度。那么什么时候会有原始高度呢?如:ShapeDrawable就没有原始宽/高,BitmapDrawable就有原始宽/高。
还有一点需要注意:getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPCIFIED情况下的测量宽/高。
二.ViewGroup的measure过程对于ViewGroup来说,除了完成自己的measure以外,还回去遍历所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此他没有重写View的onMeasure方法,但它提供了一个measureChildren方法:
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);
}
}
}
可以看出,ViewGroup在measure时,会对每一个元素进行measure,即上面的measureChild方法。如下实现:
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);
}
可以看出,它先取出了子元素的Layoutparams,之前说过的LayoutParams会影响MS的创建,然后通过getChildmeasureSpec来创建子元素的MeasureSpec,接着将MS直接传递给了View的measure方法。由于ViewGroup是一个抽象类,所以并没有定义其测量的具体过程,其测量过程的onMeasure需要各个子类去具体实现,比如LinearLayout、RelativeLayout这两者的布局显著不同,因此ViewGroup无法做统一实现。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上面代码很简单,根据LinearLayout的Orientation属性来调用不同的测量过程,比如竖直布局的大概逻辑:
//部分代码
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
从上面这段代码可以看出来,系统会遍历子元素并对子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法(它内部调用measureChildWithMargins方法,这个方法会调用measure方法),这样各个子元素就进入了measure过程,并且系统通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度,每测量一个子元素,mTotallLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,如下:
// Add in our padding
mTotalLength += mPaddingLeft + mPaddingRight;
int widthSize = mTotalLength;
// Check against our minimum width
widthSize = Math.max(widthSize, getSuggestedMinimumWidth());
// Reconcile our calculated size with the widthMeasureSpec
int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
上面代码可以看出,当子View测量完毕后,LinearLayout会根据子元素的情况来测量自己,对于竖直的LinearLayout来说,他在水平方向的测量遵守Veiw的测量,在竖直方向,如果他的布局中高度采用的是match_parent或者具体数值则遵守View的测量即精准模式,高度为specSize;但如果它的布局采用的格式warp_content,则它的高度就等于所有子元素所占用的高度总和及它在竖直方向的padding,但是仍然不能超过他的父容器的剩余空间。这个过程如下:
//这个size是 maxHeight/maxWidth,即最大高/宽
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Height方法就可以获取到正确的View的宽/高了。需要注意的是,在某些极端的情况下系统可能通过多次测量宽/高才能确定最终的测量宽/高,在这种情况下,onMeasure方法中获取的值可能不正确,一个比较好的习惯是在onLayout方法去获取View的测量宽/高或者最终宽/高。
一种情况:我们想在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽/高。
错误想法:
在onCreate、onResume里面获取某个View的宽/高事实上,在onCreate、onStart、onResume中均不能获取到正确的值,因为View的measure过程和Activity的生命周期不是同步的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了。
解决方法:
①Activity/View#onWindowChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了。注意:这个方法会被调用好多次,当Activity的窗口得到、失去焦点的时候均会被调用一次。即Acitvity继续执行或暂停执行都会调用这个方法。
典型代码:
public void onWindowFocusChanged(boolean hasFocus){
super.onWidowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasureWidth();
int height = view.getmeasureHeight();
}
}
②view.post(runnable)
通过可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。典型代码如下:
protected void onStart(){
super.onStart();
view.post(new Runnable(){
@Ovrride
public void run(){
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
});
}
③ViewTreeObserver使用ViewTreeObserver的众多回调完成,如使用OnGloballLayoutListener这个接口,当View树的状态发生改变或者View树内部的View可见性发生改变时,onGloballLayout方法会被回调,因此这是获取View的宽/高一个很好的时机。需要注意的是,伴随View树的状态改变等,onGloballLayout方法会被多次调用。
典型代码如下:
protected void onStart(){
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGloballLayoutListener(new OnGloballLayoutListener(){
@SuppreverWarnings("deprecation")
@Ovrride
public void onGloballLayout(){
view.getViewTreeObserver().removeGloballOnLayoutListener(this);
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
});
}
④view.measure(int widthMeasureSpec,int heightMeasureSpec)通过手动对View进行measure来得到View的宽/高,这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
Ⅰ. match_parent由于无法measure出具体的宽/高,所以不可用。根据上面的表图知道,当为match_parent时要知道父容器的剩余空间parentSize,而这个时间我们无法知道parentSize,故不可取。
Ⅱ.具体的数值(dp/px)比如宽/高都是100px,如下,measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
Ⅲ.wrap_content如下measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1<< 30) -1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeaasureSpec( (1<< 30) -1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
这里(1<< 30)-1就是MS的specSize的那30位二进制,其大小为2^30-1,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。关于view measure两种错误用法:
//第一种:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1,MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec,heightMeasureSpec);
//第二种
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
首先违背了系统内部的实现规范(无法通过错误的MS去得出合法的SpecMode,从而导致measure过程出错,对比正确用法),其次不能保证一定能measure出正确的结果。
layout过程
作用是ViewGroup来确定子元素的位置。当ViewGroup的位置被确定下来后,它在onLayout中会遍历所有的子元素并调用他们的layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定view本身的位置,onLayout会确定所有子元素的位置。
下面是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 oldT = 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);
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);
}
}
layout流程:首先会通过setFrame(setOpticalFrame是对子元素的)方法来设定Veiw的四个顶点的位置,即初始化mLeft、mRight、mTop和mBottom这四个值,Veiw的四个顶点一旦确定,那么Veiw在父容器的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现也与具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。我们看一下LinearLayout的onLayout方法:
@Override
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);
}
}
然后我们看竖直方向的:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else 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),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
可以看到,此方法会便利所有的子元素并调用setChildFrame方法(它仅调用了子元素的layout方法)来为子元素指定对应的位置,其中childTop会逐渐增大,就是说遍历过程中后面的元素会在前一个元素的下面,这符合LinearLayout竖直的属性。父容器在layout方法中完成自己的定位后,就通过onLayout方法去调用子元素的layout方法来确定子元素的位置,这样一层一层的传递下去,就完成了整个View树的layout过程。setChildFrame方法的实现如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
从上面可以得到,子元素的测量宽/高是width,height,从下面代码可以看出:
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft + getLocationOffset(child), childTop, childWidth, childHeight);
在layout方法中会通过setFrame去设置子元素的四个顶点的位置,在setFrame中有如下几个赋值语句,这样一来子元素的位置就确定了:
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
下面我们来回答一下之前我们的一个问题:View的测量宽/高和最终宽/高有什么区别?
这个问题可以具体为getMeasuredWidth和getWidth这两个方法有什么区别?
为什么说是这两种方法因为上面的setChildFrame方法用的宽就是getMeasuredWidth这个方法返回的值,而getWidth我们看源码就知道了。
我们来看一下getWidth的实现:
public final int getWidth(){
return mRight - mLeft;
}
我们知道mRight,mLeft是View的四顶点位置中的,所以这个方法返回的值刚好是View的宽度。
我们现在来回答一下这个问题:
在View默认实现中(也就是调用setChildFrame方法而这个方法中的参数又调用getMeasuredWidth作为child的width),View的测量宽/高和最终宽/高(getWidth)是相等的,只不过形成的时机不一样,前者在View的measure过程,后者在View的layout过程。我们日常开发中,可以认为这两者是相等的,但总是有一些特殊情况会导致两者不相等。
如:①重写View的layout,如下
public void layout(int l, int t, int r, int b){
super.layout(l,t,r+100,b+100);
}
这样会导致最终值比测量值的宽高总大100px。虽然无意义,但证明这两者可以不相等。
②在某些情况下,View会多次测量,测量值可能不同,最终宽/高和前几次测量值会不同,但最终测量值和最终宽/高相等。
draw过程
它的作用是将View绘制到屏幕上面。
View的绘制过程如下:
①绘制背景backgrund.draw(canvas)
②绘制自己(onDraw)
③绘制children(dispatchDraw)
④绘制装饰(onDrawScroBars)
这个步骤通过源码也可以看出来:
虽然比较长,但真的好好看不难理解。
@CallSuper
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;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 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) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// 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);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
View的绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历所有的子元素的draw方法,如此draw事件就这样一层层地传递下去了。
View有一个特殊方法setWillNotDraw,先看代码:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
从注释可以看出来 翻译:“如果该视图本身不进行任何绘图,则设置此标志以允许进一步优化。默认情况下,此标志不会在视图上设置,但可以在ViewGroup等视图子类上设置。通常,如果您重写{@link #onDraw(android.graphics.Canvas)},您应该清除这个标志”。
ViewGroup默认启用这个标志,这个标志对我们开发者的意义在于:当我们的自定义控件继承于ViewGroup并且其自身不具备绘制功能时,就开启这个标志位从而便于系统进行后期的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时我们要显示关闭这个标志位。