View的工作原理
View的主要工作流程包括measure,layout,draw。一个View如果要显示在界面上,首先需要通过measure方法调用onMeasure方法对View进行测量,在onMeasure方法用又会对所有子元素进行measure过程,这样就将measure流程传递到子元素中,子元素再一级一级往下传递,最终完成所有元素的测量。然后会通过layout方法调用onLayout方法对View的位置进行布局,过程同onMeasure过程相似,最后通过dispatchDraw进行绘制的分发,通过onDraw方法完成View的绘制。但是measure方法和layout方法都是final类型的方法,子类不能进行重写。
measure过程决定了View的宽高,我们可以通过getMeasureWidth和getMeasureHeight方法获取测量的View的宽高值。layout过程决定了View各个定点的坐标和实际的View的宽高,我们可以通过getTop,getLeft,getRight,getBottom方法获取View的位置,通过getWidth和getHeight方法获取View的实际宽高。draw过程决定了View的显示,只有完成draw过程View才能显示在界面上。顺序:measure>>layout>>draw。
DecorView是顶层的View,一般情况下他的内部会包含一个垂直的LinearLayout,该LinearLayout包含了TitleBar和ContentView两部分内容,我们在Activity中通过setContentView方法设置的View就包含在该contentView中。contentView的id为android.R.id.content,那么我们就可以通过该id找到我们加载到界面的View,如下所示:
//获取contentview
ViewGroup content = (ViewGroup) findViewById(android.R.id.content);
//contentview的子元素就是我们设置的View
View view = content.getChildAt(0);
MeasureSpec
View的测量过程受到父容器LayoutParams施加的规则(wrap_content,match_parent)的影响。在测量的过程中,系统会将View的layoutParams根据父容器的规则转化成对应的MeasureSpec,然后再根据这个MeasureSpec测量出View的宽高。也就是说,父容器和子容器的LayoutParams共同影响了View的测量结果。
MeasureSpec是View的一个内部类,主要由SpecMode和SpecSize两部分组成,MeasureSpec通过将两个值进行打包成一个int值控制View的测量。SpecMode有三类,UNSPECIFIED,EXACTLY,AT_MOST。
UNSPECIFIED:指父容器不对View有任何限制,要多大给多大。
EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的大小就是SpecSize所指定的值,一般对应于match_parent和具体的数值这两种模式。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体数值需要根据View模式再具体确定。
首先我们看一下ViewGroup的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);
}
该方法中对子元素进行measure,但是在measure之前,会将父元素的measureSpec与值和padding,margin进行分析通过getChildMeasureSpec来生成子元素的MeasureSpec。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
可以看到首先获取父元素的SpecMode和SpecSize,然后判断子元素的LayoutParams设置的值,最终生成子元素的MeasureSpec。
总结下来就是:当View采用固定宽高的时候,无论父容器是什么模式,View的MeasureSpec都是EXACTLY,大小为固定的值;当View的宽高都是match_parent的时候,如果父容器是EXACTLY,那么View也是EXACTLY,大小为父容器的剩余空间;如果父容器是AT_MOST,那么View也是AT_MOST,大小不会超过父容器的剩余空间。当View的宽高是wrap_content,不管父容器的模式是什么,View的模式总是AT_MOST,且大小不能超过父容器的剩余空间。
measure过程
View的measure
View的measure过程通过measure方法进行,在measure会调用onMeasure进行测量,因为measure方法是final类型的,子类中不能进行重写,只需看onMeasure方法是如何实现的即可。
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:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
首先通过getSuggestedMinimumWidth,看View是否有背景,如果没有背景,则返回mMinWidth,该值对应的就是布局文件中android:minWidth设置的值,如果没有设置该值,则为0,如果设置了背景图片,则通过mBackground.getMinimumWidth()获取图片的真实宽度,然后与mMinWidth进行比较取其中的较大值作为getDefaultSize中的size输入。
getDefaultSize会根据measureSpec中的specMode进行计算返回View的测量后的大小。
通过getDefaultSize方法的实现来看,View的宽高由specSize决定,如果我们自定义控件继承自View,在布局中无论使用wrap_content或者是match_parent都相当于使用match_parent。因为View在布局中如果使用wrap_content,那么他的SpecMode为AT_MOST,根据之前MeasureSpec章节的分析,此时他的specSize都为父容器的size,那么如果我们想要给子容器设置一个wrap_content时有一个默认的宽高,就需要在onMeasure方法中对宽高的specMode分别进行判断,当specMode为AT_MOST的时候,设置一个固定的宽高即可,如果宽高中只有一个值为wrap_content,另一个值在setMeasuredDimension传递默认的specSize即可。代码比较简单,就不给出。
ViewGroup的measure
ViewGroup与View不同的是,它不仅需要对自身进行测量,同时需要对所有的子元素进行测量。ViewGroup并没有重写onMeasure方法,因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,例如LinearLayout和RelativeLayout,两者的布局特性显然不同,因此ViewGroup无法做统一的实现。
虽然ViewGroup没有重写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);
}
}
}
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);
}
我们可以看到,在对子元素进行测量的时候,会获取所有的子元素,然后对每一个执行measureChild方法,在measureChild方法中取出每一个child的LayoutParams,然后再通过getChildMeasureSpec方法创建子元素的MeasureSpec,然后将MeasureSpec传递给子元素进行测量。
然后我们来看一下LinearLayout的具体实现:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这是LinearLayout的测量过程,分为垂直布局和水平布局两种的测量方法,我们就看一下垂直布局的,水平布局同理。我们只看其中比较关键的代码:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
.......
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
..........
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight +topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
}
我们可以看到,系统会遍历所有子元素,调用measureChildBeforeLayout方法,对子元素进行测量,并且会获取子元素的高度,同时使用mTotalLength来记录当前总的高度,没测量一个,将其高度加到mTotalLength上,这样就能获取所有子元素的高度只和。然后LinearLayout会对自身进行测量。
系统会对heightMode,及垂直方向的模式进行判断,如果为EXACTLY,即match_parent或者是固定高度,那么他的测量过程和View的测量过程相同,如果为AT_MOST,即wrap_content,那么他的高度为所有子元素的高度之和,但是仍然不能超过父容器的剩余空间。同时高度还需要考虑padding和margin的值。
获取View的测量值
很多人会遇到这样一个问题,如果我们需要在Activity中获取某个View的宽和高,但是发现在Activity的onCreate或者onResume里通过view.getWidth()或者getMeasuredWidth()方法获取到的值都为0;这是因为在onCreate的时候,view的measure方法并没有执行完毕,因此此时获取到的测量数据为0。那么应该如何获取view的宽高值呢。
方法1:
重写onWindowFocusChanged,该方法会在activity获取或者失去焦点的时候被调用,也就是activity在对用户可见或者不可见的时候被调用。当activity可见的时候,预示着各个view已经初始化完毕,已经绘制在activity上了,所以此时可以正常获取view的宽高值。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
tv.setText("width:" + tv.getWidth() + "\nmeasureWidth:" + tv.getMeasuredWidth());
}
}
方法2:
通过post可以将一个runnable投递到消息队列的尾部,等待Looper调用次runnable的时候,view也已经初始化好了。
tv.post(new Runnable() {
@Override
public void run() {
tv.setText("width:" + tv.getWidth() + "\nmeasureWidth:" + tv.getMeasuredWidth());
}
});
方法3:
使用ViewTreeObserver的众多回调可以完成这个功能,例如使用OnGlobalLayoutListener,当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法会被调用。
ViewTreeObserver observer = tv.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
tv.getViewTreeObserver().removeOnGlobalLayoutListener();
tv.setText("width:" + tv.getWidth() + "\nmeasureWidth:" +getMeasuredWidth());
}
});
layout过程
layout的作用是ViewGroup确定子元素的位置,当ViewGroup的位置确定后,他在onLayout中会比遍历所有的子元素并调用layout方法,在layout中又会调用onLayout方法,同measure的过程一样。不同的是layout方法用来确定View本身的位置,而onLayout方法用来确定子元素的位置。view中可以重写layout方法,viewGroup不能重写layout方法。
首先我们看一下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);
}
}
同样的,包括垂直方向和水平方向的layout过程,我们看一下垂直方向的:
void layoutVertical(int left, int top, int right, int bottom) {
.........
final int count = getVirtualChildCount();
.........
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();
........
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);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
首先会获取子元素的个数,然后对所有子元素进行遍历,获取子元素的宽高值,并同时用childTop保存之前所有child的总高度,那么当前child所处的高度就应该是childTop的值,然后通过setChildFrame方法调用子元素的layout方法来让每一个子元素确定(知道)自己的位置。在这个过程中需要考虑padding,margin以及对齐方式,以此确定child四个顶点确定的位置,通过setChildFrame传递过去。其中width为right-left的值,height为bottom-top的值。
然后我们看一下child的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);
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;
}
可以看到在layout方法中,获取传递过来的四个位置的值,通过setFrame方法设置四个顶点的位置。
通过上边的步骤,子元素每一个的位置都已经确定,只需通过draw过程就可以将其显示在界面上了。
draw过程
draw的过程比较简单,它只是将我们测量好的和定好位置的view绘制出来,通常会按照如下几步进行绘制;
1.绘制背景(background.draw)
2.绘制自己(onDraw)
3.绘制children(dispatchDraw)
4.绘制装饰(onDrawScrollBars)
当绘制子元素的时候,是通过dispatchDraw进行传递(分发)的,dispatchDraw会遍历所有的子元素的draw方法,然后一层一层传递下去。
view有一个特殊的方法,setWillNotDraw(),当view不需要绘制任何内容的时候,那么设置该标记为true的时候系统会进行相应的优化。默认情况下,view没有启用这个标记为,但是ViewGroup会默认启用这个优化标记位,因此在我们自定义控件继承自ViewGroup且自身不具备绘制功能的时候,可以开启这个标记位。如果重写了onDraw方法,那么就需要关闭这个标记位。