最近我在關注ViewPager2
的使用,期間一直基於官方的Demo調試android-viewpager2,今天遇到一個奇葩的問題,捉摸了半天最終找到緣由,原來是Demo中佈局的問題,過後感受有必要分享一下這個過程,一來能夠鞏固View測量的知識,二來但願你們能避開這個坑;android
閱讀指南git
爲了觀察Fragment
的生命週期,我事先在CardFragment
類中,對生命週期方法進行埋點Log;github
異常發生的操做步驟:bash
橫屏進入CardFragmentActivity
或者CardFragmentActivity
豎屏切到橫屏,控制檯瞬間打印多個Fragment的生命週期Log,場面讓人驚呆;ide
CardFragmentActivity橫屏下佈局佈局
控制檯Log輸出ui
因爲Log太長,一屏根本截不完,反正就是不少個Fragment
經歷了onCreate
->onDestory
的全部過程;google
操做前,只有Fragment2
建立並顯示,理論上旋轉屏幕以後,只有Fragment2
銷燬並重建,不會調用其餘Fragment
;如今問題發生在了,旋轉以後有一堆Fragment
建立而且銷燬,最終保留的也只有Fragment2
,這確定是個Bug,雖然發生在一行代碼都沒有改的官方Demo上;spa
MATCH_PARENT計算失效
ViewPager2
目前只支持ItemView
的佈局參數是MATCH_PARENT
,就是填充父佈局的效果;因爲ViewPager2
是基於RecyclerView
,理論上每一個ItemView
必定會是MATCH_PARENT
,控制一屏只加載一個Item
,可是一旦MATCH_PARENT
計算失效,那麼ViewPager2
基本上就是RecyclerView
的效果,瞬間多個Fragment
是能夠解釋通的;調試
ViewPager2
測量流程ViewPager2
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//測量mRecyclerView
measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec);
int width = mRecyclerView.getMeasuredWidth();
int height = mRecyclerView.getMeasuredHeight();
int childState = mRecyclerView.getMeasuredState();
//寬高計算
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
//寬高約束
width = Math.max(width, getSuggestedMinimumWidth());
height = Math.max(height, getSuggestedMinimumHeight());
//設置自身高度
setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
resolveSizeAndState(height, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
複製代碼
ViewPager2.onMeasure()
優先計算mRecyclerView
的尺寸,因此關注的重點轉移到RecyclerView.onMeasure()
上,RecyclerView
對子View
的計算和佈局邏輯在LayoutManager
中,因此本例子重要看LinearLayoutManager
,LayoutManager
對子View計算的方法是measureChildWithMargins()
,下面看一下measureChildWithMargins()
方法的調用棧;
主要分析measureChildWithMargins()
代碼:
RecyclerView.LayoutManager
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//獲取當前View的Decor(傳統理解的分割線)尺寸
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//獲取寬測量信息
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
//獲取高測量信息
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
//若是須要測量,調用child的測量方法
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
複製代碼
獲取寬高測量信息的代碼:
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
//省略
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
分析getChildMeasureSpec()
方法,因爲ViewPager2
強制設置MATCH_PARENT
,因此childDimension
確定是MATCH_PARENT
,那麼resultMode
是什麼呢,經過斷點打印輸出,這裏的parentMode
是MeasureSpec.UNSPECIFIED
和MeasureSpec.EXACTLY
交替出現;
剛開始一直在關注子View計算流程,發現MeasureSpecMode
異常,老是出現MeasureSpec.UNSPECIFIED
和MeasureSpec.EXACTLY
交替,最後直接打印RecyclerView
的onMeasure
輸出;
RecyclerView.onMeasure輸出日誌
在豎屏時,widthMeasureMode
一直都是1073741824(MATCH_PARENT
),可是橫屏狀態下,widthMeasureMode
在0(UNSPECIFIED
)和MATCH_PARENT
中徘徊;對比差異就是MeasureMode = UNSPECIFIED
,因此問題應該出在MeasureMode = UNSPECIFIED
上;
UNSPECIFIED
?<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:background="#FFFFFF">
<include layout="@layout/controls" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
複製代碼
總體佈局是LinearLayout
,在佈局裏面,ViewPager2 layout_width="0dp" layout_weight="1"
,多是width=0dp && weight=1
形成,扒一扒LinearLayout
測量代碼邏輯;
LinearLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
複製代碼
LinearLayout
的onMeasure()
方法分爲豎直方向和水平方向,咱們這裏選擇measureHorizontal()
入手;
measureHorizontal()
方法中經過判斷lp.width == 0 && lp.weight > 0
判定是否須要過渡加載useExcessSpace
,下面的過渡加載就是採用UNSPECIFIED
方式測量;
MATCH_PARENT
測量這是因爲LinearLayout
的measureHorizontal()
針對過渡加載useExcessSpace
的佈局,會進行兩次測量,第二次就會傳遞實際的測量模式;
UNSPECIFIED
模式下,MATCH_PARENT
會失效咱們暫時只討論FrameLayout
的狀況,若是FrameLayout
的父佈局給該FrameLayout
的測量模式是UNSPECIFIED
,尺寸是自身的具體寬高,並且該FrameLayout
的LayoutParams
是MATCH_PARENT
,試問FrameLayout
能測量出準確的MATCH_PARENT
尺寸嗎?
FrameLayout
FrameLayout
會測量全部可見View
的尺寸,而後算出最大的尺寸maxWidth
和maxHeight
,自身尺寸的測量調用setMeasuredDimension()
方法,每一個Dimension
的設置調用resolveSizeAndState(maxWidth, widthMeasureSpec, childState)
方法;
resolveSizeAndState()
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//來自父佈局建議的模式和尺寸
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {//父佈局建議的模式
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED://在這裏
default:
result = size;//這個size就是傳入的size
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製代碼
分析resolveSizeAndState()
,若是measureSpec
的specMode=UNSPECIFIED
,結果返回傳入的size
,在FrameLayout
中是maxWidth
和maxHeight
,而並非parent給予的specSize;
這是因爲FrameLayout
針對MATCH_PARENT
的佈局,會進行二次測量,第一次測量爲了找到最大尺寸maxsize
,二次測量把用maxsize
重新計算MATCH_PARENT
的子View;
上訴講解就是爲了說明,UNSPECIFIED
會影響MATCH_PARENT
的測量,至少在FrameLayout
上是影響的,FrameLayout
會採起子View的最大尺寸,一旦失去MATCH_PARENT
的意義,ViewPager2
就失去了ItemView
一屏顯示一個的特性,因此會出現開頭說的瞬間暴增多個Fragment
現象;
因爲ViewPager2
配合Fragment
使用時,根佈局是FrameLayout
這個沒法改變,解決辦法就是不容許出現跟滑動方向相同的維度測量上,出現UNSPECIFIED
;
若是父佈局是LinearLayout
,橫向滑動時要避免layout_width="0dp"和layout_weight="1"
,縱向滑動時要避免layout_height="0dp"和layout_weight="1"
,代碼的解決方案很簡單,去掉layout_weight="1"
,吧layout_width
設置成match_parent
;
注意ViewPager2
配合Fragment
使用時,一旦發現Fragment
瞬間暴增的狀況,多是Item尺寸測量的不對,形成這個緣由要優先想到UNSPECIFIED
,·若是用的LinearLayout
多是layout_weight="1"
的緣由,同理,RecyclerView
+PagerSnapHelper
+match_parent
實現一屏一個Item的方案,也存在這個風險;