在使用Lint掃描工程時,看到這個提示。 Google推薦將ScrollView的子View高度設置爲wrap_content, 但實際業務開發時可能根節點是LinearLayout(layout_height="match_parent"), 而後發現屏幕顯不下就包了一層ScrollView。 運行看到ScrollView能正常上下滑動,就沒改LinearLayout的layout_height屬性。java
爲何ScrollView仍然能上下滑動呢??? 按照安卓View的測量方式LinearLayout應該跟ScrollView的高度相同。 去源碼裏找答案:ScrollView重寫了ViewGroup的measureChildWithMargins方法, 該方法會在onMeasure裏調用。android
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
//設置child高度的測量方式爲UNSPECIFIED, 這也是ScrollView子View高度參數無效的緣由。
//UPSPECIFIED表示child高度由本身決定,不受父容器的限制
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
核心是設置child高度測量方式爲UNSPECIFIED, 這就是爲何LinearLayout設置高度爲match_parent仍然可以正常滑動的緣由。 後面再講爲何ScrollView要篡改子View高度的測量方式爲UNSPECIFIED。less
ScrollView和子View高度能夠設置爲wrap_content或者match_parent(與固定值高度狀況相同)、 再考慮子View高度大於/小於ScrollView的高度,排列組合有8種狀況。 上面說到給ScrollView的子View設置高度參數無效, 因此剩下4種狀況。ide
第一種狀況:ScrollView高度是match_parent或固定值且子View高度小於ScrollView, 則子View高度是實際須要的高度。 若是須要子View高度等於父容器ScrollView, 則須要添加子View即LinearLayout屬性android:fillViewPort="true"。函數
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//對應ScrollView的android:fillViewPort屬性,默認值false
if (!mFillViewport) {
return;
}
//設置android:fillViewPort="true「後纔會執行下面的代碼
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
//沒設置layout_height屬性則返回
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
...
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
//若是ScrollView的子View高度小於本身則從新測量子View高度, 就是將ScrollView的高度賦值給子View高度。
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
第二種狀況:ScrollView高度是match_parent或固定值且子View高度大於ScrollView,則測量後子View高度大於ScrollView高度。佈局
第三種狀況:ScrollView高度是wrap_content且子View高度低於屏幕高度, 則ScrollView和子View的高度相等, 即實際須要的大小。(比較好理解)ui
第四種狀況:ScrollView高度是wrap_content且子View高度大於屏幕高度, 則ScrollView高度等於填滿屏幕的高度, 而子View的高度大於ScrollView。 (後面會講wrap_content的原理,解釋這時ScrollView高度爲何不等於子View)this
下面解釋一下MeasureSpec是幹嗎的, 咱們知道View通過了onMeasure、onLayout、onDraw後纔會顯示出來。spa
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) .net
int類型佔用4個字節即32比特, widthMeasureSpec和heightMeasureSpec的高2位是測量模式,低30位是在高2位的測量模式下獲得的結果。 安卓有3種測量模式:
一、UNSPECIFIED: Measure specification mode: The parent has not imposed any constraint on the child. It can be whatever size it wants. 即父容器不限制本身的大小, 自定義ViewGroup纔會配置該屬性, 例如ScrollView。 通常用於framework。
二、EXACTLY: Measure specification mode: 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. 即父容器決定本身的大小, 對應將本身設置爲match_parent或固定值。
三、AT_MOST: Measure specification mode: The child can be as large as it wants up to the specified size. 即父容器指定了本身的最大值, 子View的大小不能超過specified size。 對應將本身設置爲wrap_content.
例外:父容器wrap_content且子View是match_parent,則子View測量模式是AT_MOST.
/* @param child The child to measure 待測量的子View
* @param parentWidthMeasureSpec The width requirements for this view
* 父容器(ViewGroup子類)寬的mesureSpec
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* 父容器在水平方向已佔用的大小(本身的兄弟View佔用的空間)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
* 該方法的做用是肯定child的MeasureSpec
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//獲得child的佈局參數
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//獲得child寬的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//獲得高的MeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//測量child寬、高
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
11111
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //父容器測量方式
int specSize = MeasureSpec.getSize(spec); //父容器測量的大小
//padding是父容器在水平或垂直方向已佔用的空間
int size = Math.max(0, specSize - padding); //獲得剩餘可用大小
int resultSize = 0;
int resultMode = 0;
//根據父容器的SpecModo肯定child的MeasureSpec
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY: //父容器是match_parent或固定值
if (childDimension >= 0) {
//若是child寬或高設置了固定值(例如10dp),則使用固定值做爲specSize
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //使用父容器剩餘空間做爲specSize
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; //父容器限制本身最大值是size(即剩餘空間), 稍候用例子證實
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST: //父容器設置了wrap_content
if (childDimension >= 0) { //例如layout_width="10dp"
// Child wants a specific size... so be it
resultSize = childDimension; //child寬高設置了固定值
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; //child的SpecSize不超過父容器
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; //child的SpecSize不超過父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED: //不限制子View
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; //根據布爾值設置specSize
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; //wrap_content和match_parent邏輯相同
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
那麼MeasureSpec最開始是如何跟wrap_content/match_parent/固定值關聯上的呢??? 在ViewRootImpl.java的getRootMesureSpec函數。 注意:Activity佈局measure過程是從DecorView開始,測量模式爲EXACTLY,寬高佔滿屏幕(即specSize等於屏幕的寬和高)。而後從DecorView逐級測量子View。
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window 屏幕大小
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//rootDimension是DecorView的參數,而DecorView配置的是match_parent. 測量模式是EXACTLY
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;
}
小結:
一、若是寬/高設置了固定值(例如layout_width="10dp"), 那麼MeasureSpec的specSize等於10dp,specMode是EXACTLY。
二、若是寬/高設置了wrap_content, 父容器是match_parent/wrap_content/固定值時本身的specSize不會超過父容器。
三、若是寬/高設置了UPSPECIFIED, 本身想多大就多大,不受父容器的限制。 這就是爲何ScrollView要篡改子View高度的測量方式爲UPSPECIFIED的緣由。
若是將ScrollView換成其它ViewGroup,能夠看到Framework、LinearLayout的高度等於屏幕剩餘高度, TextView高度是2000dp。
———————————————— 版權聲明:本文爲CSDN博主「brycegao321」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接及本聲明。 原文連接:https://blog.csdn.net/brycegao321/article/details/87186309