今天組里的同事要做一個奇葩的效果,要求在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的嵌套組合是種很蠢得方法。。。在我們解決了上面的問題后,直接摒棄了。僅當學習新知識了。