《Android自定义控件入门到精通》文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683

《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code

文章目录

  • View树的测量流程

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 thingstry {//将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)

widthMeasureSpecheightMeasureSpec

两个是对测量模式和尺寸的封装,比如我们在xml布局文件中定义的layou_width和layout_height

来看个例子:

我们自定义了一个ChildView并设置它的宽高为50px

<cn.code.code.wiget.ChildViewandroid: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.ChildViewandroid: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.ChildViewandroid: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);//设置尺寸为100x100setMeasuredDimension(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 {@Overridepublic 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的子ViewmMatchParentChildren.clear();int maxHeight = 0;int maxWidth = 0;//测量状态int childState = 0;//遍历Childfor (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 toomaxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();// Check against our minimum height and widthmaxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());// Check against our foreground's minimum height and widthfinal 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_contentif (specSize < size) {//如果FrameLayout的尺寸小于child的尺寸//FrameLayout的尺寸太小啦,child的都比你大,放不下,child表示too_smallresult = 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方法。

Android自定义控件入门到精通--View树的测量流程相关推荐

  1. Android自定义控件入门到精通--View树的布局

    <Android自定义控件入门到精通>文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683 <Andro ...

  2. Android自定义控件入门到精通--Region区域

    <Android自定义控件入门到精通>文章索引 ☞ https://blog.csdn.net/Jhone_csdn/article/details/118146683 <Andro ...

  3. Android Volley入门到精通:初识Volley的基本用法

    1. Volley简介 我们平时在开发Android应用的时候不可避免地都需要用到网络技术,而多数情况下应用程序都会使用HTTP协议来发送和接收网络数据.Android系统中主要提供了两种方式来进行H ...

  4. Android studio入门到精通实例实验

    Android studio入门到精通实例实验 实验内容: ----------------- 简单的考试程序---------------- 实验步骤: 一.打开Android studio,新建一 ...

  5. 我的新书《Android自定义控件入门与实战》出版啦

    前言:当你回首往事时,不以虚度年华而悔恨,不以碌碌无为而羞耻,那你就可以骄傲的跟自己讲,你不负此生 [Android自定义控件入门与实战]勘误:https://blog.csdn.net/harvic ...

  6. 《Android从入门到精通》家庭理财通

    闲着没事把<Android从入门到精通>最后的项目给写了.以下是该项目的各文件之间的关联. 我把书上没写的那一部分,就是OutAccount部分代码补齐了(因为书后面的光盘没有了,所以只能 ...

  7. android从入门到精通pdf 明日科技

    android从入门到精通pdf 明日科技 链接: https://pan.baidu.com/s/1dGSkbCl 密码: wpaa (失效) 链接:https://pan.baidu.com/s/ ...

  8. Android自定义控件面试题,自定义View面试总结

    本着针对面试,不负责任的态度,写下<面试总结>系列.本系列记录面试过程中各个知识点,而不是入门系列,如果有不懂的自行学习. 自定义View三种方式,组合现有控件,继承现有控件,继承View ...

  9. 【不仅仅有面经】2020大厂Android面试经历,Android从入门到精通

    本人985学校本科毕业,非科班出身,三年多Android开发经验,半年iOS开发经验,和一个JavaScript小游戏开发经验.技术水平,略低于百度T5的样子(去百度面过,止步第四轮技术面).一直在创 ...

最新文章

  1. New ADODB.Connection ADOX.Catalog 提示user-defined type not defined
  2. 用ipad维护Linux服务器
  3. 记一种验证日期格式的正则表达式
  4. C++使用SQLite步骤及示例
  5. 美国返还中国文物,阿里谣言粉碎机获奖,教育部规范研究生培养,腾讯严打微信跑分活动,推动降低港澳漫游费,这就是今天的大新闻。...
  6. map容器实现一对多
  7. Java AIO 编程
  8. mysql 左连接 重复_mysql左连接重复行
  9. php 筛选搜索,筛选——搜索
  10. Vim 激荡 30 年发展史
  11. c语言实验分支程序设计二,C语言程序实验报告分支结构的程序设计(0页).doc
  12. javascript中事件
  13. 测试团队成功适应敏捷的障碍
  14. LayUI使用distpicker.js插件实现三级联动
  15. 告别windows,拥抱ubuntu
  16. 硬盘变成Raw格式 与 移动硬盘报I/O错误问题
  17. 配置Exchange Server 2010多种邮件客户端收发电子邮件
  18. 1730: 珠心算测验
  19. VS_设置护眼背景色
  20. 西北乱跑娃 -- mysql常用操作命令

热门文章

  1. UNICODE字碼分佈表
  2. 如何让SCI期刊审稿人,理解你的文章? - 易智编译EaseEditing
  3. 普洱熟茶的冲泡方法你知道如何才是正确的吗
  4. 乐1Pro 乐视X800_官方线刷包_救砖包_解账户锁
  5. VMware虚拟机网络连接设置——仅主机模式(Windows版)
  6. qt解析json数据
  7. android 360 ppi,360特供 vs 小米手机性能大比拼!
  8. strcpy_s函数/strcpy函数简介
  9. 数据库基本知识与关系模型
  10. 日志分隔工具Cronolog的使用