在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为自己设置大小。
版权声明:本文为博主原创文章,未经博主允许不得转载(联系方式:QQ312037487 邮箱:[email protected])。