NestedScrollView 在特定情况下,部分内容不显示的问题

转载请注明出处: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,文本一中的内容比较少,文本二中的内容比较多,然后文本二有一部分内容被屏幕遮挡,向上滑动可以查看完整内容。

在大部分机器上显示正常,就是如下图所示:
nsv-normal

但是,当时程序运行到一个锤子手机的时候(该手机屏幕比较大,当时并没有注意到这一点),出现了问题,显示的内容和我们想象的不一样,屏幕上只能看到文本一,文本二已经完全不见了,如下图所示:
nsv-abnormal

刚开始以为是锤子手机ROM出了问题,哈哈,但是,不找到根本原因,怎么行呢。

2、问题定位

先排查文本二的visibility,发现是true,没问题。

然后查看文本二的尺寸,根据postDelayed,打印getMeasuredHeight,发现值为0,所以问题来了,高度为0,肯定就不显示了。但是为什么其他手机是好的?所以我当然怀疑是锤子问题啊。

但是不管什么问题,总归是要解决的,所以要找到根源。通过替换LinearLayout为自定义的MyLinearLayout(什么都没做,只是在onMeasure中打了一点日志),我们发现在显示正常的手机上,该方法被调用了一次,传入的heightMeasureSpecmodeMeaureSpec.UNSPECIFIED,根据ViewGroup.getChildMeasureSpec逻辑和两个文本的布局参数可知,传入文本一和文本二的heightMeasureSpecmodeMeaureSpec.UNSPECIFIED,然后文本一和文本二就测量出了自己的高度(备注:后面会提到这个测量)

但是在那个锤子手机上,该方法被调用了2次,第二次传入的heightMeasureSpecmodeMeaureSpec.EXACTLY,根据ViewGroup.getChildMeasureSpec逻辑和两个文本的布局参数可知,传入文本一和文本二的heightMeasureSpecmode分别为MeasureSpec.EXACTLYMeasureSpec.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有机会会被再测量一次。那么条件是什么呢?

  • fillViewPorttrue
  • 测量模式不为MeasureSpec.UNSPECIFIED
  • 子布局的高度比自己的高度小。

所以,上面3个条件都满足了,触发了第二次测量,那么去深入研究一下。

4、fillViewPort

这个属性是干什么的呢?官方文档解释是,子视图会充满用户可见的区域(可以理解为子视图会填满NestedScrollView)。假如NestedScrollView的高度为100dp,而它的子View只有50dp,那么当该属性为true时,子View会被重新测量为100dp

这个属性不会改变NestedScrollView本身的测量高度。

只有当某个子视图中的某个控件需要沉底的时候,才会使用到该属性。

5、二次测量

我们仔细看一下满足条件后的二次测量做了什么,根据逻辑,当子视图高度比自己小时,NestedScrollView会让子视图重新测量一次,就是为了让子视图跟自己一样高。所以传入文本一和文本二的heightMeasureSpecmodeMeasureSpec.EXACTLY,而传入的size就是NestedScrollView的高度减去padding的值。

然后文本二的高度就被测量为0

那么为什么为0呢?查看布局,发现文本一的高度为match_parent,所以文本一的高度应该是等于LinearLayout的高度,然后没有剩余空间留给文本二了,所以导致了文本二的高度为0。

那么为什么当传入到文本一的heightMeasureSpecmodeMeasureSpec.UNSPECIFIED时,不会出问题呢?就是之前备注的那个地方,因为View的测量遵循一个准则,也就是说,当modeMeasureSpec.UNSPECIFIED时,就表明父布局并不限定子控件的尺寸,所以子控件会计算一个自己足够用的尺寸,对于TextView,可以理解为,能把所有文本显示出来,就够了,虽然是match_parent,但是并不是真的和父布局一样大。

6、问题总结

由于跑在了大屏手机,而且fillViewPorttrue,子视图高度不够NestedScrollView高度
->
触发了二次测量
->
又因为在文本二之前的文本一是match_parent
->
文本一高度和LinearLayout高度一样
->
导致了没有剩余高度留给文本二
->
文本二高度为0,内容无法显示。

7、着手修复

对上面的问题,我们可以有下面两个解决方案:

  • 既然文本一把父布局空间全部占用,导致文本二没有高度,那么把文本一高度改为wrap_content就可以。
  • 既然触发了二次测量,内容高度我们无法控制,那么把fillViewPort设为false,也就可以阻止二次测量问题。

PS:锤子莫名背了一次锅,哈哈……….

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值