《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683
《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code
文章目录
View树的测量流程
小故事:
公司计划搞团建,大狗,二狗,三狗是三个部门的领导,他们跑到财务室去跟财务妹子要经费。
财务妹子:你们各自要多少经费,提交申请给我就好了
大狗比较实诚,提交了500块的预算申请,财务妹子二话不说,直接给了大狗500块,大狗拿着500块就跟部门的人一起去吃酸辣粉了。
二狗又精又胆小,即怕要多了公司不高兴,又怕要少了底下的兄弟们不高兴。于是二狗跟财务妹子提了申请,写明:“按公司预算给经费!”,又是财务妹子也不兜着,把老板交代的单部门预算5000元都给了二狗,二狗高高兴兴的带着部门的人先去做了个大保健,又吃了个沙县小吃
三狗是公司得力干将,对地下兄弟们也比较好,胆子比较大,三狗给财务妹子提了个申请:“我也不知道兄弟们要多少经费,你先把公司预算给我,不够再说”,于是,财务妹子把5000块的预算给了三狗,三狗开开心心带着兄弟们又是大保健,又是海底捞,眼看预算都快花完了,这时候底下有个兄弟说,今天挺开心的,我们去K个歌吧,这时候三狗直接给财务妹子打了个电话:“经费不够用,再给我转一点”,财务妹子也没有办法,又给三狗转了1000块经费,三狗底下的兄弟是真能喝,没一会儿就把1000块经费干完了,三狗再次给财务打了电话,要求加经费,财务妹子又给三狗转了1000块钱,并回复三狗说:“这是公司最后底线,人要知足啊!”,三狗也知好歹,回复说道:“不会再要了”。
这个故事讲的就是View的三种测量模式以及ViewRoot的尺寸问题。
先了解下视图的加载流程
ViewManager->WindowManager->WindowManagerImpl-> WindowManagerGlobal -> ViewRootImpl
public interface ViewManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
//WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
//最终通过ViewRootImpl来发起view的测量、布局、绘图
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
//将view设置给ViewRootImpl
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
//ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
}
}
}
上面的源码大家可以去看看,我们这里只要明白,我们的根view最终会被设置在ViewRootImpl对象中,然后在ViewRootImpl中实现测量、布局、绘图。
我们知道,在自定义View时,通过重写onMeasure()方法来测量计算自己需要的尺寸:
//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
通过setMeasuredDimension()方法就可以定义自己View的尺寸,比如,我定义我们的自定义View为100x100尺寸大小:
setMeasuredDimension(100,100)
widthMeasureSpec和heightMeasureSpec
两个是对测量模式和尺寸的封装,比如我们在xml布局文件中定义的layou_width和layout_height
来看个例子:
我们自定义了一个ChildView并设置它的宽高为50px
<cn.code.code.wiget.ChildView
android:layout_width="50px"
android:layout_height="50px"
android:background="#ff2323" />
然后我们重写onMeasure方法并获取widthMeasureSpec、heightMeasureSpec的模式和size:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Logger.e("MeasureSpec.UNSPECIFIED="+MeasureSpec.UNSPECIFIED);
Logger.e("MeasureSpec.EXACTLY="+MeasureSpec.EXACTLY);
Logger.e("MeasureSpec.AT_MOST="+MeasureSpec.AT_MOST);
Logger.e(getClass().getSimpleName()+": onMeasure widthMode="+(widthMode)+" widthSize="+widthSize);
Logger.e(getClass().getSimpleName()+": onMeasure heightMode="+(heightMode)+" heightSize="+heightSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
日志输出:
05-21 15:20:46.950 4481-4481/? E/glacat: MeasureSpec.UNSPECIFIED=0
05-21 15:20:46.950 4481-4481/? E/glacat: MeasureSpec.EXACTLY=1073741824
05-21 15:20:46.950 4481-4481/? E/glacat: MeasureSpec.AT_MOST=-2147483648
05-21 15:20:46.950 4481-4481/? E/glacat: ChildView: onMeasure widthMode=1073741824 widthSize=50
05-21 15:20:46.950 4481-4481/? E/glacat: ChildView: onMeasure heightMode=1073741824 heightSize=50
可以看到,widthMeasureSpec和heightMeasureSpec就是我们在xml文件中设置的宽高属性。
我们的测量模式是EXACTLY,size=50px,说明layout_width/height给定确定数值时,它的测量模式是EXACTLY(跟大狗一样,明确知道自己要多少经费)
三种测量模式的理解:
- UNSPECIFIED: Parent对Child没有尺寸约束,Child想要多大就给多大(这个我们一般不会用到)
- EXACTLY: 代表尺寸是明确的,确定的数值 (match_parent和确定的数值)
- AT_MOST: 代表Child也不知道自己需要多大尺寸,但是最大不会超过Parent的尺寸(wrap_content)
我们再来验证下我们的match_parent和wrap_content的模式和size
<cn.code.code.wiget.ChildView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff2323" />
onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Logger.e(getClass().getSimpleName()+": onMeasure widthMode="+(widthMode>>30)+" widthSize="+widthSize);
Logger.e(getClass().getSimpleName()+": onMeasure heightMode="+(heightMode>>30)+" heightSize="+heightSize);
//默认采用widthMeasureSpec和heightMeasureSpec的宽高
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
日志输出:
05-21 15:26:29.534 4581-4581/? E/glacat: MeasureSpec.UNSPECIFIED=0
05-21 15:26:29.534 4581-4581/? E/glacat: MeasureSpec.EXACTLY=1073741824
05-21 15:26:29.534 4581-4581/? E/glacat: MeasureSpec.AT_MOST=-2147483648
05-21 15:26:29.534 4581-4581/? E/glacat: ChildView: onMeasure widthMode=1073741824 widthSize=720
05-21 15:26:29.534 4581-4581/? E/glacat: ChildView: onMeasure heightMode=-2147483648 heightSize=1158
- match_parent的mode=EXACTLY,size=屏幕宽度=720(跟二狗一样,预算多少就用多少)
- wrapcont_parent的mode=AT_MOST ,size=屏幕高度=1158(跟三狗一样,不知道自己需要多少经费,先拿下全部预算经费再说)
那么对于View来说,它的尺寸是怎么确定的,是由谁决定的?是layout_width和layout_height吗?不一定!
例子:
在xml布局中,设置ChildView的宽为match_parent,高为20dp
<cn.code.code.wiget.ChildView
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="#ff2323" />
在onMeasure中,设置ChildView的宽高都为100
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Logger.e("MeasureSpec.UNSPECIFIED="+MeasureSpec.UNSPECIFIED);
Logger.e("MeasureSpec.EXACTLY="+MeasureSpec.EXACTLY);
Logger.e("MeasureSpec.AT_MOST="+MeasureSpec.AT_MOST);
Logger.e(getClass().getSimpleName()+": onMeasure widthMode="+(widthMode)+" widthSize="+widthSize);
Logger.e(getClass().getSimpleName()+": onMeasure heightMode="+(heightMode)+" heightSize="+heightSize);
//设置尺寸为100x100
setMeasuredDimension(100,100);
}
效果:
日志输出:
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: MeasureSpec.UNSPECIFIED=0
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: MeasureSpec.EXACTLY=1073741824
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: MeasureSpec.AT_MOST=-2147483648
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: ChildView: onMeasure widthMode=1073741824 widthSize=720
06-18 16:24:39.596 5726-5726/cn.code.code E/BUG: ChildView: onMeasure heightMode=1073741824 heightSize=30
widthSize=720,heightSize=30就是我们在布局文件中定义的宽高
但是最终,我们并没有把这个宽高设置给ChildView,而是通过
setMeasuredDimension(100,100);
把ChildView的宽高设置为了100x100
结论:View的宽高是由setMeasuredDimension()方法决定的!
也就是说widthMeasureSpec、heightMeasureSpec只是个建议,我们完全可以不接受并根据自己的实际情况来决定需要多大的尺寸
那么是谁调用了我们ChildView的onMeasure()方法,并把ChildView的布局参数传给它的呢?整个测量流程怎么实现的呢,我们再回到我们的ViewRootImpl类中看看
View树的测量流程:
View树的测量是从ViewRoolImpl对象中的mView(树根)开始的,我们来观察下mView是如何测量自己的大小的
ViewRootImpl.java
//ViewRootImpl.java
//开始线程,开始处理View树的 测量、布局、绘图
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
//开始处理View树的 测量、布局、绘图
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
ViewRootImpl.java
//ViewRootImpl.java
private void performTraversals() {
final View host = mView;
......
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
//根rootView的LayoutParams
// public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
WindowManager.LayoutParams lp = mWindowAttributes;
//rootView期望的尺寸,为屏幕尺寸大小
int desiredWindowWidth;
int desiredWindowHeight;
if (mFirst) {
// make sure touch mode code executes by setting cached value
// to opposite of the added touch mode.
mAttachInfo.mInTouchMode = !mAddedTouchMode;
ensureTouchModeLocally(mAddedTouchMode);
} else {
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
//获取屏幕的大小
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
//获取屏幕的大小
Configuration config = res.getConfiguration();
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
}
}
// Ask host how big it wants to be
//开始测量View树
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
......
if (mApplyInsetsRequested) {
mApplyInsetsRequested = false;
mLastOverscanRequested = mAttachInfo.mOverscanRequested;
dispatchApplyInsets(host);
if (mLayoutRequested) {
//开始测量View树
windowSizeMayChange |= measureHierarchy(host, lp,
mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
}
}
......
//开始布局View树
performLayout(lp, mWidth, mHeight);
......
//开始Draw View树
performDraw();
......
}
ViewRootImpl.java
//ViewRootImpl
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
//传给mView的模式和尺寸
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
//是否完成了mView的测量(标记是否完成测量)
boolean goodMeasure = false;
//lp.width=wrap_content(AT_MOST测量模式) (不知道mView需要多少尺寸) (三狗的流程,先拿预算经费,不够再说)
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
//获取Android配置的默认root View的尺寸大小(公司给大狗、二狗、三狗的团建经费预算)
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
//初始建议大小(公司给大狗、二狗、三狗的团建经费预算)
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int) mTmpValue.getDimension(packageMetrics);
}
//baseSize小于屏幕宽度(财务的经费预算baseSize不是公司的底线,公司经费底线是desiredWindowWidth)
if (baseSize != 0 && desiredWindowWidth > baseSize) {
//封装mView宽高模式和尺寸 MeasureSpec (方法在后面给出了)
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//开始测量mView,(第一次测量)(方法在后面给出了)
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//测量完成,获取mView的测量状态,MEASURED_STATE_TOO_SMALL(给的初始baseSize太小,不够mView使用)
//如果mView(host)觉得baseSize够用
if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
//测量结束
goodMeasure = true;
} else { //给的baseSize不够用(此时三狗第一次打电话给财务妹子说经费不够用,需要加钱)
//(desiredWindowWith+baseSize)/2 > baseSize (给三狗的经费增加了1000)
baseSize = (baseSize + desiredWindowWidth) / 2;
//用新的baseSize重新封装mView的宽度测量模式
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//开始测量mView,(第二次测量)(方法在后面给出了)
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果mView(host)觉得新的baseSize够用
if ((host.getMeasuredWidthAndState() & View.MEASURED_STATE_TOO_SMALL) == 0) {
//测量结束
goodMeasure = true;
}
}
}
}
//情况一:lp.width=wrap_content(上面还没有测量结束,baseSize增加了还不满足) (三狗第二次打电话给财务妹子说钱不够用了,财务妹子按照公司经费的底线desiredWindowWidth,又给三狗打了1000块钱)
//情况二:lp.width=(精确的数值,EXATLY) (大狗的流程,明确提出500块的经费要求)
//情况三:lp.width=match_parent(精确的数值,EXATLY) (二狗的流程,公司经费是多少,我就要多少)
if (!goodMeasure) {
//封装mView宽高模式和尺寸 MeasureSpec (方法在后面给出了)
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//开始测量mView,(方法在后面给出了)
//情况二、三 (大狗、二狗的情况) 是第一次测量
//情况一 (三狗的情况) 是第三次测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
上面就是测量树根mView的流程,我们看看是怎么封装测量模式和尺寸的
ViewRootImpl.java
//ViewRootImpl.java
//生成mView的测量模式 MeasureSpec
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT://二狗的情况
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT://三狗的情况 windowSize(第一次是baseSize,第二次是(baseSize+desiredWindowWidth)/2,第三次是desiredWindowWidth)
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default://大狗的情况,明确知道自己的数值大小
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
开始测量,直接调用了mView(View.java)中的measure方法
//ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//mView开始测量自己
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
在View.java中的measure()方法中,又调用了onMeasure()方法
View.java
//View.java
//widthMeasureSpec,heightMeasureSpec:ViewRootImpl中传给rootView的尺寸和模式(大狗,二狗,三狗的经费)
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
//尺寸有变动(三狗狗的预算是否加了)
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
//是否是EXACTILY的情况(是否是大狗的情况,明确知道自己要500预算)
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
//是否是match_parent的情况(是否是二狗的情况,明确需要所有的经费预算)
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
//三狗的情况
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
//是否缓存过测量尺寸
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
//缓存里没有或者忽略缓存
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//设置root View的尺寸
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {//缓存里有
long value = mMeasureCache.valueAt(cacheIndex);
//设置root View的尺寸
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
}
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//缓存测量的尺寸
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
通过onMeasure()方法设置mView的尺寸
View.java
//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
在getDefaultSize()中从measureSpec中提取宽高尺寸大小
View.java
//View.java
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//如果没有指定mView的测量模式,那么设置mView的宽高为建议的size大小
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
我们看看如果没有指定测量模式是如何获取建议的size
View.java
//View.java
protected int getSuggestedMinimumWidth() {
//mView是否设置了背景图片?没有(设置为mMinWidth大小):有(取mMinWidth和图片尺寸最大值)
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
现在大家明白是如何确定mView(根View)的尺寸了吗?
- 这也是为什么,当我们在布局xml文件中,根布局写match_parent就是屏幕大小
- 这也是为什么,在我们玩悬浮窗,popwindow等窗体布局时,我们在根布局明明写的wrap_content,窗体的大小总是match_parent
到这里大家可能还是会有疑问,mView(根View)自己的尺寸是确定了,如果它有子View,那它的子View是怎么测量呢,也就是我们的View树是怎么完成测量的。
如果mView有子View,那它就是个ViewGroup(实际上mView一定是ViewGroup,即使我们setContent(View),还是会被添加到ViewGroup中),既然它最终是调用onMeasure()来确定自己的尺寸,那只要ViewGroup重写onMeasure()方法,先遍历child并调用child.measure()方法完成child的尺寸设置,再调用setMeasuredDimension()设置自己的尺寸,不就用递归的方式完成了View树的测量吗?
可是我们在ViewGroup中,并没有发现onMeasure()方法,因为ViewGroup是个抽象类,并且ViewGroup不知道你的子View的摆放规则,无法算出ViewGroup自己的尺寸大小,所以,ViewGroup的onMeasure()方法肯定是在它的实现类中。
我们拿FrameLayout的onMeasure源码来看看,它是怎么实现child的测量和自身大小的确定的
FrameLayout.java
//FrameLayout.java
//widthMeasureSpec、heightMeasureSpec:
//当FrameLayout对象作为根View,widthMeasureSpec和heightMeasureSpec是ViewRootImpl中给的初始大小(baseSize,desiredWindowWidth)
//当FrameLayout对象作为子View,widthMeasureSpec和heightMeasureSpec是父ViewGroup的测量尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取子View的数量
int count = getChildCount();
//当前FrameLaoyut是否设置成了wrap_content(AT_MOST模式)
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
//一个View集合,存放子View的模式是match_parent的子View
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
//测量状态
int childState = 0;
//遍历Child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//当child设置成GONE时,会跳过测量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//通知child测量自己 (方法后面给出)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
//获取child的布局参数
final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
//得出所有子View中宽高最大值
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
//组合测量状态
childState = combineMeasuredStates(childState, child.getMeasuredState());
//如果当前FrameLayout是wrap_content模式,并且child是match_parent模式,则加入到mMatchParentChildren集合,在最后重新测量
if (measureMatchParentChildren) {
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT ||
lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// 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());
}
//根据测量出来的child中的最大值和父View的尺寸,设置当前FrameLayout的大小,注意resolveSizeAndState()方法 (后面给出)
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1) {
//遍历mMatchParentChildren (布局是match_parent的子View集合)
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
//重新测量
final int childWidthMeasureSpec;
if (lp.width == FrameLayout.LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
//child测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
照着注释看,应该能看明白,这里有两个方法需要注意
- measureChildWithMargins()
- resolveSizeAndState()
measureChildWithMargins()
主要是看看child如何获取自己的布局参数并传递给自己的onMeasure()方法
ViewGroup.java
//ViewGroup.java
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed)
//child的布局参数
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//生成child的测量模式和尺寸(如何生成自己看看源码,不难)
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将自己的布局模式传递给child.onMeasure()方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
resolveSizeAndState()
主要是看看这个state对测量有什么影响
View.java
//View.java
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//FrameLayout的测量模式和尺寸
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;//封装的尺寸和状态
switch (specMode) {
case MeasureSpec.AT_MOST://如果FrameLayout的模式是wrap_content
if (specSize < size) {//如果FrameLayout的尺寸小于child的尺寸
//FrameLayout的尺寸太小啦,child的都比你大,放不下,child表示too_small
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);
}
可以看到,当child尺寸比parent大,测量的时候会加上一个MEASURED_STATE_TOO_SMALL标记,是不是有点眼熟?
在ViewRootImpl根View的测量流程中有:
//ViewRootImpl
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
}
在前面的故事中,三狗多次反应给的团建经费不够,打电话反应要加钱
那么在我们的View测量过程中,如果不满意默认的根View尺寸大小,就可以通过设置MEASURED_STATE_TOO_SMALL这个标记,来让根View发起重新测量。
从这也可以看出,我们的onMeasure()方法可能被多次调用,不仅仅上面说道的三次,这也是很多初学者不明白为什么自己自定义的View,会被多次调用onMeasure、onLayout方法。