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的嵌套組合是種很蠢得方法。。。在我們解決了上面的問題后,直接摒棄了。僅當學習新知識了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM