转载请注明出处:http://blog.csdn.net/hjf_huangjinfu/article/details/79140601
预备知识点,View的measure过程:http://blog.csdn.net/hjf_huangjinfu/article/details/51147636
概述
最近碰到一个关于NestedScrollView
在特定情况下,里面部分内容不显示的问题,做个记录。
1、问题详情
有一个布局文件,精简后,如下所示:
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:id="@+id/study_explore_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/green"
android:text="文本一\n文本一"
android:textSize="30dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/blue"
android:text="文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二\n文本二"
android:textSize="30dp" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
一个NestedScrollView
包裹一个LinearLayout
,然后里面垂直放置两个TextView
,文本一中的内容比较少,文本二中的内容比较多,然后文本二有一部分内容被屏幕遮挡,向上滑动可以查看完整内容。
在大部分机器上显示正常,就是如下图所示:
但是,当时程序运行到一个锤子手机的时候(该手机屏幕比较大,当时并没有注意到这一点),出现了问题,显示的内容和我们想象的不一样,屏幕上只能看到文本一,文本二已经完全不见了,如下图所示:
刚开始以为是锤子手机ROM出了问题,哈哈,但是,不找到根本原因,怎么行呢。
2、问题定位
先排查文本二的visibility
,发现是true
,没问题。
然后查看文本二的尺寸,根据postDelayed
,打印getMeasuredHeight
,发现值为0
,所以问题来了,高度为0
,肯定就不显示了。但是为什么其他手机是好的?所以我当然怀疑是锤子问题啊。
但是不管什么问题,总归是要解决的,所以要找到根源。通过替换LinearLayout
为自定义的MyLinearLayout
(什么都没做,只是在onMeasure
中打了一点日志),我们发现在显示正常的手机上,该方法被调用了一次,传入的heightMeasureSpec
的mode
为MeaureSpec.UNSPECIFIED
,根据ViewGroup.getChildMeasureSpec
逻辑和两个文本的布局参数可知,传入文本一和文本二的heightMeasureSpec
的mode
为MeaureSpec.UNSPECIFIED
,然后文本一和文本二就测量出了自己的高度(备注:后面会提到这个测量)。
但是在那个锤子手机上,该方法被调用了2次,第二次传入的heightMeasureSpec
的mode
为MeaureSpec.EXACTLY
,根据ViewGroup.getChildMeasureSpec
逻辑和两个文本的布局参数可知,传入文本一和文本二的heightMeasureSpec
的mode
分别为MeasureSpec.EXACTLY
和MeasureSpec.AT_MOST
。然后就出现了上面那个问题。
ViewGroup.getChildMeasureSpec
代码逻辑如下:
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);
3、第一次着手修复
既然不正常的机器上面,它被测量了2次,那么,找到它的父布局,也就是NestedScrollView,我们看一下它的onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
int height = getMeasuredHeight();
if (child.getMeasuredHeight() < height) {
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight(), lp.width);
height -= getPaddingTop();
height -= getPaddingBottom();
int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
发现在super.onMeasure
走完之后,如果条件满足,child
有机会会被再测量一次。那么条件是什么呢?
fillViewPort
为true
。- 测量模式不为
MeasureSpec.UNSPECIFIED
。 - 子布局的高度比自己的高度小。
所以,上面3个条件都满足了,触发了第二次测量,那么去深入研究一下。
4、fillViewPort
这个属性是干什么的呢?官方文档解释是,子视图会充满用户可见的区域(可以理解为子视图会填满NestedScrollView
)。假如NestedScrollView
的高度为100dp
,而它的子View
只有50dp
,那么当该属性为true
时,子View
会被重新测量为100dp
。
这个属性不会改变NestedScrollView本身的测量高度。
只有当某个子视图中的某个控件需要沉底的时候,才会使用到该属性。
5、二次测量
我们仔细看一下满足条件后的二次测量做了什么,根据逻辑,当子视图高度比自己小时,NestedScrollView
会让子视图重新测量一次,就是为了让子视图跟自己一样高。所以传入文本一和文本二的heightMeasureSpec
的mode
为MeasureSpec.EXACTLY
,而传入的size
就是NestedScrollView
的高度减去padding
的值。
然后文本二的高度就被测量为0
。
那么为什么为0
呢?查看布局,发现文本一的高度为match_parent
,所以文本一的高度应该是等于LinearLayout
的高度,然后没有剩余空间留给文本二了,所以导致了文本二的高度为0。
那么为什么当传入到文本一的heightMeasureSpec
的mode
为MeasureSpec.UNSPECIFIED
时,不会出问题呢?就是之前备注的那个地方,因为View
的测量遵循一个准则,也就是说,当mode
为MeasureSpec.UNSPECIFIED
时,就表明父布局并不限定子控件的尺寸,所以子控件会计算一个自己足够用的尺寸,对于TextView
,可以理解为,能把所有文本显示出来,就够了,虽然是match_parent
,但是并不是真的和父布局一样大。
6、问题总结
由于跑在了大屏手机,而且fillViewPort
为true
,子视图高度不够NestedScrollView
高度
->
触发了二次测量
->
又因为在文本二之前的文本一是match_parent
->
文本一高度和LinearLayout
高度一样
->
导致了没有剩余高度留给文本二
->
文本二高度为0
,内容无法显示。
7、着手修复
对上面的问题,我们可以有下面两个解决方案:
- 既然文本一把父布局空间全部占用,导致文本二没有高度,那么把文本一高度改为wrap_content就可以。
- 既然触发了二次测量,内容高度我们无法控制,那么把fillViewPort设为false,也就可以阻止二次测量问题。
PS:锤子莫名背了一次锅,哈哈……….