• Android:ScrollView和SwipeRefreshLayout高度测量


    今天组里的同事要做一个奇葩的效果,要求在ScrollView里嵌套一个RefreshLayout。类似代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        //红色背景
    
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ff00ff">
    
                //黄色背景
    
                <android.support.v4.widget.SwipeRefreshLayout
                    android:layout_width="match_parent"
                    android:layout_height="fill_parent"
                    android:background="#ffff00">
    
                    //黑色背景
                    <LinearLayout
                        android:layout_width="match_parent"
                        android:background="#000000"
                        android:layout_height="100dp" />
                </android.support.v4.widget.SwipeRefreshLayout>
        </ScrollView>
    </LinearLayout>

    期望效果是这样的:

    (蓝色部分是ToolsBar,请忽略)

    而实际效果是这样的:

    好奇怪,明明设置了SwipeRefreshLayout的高度是fill_parent,为何完全不显示?要知道,在SwipeRefreshLayout内部还设置了一个高度为100dp的LinearLayout,正常来说SwipeRefreshLayout最少也占据了100dp的高度啊,现在的高度居然为0。

    这个问题得分两部分说明:

    1.ScrollView的高度测量。

    2.SwipeRefreshLayout的高度测量。

    在这之前,先简单介绍一下View测量的三种模式(mode),具体的关于View的测量流程不细说,可到网上找些资料。

    AT_MOST:最大尺寸模式,一般设置为wrap_content时会使用该模式测量,子View不会超过父View给与的最大宽高。

    EXACTLY:精确模式,一般设置为fill_parent时会使用该测量模式,父View给与多少宽高,子View就使用多少。

    UNSPECIFIED:未指定模式,子View测量出来有多大就有多大,不受父View给与的宽高影响。

    关于ScrollView的高度测量,是在这篇文章中找到原因和解决方案的(老外的,需要翻墙)。重点在于fillViewport属性。我们先把布局文件中的SwipeRefreshLayout换成LinearLayout。看下效果

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        //红色背景
    
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ff00ff">
    
            //黄色背景
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="fill_parent"
                android:background="#ffff00">
    
                //黑色背景
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#000000" />
            </LinearLayout>
        </ScrollView>
    </LinearLayout>

    我们会发现,尽管已经将ScrollView内部的LinearLayout设置成fill_parent,它的高度仍旧只有100dp。接下来将ScrollView的fillViewport设置为true。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        //红色背景
    
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true"
            android:background="#ff00ff">
    
            //黄色背景
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="fill_parent"
                android:background="#ffff00">
    
                //黑色背景
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#000000" />
            </LinearLayout>
        </ScrollView>
    </LinearLayout>

    正常了,LinearLayout填充满了ScrollView的高度。

    接下去看看ScrollView的和测量相关的几个方法:

     @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,
                            mPaddingLeft + mPaddingRight, lp.width);
                    height -= mPaddingTop;
                    height -= mPaddingBottom;
                    int childHeightMeasureSpec =
                            MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    
                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
                }
            }
        }
    
    @Override
        protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
            ViewGroup.LayoutParams lp = child.getLayoutParams();
    
            int childWidthMeasureSpec;
            int childHeightMeasureSpec;
    
            childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
                    + mPaddingRight, lp.width);
    
            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    
    @Override
        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 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }

    看下源码就会知道,ScrollView继承于FrameLayout,所以super.onMeasure(widthMeasureSpec, heightMeasureSpec);就是执行了FrameLayout的高度测量方法。但是这里重写了measureChildWithMargins方法,在这一步中,将可分配高度设置成了MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED),这导致了子View的fill_parent无效了。(这一步不懂得可以看下View测量的原理和MeasureSpec的几种mode)。

    所以在super.onMeasure后,子View只能获取到它本身的高度(子View的子View的最大高度),但假如将fillViewport设置为true,ScrollView又会调用另外一步测量:

    1.获取第一个子View,在此就是上述xml文件中的LinearLayout

    2.获取ScrollView高度,由于设置了ScrollView高度为fill_parent,因此就是屏幕高度。

    2.获取子View被测量后的高度(前面通过super.onMeasure测量获取),假如子View的高度小于ScrollView高度,会进行第二次的测量,这次测量的参数是这样的:int childHeightMeasureSpec =

                            MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

    即子View能分配的高度为ScrollView自身高度。

    这样,当ScrollView的子View为LinearLayout时,只要设置fillViewort为true,就能实现我们想要的效果了,但是改为SwipeRefreshLayout看看

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        //红色背景
    
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true"
            android:background="#ff00ff">
    
            //黄色背景
    
            <android.support.v4.widget.SwipeRefreshLayout
                android:layout_width="match_parent"
                android:layout_height="fill_parent"
                android:background="#ffff00">
    
                //黑色背景
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#000000" />
            </android.support.v4.widget.SwipeRefreshLayout>
        </ScrollView>
    </LinearLayout>

    居然全黑了!那就表示SwipeRefreshLayout内部的LinearLayout变成了fill_parent了,但是它的height是固定的100dp啊。

    好吧,继续看SwipeRefreshLayout的onMeasure方法:

    @Override
        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            if (mTarget == null) {
                ensureTarget();
            }
            if (mTarget == null) {
                return;
            }
            mTarget.measure(MeasureSpec.makeMeasureSpec(
                    getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                    MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                    getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
            mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
           ....
        }

    这里的mTarget就是SwipeRefreshLayout的第一个子View,即我们上述xml中的LinearLayout。我们可以看到这里传入的高度是

     MeasureSpec.makeMeasureSpec(
                    getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)

    很明显,子View会被强制设置成SwipeRefreshLayout的高度。

    在此所有问题都解决了,来个总结:

    1.ScrollView内部的View的测量方法会默认设置成UNSPECIFIED,这种情况下,子View会根据自身实际高度显示,所以设置fill_parent是无效的。

    2.设置ScrollView的fillViewport属性为true,这种情况下,在子View高度小于父View高度时,会重新进行一次高度测量,并且强行将子View高度设置为父View高度。而子View高度大于父View高度时,不受影响。即原来没满屏会强行变成强行满屏,原来满屏了可以继续滚动。

    3.SwipeRefreshLayout的高度测量方法,会强行将子View设置成自身高度。

    最后,ScrollView+SwipeRefreshLayout的嵌套组合是种很蠢得方法。。。在我们解决了上面的问题后,直接摒弃了。仅当学习新知识了。

  • 相关阅读:
    理解RESTful架构
    redis 未授权漏洞利用直接登录服务器
    初创公司应该如何做好持续集成和部署?
    Redis 作为缓存服务器的配置
    自己写的轻量级PHP框架trig与laravel5.1,yii2性能对比
    利用SecureCRT上传、下载文件(使用sz与rz命令)
    ZendStudio10 代码格式化 xml
    LESS CSS 框架简介
    为什么浏览器User-agent总是有Mozilla字样
    在 JavaScript 中 prototype 和 __proto__ 有什么区别
  • 原文地址:https://www.cnblogs.com/linjzong/p/5221604.html
Copyright © 2020-2023  润新知