從原始碼角度解析 - ScrollView巢狀ViewPager不顯示的問題
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation ="vertical">
<TextView
android:id="@+id/tv_header"
android:layout_width="match_parent"
android:layout_height="300dp"
android:gravity="center"
android:text="Header"/>
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@android:color/white"
app:tabIndicatorColor="@color/colorPrimary"
app:tabIndicatorHeight ="1dp"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/colorPrimary"
app:tabTextColor="@android:color/black"/>
<ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</ScrollView>
以上佈局的方式(ScrollView巢狀ViewPager),在預設情況下是達不到我們想要的效果,整個ViewPager是無法顯示的,網上已經有很多解決方案,但很少有文章解釋為什麼要這樣修改,是什麼原因造成ViewPager顯示不了,現在我們從原始碼的角度來分析問題的所在。
既然是無法顯示,那可能是在ViewPager測量過程中出現了異常,可以先從原始碼看下ViewPager在onMeasure方法是如何構成的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// For simple implementation, our internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
……………………
省略
}
在onMeasure方法內中通過setMeasuredDimension方法設定測量的寬高,而View的測量具體值,是在getDefaultSize方法中得到的,接下來看看原始碼
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
在onMeasure方法中,getDefaultSize傳入兩個引數,一個大小size,另外一個測量規格measureSpec,而傳入的size的值是為0,在getDefaultSize方法內,可以看出在UNSPECIFIED測量模式 下,getDefaultSize返回值是0,而這個返回值就是我們想要的寬或高。所以在此猜測是UNSPECIFIED測量模式而導致的,可能有人會問,我明明在佈局中給ViewPager設定的match_parent屬性,是一個精確測量模式EXACTLY,這麼突然變成了UNSPECIFIED模式了。
我們知道ViewGroup的measure測量過程是一個遞迴過程,它會在父元素中的onMeasure方法中,遍歷所有的子元素進行對子元素逐個measure測量,而父元素的測量規格MeasureSpec同時也會影響到子元素的測量規格MeasureSpec;我們可以從ScrollView的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;
}
……省略
}
我們進入super.onMeasure方法中,再去看看具體
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//這個方法很重要,是用測量子view的大小
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
……省略
}
從上面的原始碼可以看出,ScrollView的onMeasure方法內通過for迴圈遍歷子view,通過measureChildWithMargins方法來實現子View的測量工作,我們再點進measureChildWithMargins方法內看看細節
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 = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
從上面的程式碼可知,會根據父元素的parentWidthMeasureSpec、parentHeightMeasureSpec的測量規格,得到子元素的childWidthMeasureSpec 、childHeightMeasureSpec 測量規格,在其過程中並未改變子元素的測量模式,其實這時候我們看到measureChildWithMargins這個方法是ViewGroup的標準測量過程,而ScrollView已經對measureChildWithMargins方法進行重寫了,接下來看看ScrollView的重寫後的measureChildWithMargins方法
@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,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
從原始碼可知,在構建子元素的高度測量規格childHeightMeasureSpec 時,已經把子元素的測量模式設定成了UNSPECIFIED模式了,由此可知,ViewPager的高度測量模式實際上是UNSPECIFIED,並非我們在佈局上設定match_parent屬性了,此時再去看getDefaultSize方法會很明瞭了,知道問題所在。
解決思路
給ViewPager的設定一個具體的高度值,有兩種方式
1> 通過view.post(runnable)或者ViewTreeObserver方法中設定具體值
this.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
StickHeadScrollView.this.getViewTreeObserver().removeOnGlobalLayoutListener(this);
nestedContent.getLayoutParams().height = StickHeadScrollView.this.getHeight() - headView.getHeight();
nestedContent.requestLayout();
}
});
2> 重寫ViewPager的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
int h=0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//1.測量子View的寬高
child.measure(widthMeasureSpec,MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED));
//2.獲取view的高度
int measuredHeight = child.getMeasuredHeight();
//3.取所有的子View中的高度最高的那個
if (measuredHeight>h){
h =measuredHeight;
}
Log.d(TAG, "onMeasure: "+measuredHeight);
}
//4、最後設定高度的測量模式為EXACTLY
heightMeasureSpec= MeasureSpec.makeMeasureSpec(h,MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
子元素如果是match_parent屬性,是無法得到子元素的高度值,如果想得到子元素的值,前提必須知道父元素的剩餘空間是多少,而此時父元素的剩餘空間我們是無法獲取的,因為在父元素中onMeasure方法中,還在遍歷測量子元素,子元素都還在測量中,是獲取不到父元素的具體值的。