在Android的学习道路上,每一个人员都免不了去翻阅Android的源码,因为只有从源码的角度分析问题,我们才能真正的玩转Android开发。最近由于工作比较闲,总想着想写点什么东西,正好自己也可以整理一下。考虑到view的显示机制是自定义view的基础,也是面试中经常被问到的问题,所以记录此文,和大家共享,因水平有限,望大家踊跃拍砖,不胜感激。
有过自定义view的同行们都应该知道,view的显示依托于activity的setContentView方法依附到PhoneWindow窗体上的,在显示的过程中,这个view会经历测量(measure)、布局(layout)、draw(绘制)三个阶段, measure阶段就是得到每个View的大小,layout阶段就是计算每个View在UI上的坐标,draw阶段就是根据前面两个阶段的数据进行UI绘制。绘制完毕后方可被显示在界面上。
measure:
当我们要讲一个xml显示到ui上时,就是把layout的id传入到activity的setContentView中去,最终调用的是ViewRoot的performTraversals方法,此方法担任着view的绘制工作。
private void performTraversals() {
final View host = mView;
if (DBG) {
host.debug();
}
if (host == null || !mAdded)
return;
......
Rect frame = mWinFrame;
if (mFirst) {
fullRedrawNeeded = true;
mLayoutRequested = true;
DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
......
}
........
boolean insetsChanged = false;
if (mLayoutRequested) {
getRunQueue().executeActions(attachInfo.mHandler);
if (mFirst) {
host.fitSystemWindows(mAttachInfo.mContentInsets);
mAttachInfo.mInTouchMode = !mAddedTouchMode;
ensureTouchModeLocally(mAddedTouchMode);
} else {
if (!mAttachInfo.mContentInsets.equals(mPendingContentInsets)) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
host.fitSystemWindows(mAttachInfo.mContentInsets);
insetsChanged = true;
}
if (!mAttachInfo.mVisibleInsets.equals(mPendingVisibleInsets)) {
mAttachInfo.mVisibleInsets.set(mPendingVisibleInsets);
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowResizesToFitContent = true;
DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...........
}
在getRootMeasureSpec中传入的参数中有一个参数是lp的属性,而lp的定义是这样的:
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
WindowManager.LayoutParams lp = mWindowAttributes;
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
所以也就意味着lp.width和lp.height的值都是match_parent。
在getRootMeasureSpec方法中,根据windowSize和rootDimension返回测量规格。拿lp.width打比方,也就是decorView的width。如果width的值是MATCH_PARENT,那么返回的测量规格就是windowSize + MeasureSpec.EXACTLY的值,如果是WRAP_CONTENT则是
windowSize + MeasureSpec.AT_MOST的值。
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
至于上述提到的为何将windowSize和rootDimension映射出来的mode值相加,我们需要了解另外一个知识点,先看下面的代码:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
..........
}
MeasureSpec.makeMeasureSpec中只是简单的把size和mode进行相加然后返回。这里是因为这样的返回值可以表示出当前view的测量规格和测量大小,如AT_MOST、EXACTLY就是表示mode,他们都是占用2位并且都是int值,int值在java中是占用32位的,所以另外的30位在Android中被设计成了size。也就是说每个高2位表示specMode,而低30位表示尺寸的大小。
做完上一步操作后,就是开始调用host.measure操作来计算view的大小。而host代表的就是DecorView,decorView其实是个FrameLayout也就是个ViewGroup,但是在ViewGroup继承自View,而view的measure方法是不能被重写的,所以host.measure走的还是view的measure方法,在这个方法中调用了ViewGroup的 onMeasure(widthMeasureSpec, heightMeasureSpec)方法,所以我们移步到FrameLayout的onMeasure方法中
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
// Find rightmost and bottommost child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
}
}
// Account for padding too
maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
resolveSize(maxHeight, heightMeasureSpec));
}
这个方法里面调用了measureChildWithMargins方法,其实ViewGroup的子类在测量的时候都会走这个方法,最后还是会调用View类的measure方法进行测量,原型如下:
protected 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);
}
在view的measure方法中的两个参数分别对应宽和高的measureSpec,该参数是父视图传递给子视图的一个整型值,也就是说是父视图提供给子视图的测量规格,因为子视图最终占用的窗体大小是由父视图和子视图共同决定的。因此childWidthMeasureSpec是父视图传递给子视图的一个建议值。在测量大小的时候,padding和margin的值也作为大小的一部分。
上面描述的仅仅是view的测量过程。下面分析一下LinearLayout的测量过程:
在LinearLayout的测量中以下为主要代码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 如果是垂直方向的布局
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
// See how tall everyone is. Also remember max width.
// 遍历所有的子View,获取所有子View的总高度,并对每个子View进行measure操作
// 并且记录高度最高的子view
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
// //如果child 是Null,则mTotalLength加0
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
如果child不可见,则跳过
i += getChildrenSkipCount(child, i);
continue;
}
//拿到child的LayoutParams
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
//将weight的值加到totalWeight,weight的值就是xml文件中的layout_weight属性的值
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
/**
如果父View的mode是EXACTLY,并且height==0 并且lp.weight>0
那么就先不measure这个child,直接把topMargin和bottoMargin等属性加到totaoLength中
*/
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
int oldHeight = Integer.MIN_VALUE;
//如果父View不是EXACLTY,并且lp.height == 0 && lp.weight > 0,那么将子View的height变为WRAP_CONTENT
if (lp.height == 0 && lp.weight > 0) {
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
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 + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
.....................
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (useLargestChild && heightMode == MeasureSpec.AT_MOST) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// 给总高度加上自身的padding
mTotalLength += mPaddingTop + mPaddingBottom;
//将所有View的高度赋值给heightSize
int heightSize = mTotalLength;
// Check against our minimum height
// 最小的高度就是背景的高度mBGDrawable.getMinimumHeight()
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
//这里对heightSize再次赋值,不过如果LinearLayout是xml文件的根标签,并且设置到Activity的话
//此时heightSize的大小就是屏幕的高度,我们暂时就考虑等于屏幕高度的情况,其他情况类似
// Reconcile our calculated size with the heightMeasureSpec
// 如果heightMeasureSpec的size是精确的,那么这个heightsize值==heightMeasureSpec的size,也就是屏幕的高度
heightSize = resolveSize(heightSize, heightMeasureSpec);
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds
//屏幕的高度还剩下delta
int delta = heightSize - mTotalLength;
if (delta != 0 && totalWeight > 0.0f) {
//如果设置了weightsum属性,这weightSum等于weightsum的属性,否则等于totalWeight
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
// 计算方式:子view的weight属性值 * 剩余高度 / weight总和
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
// 重新设置子view的高度
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//所有的孩子View测量完毕,为自己设置大小
setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
在测量的过程中,会遍历出所有的子view,计算出所有子View的总高度和totalWeight,并对每个子View进行measure操作 。
在遍历子view的child过程中,如果child 是Null,则mTotalLength加0 ,如果child是不可见的,不做任何操作继续遍历下一个子view,然后拿到child的layoutParams,取出child的layout_weight,然后 totalWeight加上weight值。
接着判断如果父视图提供的高度mode等于MeasureSpec.EXACTLY,并且child的height等于0,并且child的weight的值大于0的时候,则先不测量这个child,因为有了weight,在把所有的child测量完毕后,再根据weight的值分配高度,但是在这里还是要把child的margin值加上去的,因为就算weight怎么变,margin值是不影响的, mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin)。这里取max的原因是child的margin可能为负数。
如果不满足上一个条件mode等于MeasureSpec.EXACTLY,就需要测量child,经历过测量之后可以知道child的高度,然后把这个高度加入到mTotalLength 中。在这个条件里面,如果child的height等于0,并且child的weight的值大于0,会把当前view的height设置成LayoutParams.WRAP_CONTENT,然后去测量。
到目前为止只是测量出了总的高度,还没有按照weight分配高度,哈哈,上面说了这么多可能有读者快要晕了,没事,说完weight的分配,我会集合源码举个例子给大家讲述(见下一篇文章)。
紧接着给mTotalLength加上父视图的paddingTop和paddingBottom值,得出heightSize。但是每个view都是有背景的,背景是有个高度的,所以要比较heightSize和getSuggestedMinimumHeight()的值的大小,取最大值作为heightSize,getSuggestedMinimumHeight()返回的mBGDrawable.getMinimumHeight()也就是drawable背景的最小高度。接着通过resolveSize再次的给heightSize赋值,因为如果父视图的heightMeasureSpec的mode是精确的话,那么这个heightSize的最终值也就应该是父视图指定的大小。
接下来就开始按照weight分配高度了,在分配高度之前要保证余下的高度heightSize - mTotalLength 不能为0并且totalWeight要大于0。然后遍历子view,得到子view的weight,如果weight大于0,通过公式:child的weight属性值 * 剩余高度 / weight总和计算出child要从剩余的高度里面获得多少的高度值,然后刷新剩余的高度值,给child重新设置高度并进行测量。
最后所有的孩子View测量完毕,调用setMeasuredDimension为自己设置大小。