请转至我的CSDN博客查看 --------------------------------------------------------------------------- 从Android自定义View学习系列计划开始,到现在也有挺长时间了,上一篇Android自定义View学习教程01之常用工具源码分析讲到了Android开发过程中常使用到的一些工具,都从源码上进行了分析,让大家对要使用的工具都有了一定的了解。 我们都知道,自定义View的三个重要过程分别是measure、layout、draw,而measure处于这条处理链的首端,自然是非常重要的。所以接下来的这一篇Android自定义View学习教程02之onMeasure()的源码分析及重写我们就来看一看自定义View里经常用到的onMeasure()方法的源码分析以及它们的重写!! --------------------------------------------------------------------------- 系统显示一个View,首先需要通过测量(measure)该View来获得其长和宽从而确定显示该View时需要多大的空间。而在测量的过程中MeasureSpec贯穿全程,发挥着不可或缺的作用。 所以,了解View的测量过程,最合适的切入点就是MeasureSpec。 MeasureSpec 我们来看一下Google官方文档对MeasureSpec的描述
A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.大概意思就是,MeasureSpec封装了从父节点传递给子节点的布局要求。每个MeasureSpec表示宽度或高度的要求。度量规格包括size(大小)和mode(模式)。 这句话里透漏了几个重点:
- MeasureSpec封装了从父节点传递给子节点的布局要求
- MeasureSpec可以表示宽度和高度
- MeasureSpec由size和mode组成
```int specMode = MeasureSpec.getMode(measureSpec);
```获取大小(size)
```int specSize = MeasureSpec.getSize(measureSpec);```还可以通过上面的两个值生成新的MeasureSpec
```int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);```那么所谓从父节点传递给子节点的布局要求是什么呢?MeasureSpec里有着三种模式(mode),我们来看一下:
UNSPECIFIED
The parent has not imposed any constraint on the child. It can be whatever size it wants.父View没有对子View施加任何约束。它可以是任何它想要的大小。
EXACTLY
The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.父View已确定子View的确切大小。子View将被给予父View所确定的大小,无论它想要多大。 AT_MOST
The child can be as large as it wants up to the specified size.父View不限制View的大小,子View可以得到它想要达到的大小。 好了,接下来让我们从源码角度来分析一下它们是怎么形成的:
```/** * @param child * 子View * @param parentWidthMeasureSpec * 父容器(比如LinearLayout)的宽的MeasureSpec * @param widthUsed * 父容器(比如LinearLayout)在水平方向已经占用的空间大小 * @param parentHeightMeasureSpec * 父容器(比如LinearLayout)的高的MeasureSpec * @param heightUsed * 父容器(比如LinearLayout)在垂直方向已经占用的空间大小 */ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (ViewGroup.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传进来的参数却包含了父容器的宽的MeasureSpec,父容器在水平方向已经占用的空间大小,父容器的高的MeasureSpec,父容器在垂直方向已经占用的空间大小等父View相关的信息。这在一定程度体现了:父View影响着子View的MeasureSpec的生成。 该方法主要有四步操作:
- 得到子View的LayoutParams
- 得到子View的宽的MeasureSpec
- 得到子View的高的MeasureSpec
- 测量子View
``` public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = View.MeasureSpec.getMode(spec); int specSize = View.MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { case View.MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } break; case View.MeasureSpec.AT_MOST: if (childDimension >= 0) { resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } break; case View.MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = View.MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = View.MeasureSpec.UNSPECIFIED; } break; } return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode); }```应该可以看出来这就是确定子View的MeasureSpec的具体实现了吧!! 接下来我们对源码进行分析 首先来看一下getChildMeasureSpec()方法的内部参数
- spec
父View(比如LinearLayout)的宽或高的MeasureSpec
- padding
父容器(比如LinearLayout)在垂直方向或者水平方向已经被占用的空间。为什么这么说呢? 我们可以看measureChildWithMargins()方法里调用getChildMeasureSpec()的这一部分,传递给getChildMeasureSpec()的第二个参数构成如下: 例如 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 其中
- mPaddingLeft和mPaddingRight表示父容器左右两内侧的padding
- lp.leftMargin和lp.rightMargin表示子View左右两外侧的margin
- childDimension
通过子View的LayoutParams获取到的子View的宽或高从getChildMeasureSpec()方法的第一个参数spec和第二个参数padding也可以看出,父容器(如LinearLayout)的MeasureSpec和子View的LayoutParams共同决定着子View的MeasureSpec!! 好,了解了方法的参数后我们来分析一下getChildMeasureSpec()方法的具体实现步骤
- 得到父容器的specMode和specSize
- 得到父容器在水平方向或垂直方向可用的最大空间值
- 确定子View的specMode和specSize
- if (childDimension >= 0)
- else if (childDimension == LayoutParams.MATCH_PARENT)
- else if (childDimension == LayoutParams.WRAP_CONTENT)
- LayoutParams.MATCH_PARENT=-1
- LayoutParams.WRAP_CONTENT=-2
```if (childDimension >= 0) ```
所以上面这一句代码的意思表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。 此时子View的size就是childDimension,子View的mode也为MeasureSpec.EXACTLY,即:
```resultSize = childDimension; resultMode = MeasureSpec.EXACTLY;```
```else if (childDimension == LayoutParams.MATCH_PARENT)```这一句代码表示子View的宽或高是LayoutParams.MATCH_PARENT。 此时子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode也为MeasureSpec.EXACTLY,即:
```resultSize = size; resultMode = MeasureSpec.EXACTLY;``` ```else if (childDimension == LayoutParams.WRAP_CONTENT)```这一句表示子View的宽或高是LayoutParams.WRAP_CONTENT。 此时子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode为MeasureSpec.AT_MOST,即:
```resultSize = size; resultMode = MeasureSpec.AT_MOST;```另外两种模式传入时父容器的specMode不同,同样对应三种情况以及不同的size和mode,就不再讲解了,大家对着源码看就知道了。 到这一步,我们已经可以清楚的知道 子View的MeasureSpec由其父容器的MeasureSpec和该子View本身的布局参数LayoutParams共同决定。 经过测量得出的子View的MeasureSpec是系统给出的一个期望值(参考值),我们也可摒弃系统的这个测量流程,直接调用setMeasuredDimension( )设置子View的宽和高的测量值。 --------------------------------------------------------------------------- onMeasure()方法源码分析 到这里终于把MeasureSpec搞懂了,那么接下来就可以开始看我们的主角了!! 我们来看一下View的onMeasure()方法的源码
```protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } ```onMeasure( )源码流程如下:
- (1) 在onMeasure调用setMeasuredDimension( )设置View的宽和高
- (2)在setMeasuredDimension()中调用getDefaultSize()获取View的宽和高
- (3)在getDefaultSize()方法中会调用到getSuggestedMinimumWidth()或者getSuggestedMinimumHeight()获取到View宽和高的最小值
```//Returns the suggested minimum width that the view should use protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } ```
```//Returns the suggested minimum height that the view should use protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); } ```这两个方法分别返回View的宽度和高度的最小值MinimumWidth、MinimumHeight 在此需要注意该View是否有背景 (1) 若该View没有背景
那么该MinimumWidth为View本身的最小宽度即mMinWidth
有两种方法可以设置该mMinWidth值:
- 在XML布局文件中定义minWidth
- 调用View的setMinimumWidth()方法为该值赋值
那么该MinimumWidth为View本身最小宽度mMinWidth和View背景的最小宽度的最大值
getSuggestedMinimumHeight()方法与getSuggestedMinimumWidth()类似,不再多说
---------------------------------------------------------------------------
然后是getDefaultSize(),我们来看一下它的源码
```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; } ```该方法用于获取View的宽或者高的大小 该方法的第一个输入参数size就是通过调用getSuggestedMinimumWidth()方法获得的View的宽或高的最小值 从getDefaultSize()的源码里的switch语句可看出该方法的返回值有两种情况: (1) measureSpec的specMode为MeasureSpec.UNSPECIFIED 时 在此情况下该方法的返回值就是View的宽或者高最小值 这种情况很少见,可忽略不计 (2) measureSpec的specMode为MeasureSpec.AT_MOST或MeasureSpec.EXACTLY 时: 在此情况下getDefaultSize()的返回值就是该子View的measureSpec中的specSize 除去第一种情况以外,可知,在measure阶段View的宽和高由其measureSpec中的specSize决定!! 如果子View在XML布局文件中对于大小的设置采用wrap_content,那么不管父View的specMode是 MeasureSpec.AT_MOST还是MeasureSpec.EXACTLY 对于子View而言系统给它设置的specMode都是MeasureSpec.AT_MOST,并且其大小都是parentLeftSize即父View目前剩余的可用空间。这时wrap_content就失去了原本的意义,变成了match_parent一样了。 所以自定义View在重写onMeasure()的过程中应该手动处理View的宽或高为wrap_content的情况 --------------------------------------------------------------------------- 最后是setMeasuredDimension(),同样的,我们来看一下它的源码
```protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; }```经过调用前面的各种方法,终于得到了View的宽度和高度 在这里就可以通过setMeasuredDimension()方法来设置View的宽度和高度的测量值 --------------------------------------------------------------------------- 重写onMeasure()方法 上面说到,当子View在XML布局文件设置的宽或高为wrap_content时,系统给子View设置的specMode都是MeasureSpec.AT_MOST,这时wrap_content就失去了原本的意义,变成了match_parent一样。所以此时我们需要重写onMeasure来处理这种情况。 那么我们该怎么对这两种情况进行处理呢? 第一种情况: 如果在xml布局中View的宽和高均用wrap_content,那么需要设置View的宽和高为mWidth和mHeight 第二种情况: 如果在xml布局中View的只是宽或高其中一个为wrap_content,那么就将该值设置为默认的宽或高,另外的一个值采用系统测量的specSize即可 具体的实现可以这样做:
```protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec , heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpceSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec); int heightSpceSize=MeasureSpec.getSize(heightMeasureSpec); if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth, mHeight); }else if(widthSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth, heightSpceSize); }else if(heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpceSize, mHeight); } } ```上面的代码主要分两步完成
- 调用super.onMeasure()
- 处理View的大小为wrap_content时的情况
```public abstract class ViewGroup extends View implements ViewParent,ViewManager{ }```我们可以看到ViewGroup是一个抽象类,它没有重写View的onMeasure( )但它提供了measureChildren( )测量所有的子View。在measureChildren()方法中调用measureChild( )测量每个子View,在该方法内又会调用到child.measure( )方法,这又回到了前面熟悉的流程。 即ViewGroup中测量子View的流程: 请转至我的CSDN博客查看 既然ViewGroup继承自View而且它是一个抽象类,那么ViewGroup的子类就应该根据自身的要求和特点重写onMeasure( )方法。 那么这些ViewGroup的子类会怎么测量子View和确定自身的大小呢? 如果ViewGroup知道了每个子View的大小,将它们累加起来是不是就知道了自身的大小呢? 顺着这个思路,我们选择ViewGroup的子类LinearLayout来看,就从它的onMeasure( )开始看吧!!
``` @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
```我们可以看到,代码里分了水平线性和垂直线性这两种情况进行测量,类似的逻辑在其onLayout( )阶段也会看到。我们就选measureVertical( )继续往下看
``` void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; int maxWidth = 0; int childState = 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; boolean skippedMeasure = false; final int baselineChildIndex = mBaselineAlignedChildIndex; final boolean useLargestChild = mUseLargestChild; int largestChildHeight = Integer.MIN_VALUE; 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; } if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { int oldHeight = Integer.MIN_VALUE; 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; } if (i < baselineChildIndex && lp.weight > 0) { throw new RuntimeException("Exception"); } boolean matchWidthLocally = false; if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) { 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) { weightedMaxWidth = Math.max(weightedMaxWidth, matchWidthLocally ? margin : measuredWidth); } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); } i += getChildrenSkipCount(child, i); } if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerHeight; } if (useLargestChild && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) { 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(); final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + largestChildHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } } mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); heightSize = heightSizeAndState & MEASURED_SIZE_MASK; int delta = heightSize - mTotalLength; if (skippedMeasure || delta != 0 && totalWeight > 0.0f) { 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) { int share = (int) (childExtra * delta / weightSum); weightSum -= childExtra; delta -= share; final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width); if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { int childHeight = child.getMeasuredHeight() + share; if (childHeight < 0) { childHeight = 0; } child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); } else { child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, MeasureSpec.EXACTLY)); } childState = combineMeasuredStates(childState, child.getMeasuredState() & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)); } 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)); } mTotalLength += mPaddingTop + mPaddingBottom; } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, weightedMaxWidth); if (useLargestChild && heightMode != MeasureSpec.EXACTLY) { for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null || child.getVisibility() == View.GONE) { continue; } final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); float childExtra = lp.weight; if (childExtra > 0) { child.measure( MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(largestChildHeight, MeasureSpec.EXACTLY)); } } } } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { maxWidth = alternativeMaxWidth; } maxWidth += mPaddingLeft + mPaddingRight; maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState); if (matchWidth) { forceUniformWidth(count, heightMeasureSpec); } }```代码有点多,但没关系,我们来分析一下其中的关键点,这段代码的主要操作其实就只有下面两点
- 遍历每个子View,并对每个子View调用measureChildBeforeLayout(),代码115-133行
- 设置LinearLayout的大小,代码241行
没有评论:
发表评论