在使用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