Android事件分发和View绘制流程分析(三)
本篇就Android中View的测量整体流程简单记录如下<下面涉及到的Android源码均为API23的源码>
二、Android中View树的绘制
对于View树的绘制起始点可以追溯到在ActivityThread的performResumeActivity()–>WindowManagerGloble.addView()–>new RootViewImpl()+root.setView(DecorView)–>RootViewImpl.setView()—->RootViewImpl.requestLayout()以上步骤在第一篇文章中均可以找到,最终调用到RootViewImpl.requestLayout()方法
下面我们就从RootViewImpl.requestLayout()开始:
2.1、RootViewImpl.requestLayout()的代码,最终会调用到performTraversals(),其简单的代码逻辑如下:
@ViewRootImpl.java
@Override
//-----------1、在setView()中有调用该方法,开始进行视图绘制----------
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();//schedule:安排 Traversals:穿越
}
}
//---------------2、异步调用绘制视图 Choreographer:Coordinates the timing of animations, input and drawing.---------
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//--------***mTraversalRunnable 是一个TraversalRunnable类型可执行对象,视图绘制专用线程***-------------
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
//-------------3、TraversalRunnable定义-------------------
@ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
//在该处调用doTraversal()
doTraversal();
}
}
//--------------4、mTraversalRunnable初始化---------------
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
//--------------5、一般常见的View开始绘制的调用performTraversals();在此方法中调用--------------
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();//---------6.performTraversals()----
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
2.2 我们下面看performTraversals()的具体实现,该部分代码是在ViewRootImpl。实现如下:
2.2.1 从2.1中调用到 performTraversals()
2.2.2 把mWindowAttributes赋值给lp,此处的lp会在下面合成DecorView的测量模式时使用。mWindowAttributes实际就是当前从当前的PhoneWindow的参数中传递过来的,而PhoneWindow中的该值是在其Window初始化话时赋值的,其默认宽高都是LayoutParams.MATCH_PARENT 具体代码在Window中,代码如下:
@Window
private final WindowManager.LayoutParams mWindowAttributes =
new WindowManager.LayoutParams();
@WindowManager.LayoutParams
//LayoutParams 的默认构造函数实现如下,可知其宽高默认都是LayoutParams.MATCH_PARENT的
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
2.2.3 初始化测量开始时DecorView所需的具体size大小,默认为实际屏幕的宽高
2.2.4 该处最终会回调到Activity的onAttatchWindow()
2.2.5 mApplyInsetsRequested该值是在requestFitSystemWindows()方法中设置的,如果该值满足为true,则会执行2.2.6,否则跳过2.2.6
我们在View中调用的requestFitSystemWindows()最终会调用到ViewRootImpl中的requestFitSystemWindows() 并设置mApplyInsetsRequested 为true,大致是在设置了Window的属性之后例如SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 属性之后应该调用一次该方法,以保证得到我们想要的效果
2.2.6 在此处调用measureHierarchy(),此处的lp为WindowManager.LayoutParams desiredWindowWidth为 屏幕宽度,该方法最终会调用到 ViewRootImpl的performMeasure(),和在2.2.8处调用的是同一个方法
2.2.7 生成顶级DecorView的MeasureSpec,默认是Mode为:EXACTLY Size:屏幕相应的宽高
2.2.8 performMeasure() 正式测量确认各子View的尺寸,会回调所有View的onMeasure()方法确定其大小。(前面2.2.6处的执行依赖2.2.5中mApplyInsetsRequested的值不一定会执行)
2.2.9 如果Window的Layoutparams中设置的有权重值则会根据权重再次测量,一般也不会执行
2.2.10 测量完毕继续调用执行layout,此时则会回调所有ViewGroup及View中的onLayout(),即我们常说的onLayout()方法
2.2.11 布局完之后开始调用performDraw(),最终会回调到所有可绘制View的onDraw()
@ViewRootImpl.java
private void performTraversals() {//---------2.2.1 经过上面的逻辑最终会调用到此处performTraversals()
// cache mView since it is used so much below...
final View host = mView;
WindowManager.LayoutParams lp = mWindowAttributes;//------2.2.2下面measureHierarchy()方法中调用的getRootMeasureSpec()方法参数中的lp 实际即为当前Window的LayoutParams
int desiredWindowWidth;//下面measureHierarchy()方法中调用的getRootMeasureSpec()方法参数中宽度大小,实际即为屏幕的宽
int desiredWindowHeight;//下面measureHierarchy()方法中调用的getRootMeasureSpec()方法参数中高度大小,实际即为屏幕的高
...
if(mFirst){
....
//----2.2.3初始化desiredWindowWidth和desiredWindowHeight 对应下面measureHierarchy()第一个参数初始化位置;对于普通Activity即为屏幕的宽高
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
....
host.dispatchAttachedToWindow(mAttachInfo, 0);//------2.2.4、该处最终会回调到Activity的onAttatchWindow()
}
...
if (mApplyInsetsRequested) {//-----2.2.5 mApplyInsetsRequested该值是在requestFitSystemWindows()方法中设置的,我们在View中调用的requestFitSystemWindows()最终会调用到ViewRootImpl中的requestFitSystemWindows() 并设置mApplyInsetsRequested 为true,大致是在设置了Window的属性之后例如SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 属性之后需要调用一次该方法
mApplyInsetsRequested = false;
mLastOverscanRequested = mAttachInfo.mOverscanRequested;
dispatchApplyInsets(host);
if (mLayoutRequested) {
// Short-circuit catching a new layout request here, so
// we don't need to go through two layout passes when things
// change due to fitting system windows, which can happen a lot.
//----------2.2.6在此处调用measureHierarchy()开始measure 此处的lp为WindowManager.LayoutParams desiredWindowWidth为 屏幕宽度 desiredWindowHeight为屏幕高度---------
windowSizeMayChange |= measureHierarchy(host, lp,
mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
}
}
...
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
-----2.2.7拿取根部局的MeasureSpec-----
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);//---1.拿取根部局的MeasureSpec---
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);//---2.拿取根部局的MeasureSpec---
if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth="
+ mWidth + " measuredWidth=" + host.getMeasuredWidth()
+ " mHeight=" + mHeight
+ " measuredHeight=" + host.getMeasuredHeight()
+ " coveredInsetsChanged=" + contentInsetsChanged);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//------2.2.8正式测量确认各子View的尺寸------
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {//--------如果权重值不管是横向的还是竖向的都需要再次measure()
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {//----------如果权重值不管是横向的还是竖向的都需要再次measure()
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {//---------如果需要再次测量
if (DEBUG_LAYOUT) Log.v(TAG,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ------2.2.9 如果Window的Layoutparams中设置的有权重值则会根据权重再次测量
}
layoutRequested = true;
}
}
} else {//???--wtf如果不是第一次测量&&window/insets/visibility都没有变化,只是窗口有移动
...//省略代码
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);//------2.2.10、执行layout,会回调所有ViewGroup及View中的onLayout()-------------
//By this point all views have been sized and positioned 到此则所有View的大小及位置都已经确定完毕
// We can compute the transparent area
if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
// start out transparent
// TODO: AVOID THAT CALL BY CACHING THE RESULT?
//----##首先确认DecorView在Window中的位置##-----
host.getLocationInWindow(mTmpLocation);
...
}
}
mFirst = false;
mWillDrawSoon = false;
mNewSurfaceNeeded = false;
mViewVisibility = viewVisibility;
...
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();//------------2.2.11该方法中会回调所有onDraw()-----
}
} else {
if (viewVisibility == View.VISIBLE) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
mIsInTraversal = false;
}
2.3 在2.2.6中的ViewRootImpl的measureHierarchy()最终会调用到和2.2.8处相同的方法performMeasure(),而performMeasure()最终会调用到DecorView的measure()方法,measureHierarchy()和performMeasure()的实现代码如下:
2.3.1 在performMeasure()中,会调用到mView.measure(),而该mView正是DecorView,至此进入我们比较熟悉的View的measure()方法中
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
"Measuring " + host + " in display " + desiredWindowWidth
+ "x" + desiredWindowHeight + "...");
boolean goodMeasure = false;
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {//当Window设置的为WrapContent时
// On large screens, we don't want to allow dialogs to just
// stretch to fill the entire width of the screen to display
// one line of text. First try doing the layout at a smaller
// size to see if it will fit.
....
}
if (!goodMeasure) {//一般的Activity中的Window在初始化时都是ViewGroup.LayoutParams.MATCH_PARENT的,上面的逻辑不会走直接走到此处
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//---------执行perforMeasure()-----------
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals -- after measure");
host.debug();
}
return windowSizeMayChange;
}
//----------至此开始调用DecorView的measure()方法,Activity中一般给入的参数都是屏幕的实际宽高和MeasureSpec.EXACTLY合成的MeasureSpec
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//----2.3.1 该mView正是DecorView
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
2.4 View测量开始:因为DecorView继承自FrameLayout因此其是一个ViewGroup,从此处开始就是常见的ViewGroup和View的measure过程了。
View的测量开始是从View.measure()方法开始的,我们先理清一下View树的测量逻辑
- 1.对于顶级DecorView的measure()我们在2.2中已经看到是在ViewRootImpl中触发,然后会调用的DecorView的onMeasure()方法(需要明确的是DecorView是一个ViewGroup);
- 2.对于一个子View(不管是单个的View还是一个ViewGroup)其measure()方法的触发逻辑为:当其父View在执行onMeasure的时候会依次调用测量子View,最终会调用到子View的measure()方法。
- 3.如果子View是一个普通的View,当触发其onMeasure之后(该过程可能执行不止一次),该View会根据传入的MeasureSpecMode和自己的测量规则算出自己的大小(只算大小,不管位置),并调用setMeasuredDimension()保存自己的测量出来的大小。
- 4.如果子View还是一个ViewGroup,则会再次在其onMeasure()方法中按照实际的逻辑依次触发子View的measure()方法,并调用setMeasuredDimension()保存自己的测量出来的大小;
- 5.进一步依次递归
2.4.1 在测量过程中,我们知道View的measure()最终都会回调到View的onMeasure(),onMeasure()在View中的默认实现如下:
@View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//测量完成后调用setMeasuredDimension(),只有在调用该方法之后View的getMeasureWidth()才会有值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
2.4.2 在上面的View的onMeasure()回调方法中的有两个参数widthMeasureSpec和heightMeasureSpec,我们暂撇开DecorView的具体测量,首先先明确一下onMeasure函数中的两个参数是怎么来的:
2.4.2.1 对于DecorView因为其是一个顶级的布局View,DecorView之上并没有父布局,其WidthMeasureSpec和heightMeasureSpec从2.2中我们可以看到是在ViewRootImpl中通过getRootMeasureSpec()生成,然后调用DecorView的measure()方法传递进去的,getRootMeasureSpec()方法如下:
对于常见的Activity,DecorView默认其是由屏幕的实际宽高和MeasureSpec.EXACTLY合成的,具体其他的情况可以参考源码查看
@ViewRootImpl.java
private static int getRootMeasureSpec(int windowSize, int rootDimension) {//---此处的rootDimension默认是LayoutParams.MATCH_PARENT
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;
}
一般对于Activity来说其WindowSize即为屏幕的宽高;rootDimension则对应为WindowManager.LayputParams,因为一般默认系统生成的PhoneWindow都会设定为Match_PARENT,因此Mode就对应为就对应MeasureSpec.EXACTLY。但是该LayoutParams是可以改变的,我们比较常用的手动设置的场景是在自定义Dialog时根据需要设置Dialog的Window的相关参数
2.4.2.2 对于一般的View无论是一个View还是一个ViewGroup,这两个参数都是在父布局中生成并最终传入的,而只要是父布局则肯定是ViewGroup,其测量模式都是在父布局中生成的:
在ViewGroup中不同的ViewGroup实现的onMeasure由于工作机制不同,其measureChildren()和measureChild()的实现也不相同,默认实现如下:
@ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//measureChild的默认实现如下
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
但是不管ViewGroup的具体实现是什么,最终都会调用getChildMeaureSpec()来生成子View所需的MeasureSpec,实现如下:
@ViewGroup.java
//getChildMeasureSpec()实现如下
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
而该getChildMeasureSpec(int spec, int padding, int childDimension)的逻辑则可以总结为我们经常能够见到的一张表格:
2.5 以上我们知道了在View的测量起始位置measure(),然后会调用onMeasure(,以及onMeasure()方法中的两个参数的来历,下面就以一个常见的LinearLayout来走一下一个ViewGroup具体的measure过程:
首先:我们带着以下几个问题来看其测量过程
- 1.在测量中为什么有时measure会调用多次?
- 2.LinearLayout中设置Weight后是怎么测量的?
- 3.什么时候getMeasureWidth(),getMeasureHeight()才能得到正确的值?
- 4.什么时候getWidth()和getHeight()才能拿到正确的值?
2.5.1 我们先看LinearLayout的onMeasure方法,再提一下View(不管是View还是ViewGroup)的onMeasure方法的调用逻辑为:当父View在执行onMeasure的时候依次调用子View的measure()方法,而在View的measure中会回调onMeasure():
2.5.1.1在LinearLayout的onMeasure()中方法中,首先会根据当前LinearLayout设置的orientation调用来调用相关的测量方法,代码如下:
@LinearLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);//设置Orientation = VERTICAL 时会使用该方法进行测量
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
2.5.1.2 我们只以orientation设置为VERTICAL的来进行分析,measureVertical的代码如下:**
- 1、在该方法中会首先置mTotalLength为0,该字段记录当前LinearLayout的总高,每测量出来一个子View的总高时即会加上该子View的高度,以及其设置的margin值
- 2、在该方法中会声明局部变量totalWeight用来记录子View设置的权重值
- 3.拿取当前LinearLayout所有的子View个数,不管是是否已经设置为了GONE,但是在下面测量中设置为GONE的子View将不会测量
- 4.然后会得到当前LinearLayout的widthMode和heightMode,以便在确定子View的测量模式时使用
- 5.for循环拿取依次遍历子View,对于每一个子View实现以下luoji
- 6.如果该子View为设置为了GONE,则直接continue,跳过该View的测量并统计跳过的View
- 7.判断该子View是否有Divider,如果有的话则总高加上一个Divider的高度
- 8.拿取子View设置的LayoutParams,即在XML中的声明的相关LayoutParams或代码中设置的相应LayoutParams
- 9.1如果当前LinearLayout的测量模式是MeasureSpec.EXACTLY并且该子View的LayoutParams的lp.height == 0 并且lp.weight > 0,此时会先跳过该子View的测量,但是mTotalLength会加上该子View设置的margin,并设置skippedMeasure标志位为true,标识有跳过的子View未测量,以便在下面的逻辑中测量
- 10.如果不满足9.1的条件,该开始对子View会进行一次测量
- 10.1、如果当该子View的lp.height == 0 并且lp.weight > 0时,此时当前LinearLayout的测量模式肯定不为MeasureSpec.EXACTLY,则设更改该子View的lpt的参数lp.height为LayoutParams.WRAP_CONTENT,表示当当前LinearLayout的尺寸高度不确定时,该子类View的在测量时尽量利用所有的有效空间
- 10.2 调用 measureChildBeforeLayout(),正式开始测量子View的大小,该方法会调用ViewGroup中的 measureChildWithMargins(child,widthMeasureSpec, totalWidth,heightMeasureSpec, totalHeight);其中的参数totalHeight即是当前已经测量出来的mTotalLength。该参数最终会被当做已经占用的高度(heightused),在合成子View的MeasureSpec时候使用的size会相应的减掉该值
- 11、根据已经占用的高度计算剩余的有效高度空间,此时满足9.1条件的View还没有进行测量不再已占用的高度之内,另外需要注意的一个场景是当一个View的高设置为WrapContent,同时weight>0次数该View的高度会按照WrapConten的测量模式,执行measureChildBeforeLayout()进行过一次测量一遍并已经计入了mTotalLength。
- 12、到该步骤则View大致是对应9.1的条件满足还没有进行过测量的,以及不管是否有weight值,但是已经按照相应的测量模式测量过一次了的
- 13.如果满足有跳过的View没有测量(即有满足9.1条件的View)或者统计到的weight值大于0同时还有剩余空间,则循环遍历所有子View
- 14、如果子View的weight>0,则根据剩余高度和统计出来的所有子View的weight值,对剩余空间进行切分,并把该值按比例添加到子View已经测量出来的高度上并和MeasureSpec.EXACTLY合成子View测量模式进行重新测量,总之就是把剩余的空间按比例重新分配到子View上去,需要注意对于前面满足9.1跳过的子View,不会有把当前的高度加上相应比例剩余空间的操作,而是直接按照其需要占用的剩余空间的大小进行重新测量。因为之前没有对其测量过根本
- 15、如果没有跳过任何子View的测量的(对应13条件的else{}),且满足设置了使用useLargestChild为true且当前的LinearLayout的测量模式不为MeasureSpec.EXACTLY,ps:一般的情形不会走到该逻辑中
- 16、当前LinearLayout测量完毕 调用setMeasuredDimension()设置其宽高。其中的入参heightSizeAndState,在第11步代码之前已经确认,因为如果没有weight则那是已经计算出了总高,如果是设置有weight则weight就是要占据所有剩余空间,此时height也是可以确定的
具体LinearLayout的代码及相应的简单注释如下:
@LinearLayout.java
//竖直布局时使用的测量方法
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;//---1.用来记录当前LinearLayout已经占用的高度(或者简单的说每测量一个子View该值都会加上该子View的高度)
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;//---2.权重值统计
final int count = getVirtualChildCount();//-----3.拿取子View的个数,但不一定是最终子View的个数,子View的Visible属性为GONE时会直接跳过不再测量
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);//----4.当前布局的测量模式--对于该LinearLayout的子View来书这个即使父布局的测量模式,在合成子View的MeasureSpecMode的时候父布局的测量模式即使从此处得到
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;
// See how tall everyone is. Also remember max width.----5、 开始所有子View的测量,ViewGrop的测量师递归的即如果子View也是一个ViewGroup则会直接进入该子View其内部进行测量
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {//----6、如果子View的Visibility属性设置为GONE则统计后直接continue
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {//------7、是否有Divider
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();//----8、拿取子View的LayoutParams
totalWeight += lp.weight;//统计子View设置的权重值
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {//----9.如果LinearLayout的SpecMode为EXACTLY&&子View的height ==0 且子View设置的weight>0(简单书就是如果当前的Linearlayout的高度尺寸已经确定同时该子View的高度设置为0且该子View的weight值不为0)---
// Optimization: don't bother measuring children who are going to use
// 优化:不再过度的去测量将会使用剩余空间的子View
// leftover space. These views will get measured again down below if
//这些子View将会在下面再次得到测量当还有剩余空间时
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;//测量子View过程中是否有跳过
} else {//----10、否则
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {//----11、如果子View的Weight>0(即有权重值)且设置的高度为0
// heightMode is either UNSPECIFIED or AT_MOST, and this
//不管当前布局的测量模式是 ATMOST还是UNSPECIFIED(不可能再会是EXACTLY),
// child wanted to stretch to fill available space.
//并且该子View想要填充当前Linearlayout剩余的有效空间。
// Translate that to WRAP_CONTENT so that it does not end up
//则设置该子View的LayoutParams为WrapContent
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;//设置子View为WrapContent
}
// Determine how big this child would like to be. If this or
//确认该子View想要得到多大的空间如果该子View或者之前有子View设置过Weight,
// previous children have given a weight, then we allow it to
//那么会允许子View得到所有的有效空间(但是如果需要的话我们在后面可能会收缩该空间)
// use all available space (and we will shrink things later
// if needed).
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 applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
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) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
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();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);// ------heightSizeAndState 确认
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int delta = heightSize - mTotalLength;//----11、算出之前没有跳过的所有子View已经占据的空间打小,注意当子View设置为WrapContent weight>0时,此时mTotallenrth中是包含实际按AtMost算出来的大小值的,该处可以说明weight是所有子View计算之后剩余子View的剩余大小再按权重分配的---
//-----12、到该步骤则View大致是对应9.1的条件满足还没有进行过测量的,以及不管是否有weight值,但是已经按照相应的测量模式测量过一次了的
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
//----13、循环遍历子View 根据相应的规则进行重新测量
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) {//14、如果子View的权重值大于0 按以下机制再次计算,------------
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);//12.计算需要该子View的weight所需要占用的剩余空间
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)) {//此处逻辑(对应之前跳过没有测量的View):子View满足lp.weight>0的同时lp.height!=0或者当前LinearLayout的SpecMode不是EXACTLY
//此处对应一种情况是当weight>0 lp.height为wrapcontent时,当前LinearLayout为EXACTLY时,得到的效果即是,该子View的大小为按照权重得到的剩余空间+上面按照ATMOST已经测量出来的大小。我们可以设置该多个子View的LayoutParamas为WrapContent同时设置其weight都为1,会看到子View的所占的高度是不等的,但是其减去实际占的位置之后,剩余的空间是相等的
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;//把剩余的空间按照比例添加到子View上
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));//重新测量子View
} else {//此处逻辑是子View满足(lp.weight>0&&lp.height==0&&当前Linearlayout的heightSpecMode==EXACTLY)简单的说即是上面直接跳过的没有进行过测量的子View
// child was skipped in the loop above.
// Measure for this first time here
// 一开始跳过的子View此时第一次测量 尺寸大小为根据剩余尺寸和所占的权重计算出来的size大小,SpecMode为EXACTLY
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// Child may now not fit in vertical dimension.
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));//再次统计当前Linearlayout的高度
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {//----如果没有跳过任何子View的测量的则执行以下逻辑----
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {//----15、如果没有跳过任何子View的测量的则执行以下逻辑,且满足设置了使用useLargestChild为true且当前的LinearLayout的测量模式不为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;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);//----16、当前LinearLayout测量完毕 调用setMeasuredDimension
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
2.5.2、上面我们大致的走了LinearLayout的整体测量流程。我们就来解释一下我们一开始提出的几个问题:
2.5.2.1 在测量中为什么有时measure会调用多次?
对于LinearLayout如果一旦子View设置了weight,并且在当前LinearLayout的SpecMode不为EXACTLY的时候,会首先该子View会首先按照ATMOST+剩余有效空间大小合成子View的SpecMode进行一次测量,然后当所有子View测量完毕之后,会把剩余的空间按照该View占总权重的比例算出一个高度值,并把该值添加到已经测量出的高度值上与MeasureSpec.EXACTLY合成之后再次测量,此时子View的measure就不止调用一次,当让这只是一种情况。但说明measure调用的次数是和具体的测量策略有一定关系的
2.5.2.2 LinearLayout中设置Weight后是怎么测量的?
问题1中也已经回答了该问题,有一个比较特殊的场景可以验证,假设当前LinearLayout高满足测量模式为MeasureSpec.EXACTLY,有三个子View,当把所有子View设置为WrapContent,weight值为1时,如果三个子View的占用高度不同但是加起来当前LinearLayout仍有剩余空间时,则每个子View的实际测量高度为其有效占用高度+剩余空间的1/3,简单的各子View的最终测量出来的大小是不同的。(子View嵌套又有设置了Weight的子View的场景除外)。但是如果三个子View的高度设置为0,则会满足2.4.1中9.1的条件,此时最终子View的测量出来的高度值相同为当前LinearLayout高度的1/3。因此想要用weight值来让子View之间的间距看起来一样的时候,需要设置为WrapContent但是不建议使用此种方法,因为一旦子View中还有子View且设置了Weight值那么就不一样了。
2.5.5.3 什么时候getMeasureWidth(),getMeasureHeight()才能得到正确的值?
这个我们在跟进代码setMeasuredDimension()之后就能清除看到只有测量完毕调用了该方法之后getMeasureWidth()和getMeasureHeight()才能拿到有效的值。
//setMeasuredDimension() 会调用到下面的方法
@View.java
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;//给mMeasuredWidth赋值,在这之后getMeasureHeight()才能拿取到值
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
2.5.5.4什么时候getWidth()和getHeight()才能拿到正确的值?
mRight和mLeft则是在onLayout之后才能确定,所以getWidth()想要拿取到正确的值需要等到layout完毕之后才行
对于这个问题我们看代码:
@View.java
public final int getWidth() {
return mRight - mLeft;
}