摘要:前言最近我在關(guān)注的使用,期間一直基于官方的調(diào)試,今天遇到一個(gè)奇葩的問(wèn)題,捉摸了半天最終找到原因,原來(lái)是中布局的問(wèn)題,事后感覺(jué)有必要分享一下這個(gè)過(guò)程,一來(lái)可以鞏固測(cè)量的知識(shí),二來(lái)希望大家能避開(kāi)這個(gè)坑閱讀指南代碼基于,看官老爺最好能下載
前言
最近我在關(guān)注ViewPager2的使用,期間一直基于官方的Demo調(diào)試android-viewpager2,今天遇到一個(gè)奇葩的問(wèn)題,捉摸了半天最終找到原因,原來(lái)是Demo中布局的問(wèn)題,事后感覺(jué)有必要分享一下這個(gè)過(guò)程,一來(lái)可以鞏固View測(cè)量的知識(shí),二來(lái)希望大家能避開(kāi)這個(gè)坑;
閱讀指南
代碼基于android-viewpager2,看官老爺最好能下載源碼親身體會(huì);
入坑現(xiàn)場(chǎng)
為了觀察Fragment的生命周期,我事先在CardFragment類(lèi)中,對(duì)生命周期方法進(jìn)行埋點(diǎn)Log;
異常發(fā)生的操作步驟:
橫屏進(jìn)入CardFragmentActivity或者CardFragmentActivity豎屏切到橫屏,控制臺(tái)瞬間打印多個(gè)Fragment的生命周期Log,場(chǎng)面讓人驚呆;
CardFragmentActivity橫屏下布局
控制臺(tái)Log輸出
由于Log太長(zhǎng),一屏根本截不完,反正就是很多個(gè)Fragment經(jīng)歷了onCreate->onDestory的所有過(guò)程;
操作前,只有Fragment2創(chuàng)建并顯示,理論上旋轉(zhuǎn)屏幕之后,只有Fragment2銷(xiāo)毀并重建,不會(huì)調(diào)用其他Fragment;現(xiàn)在問(wèn)題發(fā)生在了,旋轉(zhuǎn)之后有一堆Fragment創(chuàng)建并且銷(xiāo)毀,最終保留的也只有Fragment2,這肯定是個(gè)Bug,雖然發(fā)生在一行代碼都沒(méi)有改的官方Demo上;
初步原因MATCH_PARENT計(jì)算失效
ViewPager2目前只支持ItemView的布局參數(shù)是MATCH_PARENT,就是填充父布局的效果;由于ViewPager2是基于RecyclerView,理論上每個(gè)ItemView一定會(huì)是MATCH_PARENT,控制一屏只加載一個(gè)Item,但是一旦MATCH_PARENT計(jì)算失效,那么ViewPager2基本上就是RecyclerView的效果,瞬間多個(gè)Fragment是可以解釋通的;
ViewPager2測(cè)量流程
ViewPager2
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //測(cè)量mRecyclerView measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec); int width = mRecyclerView.getMeasuredWidth(); int height = mRecyclerView.getMeasuredHeight(); int childState = mRecyclerView.getMeasuredState(); //寬高計(jì)算 width += getPaddingLeft() + getPaddingRight(); height += getPaddingTop() + getPaddingBottom(); //寬高約束 width = Math.max(width, getSuggestedMinimumWidth()); height = Math.max(height, getSuggestedMinimumHeight()); //設(shè)置自身高度 setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), resolveSizeAndState(height, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); }
ViewPager2.onMeasure()優(yōu)先計(jì)算mRecyclerView的尺寸,所以關(guān)注的重點(diǎn)轉(zhuǎn)移到RecyclerView.onMeasure()上,RecyclerView對(duì)子View的計(jì)算和布局邏輯在LayoutManager中,所以本例子重要看LinearLayoutManager,LayoutManager對(duì)子View計(jì)算的方法是measureChildWithMargins(),下面看一下measureChildWithMargins()方法的調(diào)用棧;
主要分析measureChildWithMargins()代碼:
RecyclerView.LayoutManager
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); //獲取當(dāng)前View的Decor(傳統(tǒng)理解的分割線)尺寸 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; //獲取寬測(cè)量信息 final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, canScrollHorizontally()); //獲取高測(cè)量信息 final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, canScrollVertically()); //如果需要測(cè)量,調(diào)用child的測(cè)量方法 if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); } }
獲取寬高測(cè)量信息的代碼:
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強(qiáng)制設(shè)置MATCH_PARENT,所以childDimension肯定是MATCH_PARENT,那么resultMode是什么呢,通過(guò)斷點(diǎn)打印輸出,這里的parentMode是MeasureSpec.UNSPECIFIED和MeasureSpec.EXACTLY交替出現(xiàn);
剛開(kāi)始一直在關(guān)注子View計(jì)算流程,發(fā)現(xiàn)MeasureSpecMode異常,總是出現(xiàn)MeasureSpec.UNSPECIFIED和MeasureSpec.EXACTLY交替,最后直接打印RecyclerView的onMeasure輸出;
RecyclerView.onMeasure輸出日志
在豎屏?xí)r,widthMeasureMode一直都是1073741824(MATCH_PARENT),但是橫屏狀態(tài)下,widthMeasureMode在0(UNSPECIFIED)和MATCH_PARENT中徘徊;對(duì)比差別就是MeasureMode = UNSPECIFIED,所以問(wèn)題應(yīng)該出在MeasureMode = UNSPECIFIED上;
如何產(chǎn)生的UNSPECIFIED?
整體布局是LinearLayout,在布局里面,ViewPager2 layout_width="0dp" layout_weight="1",可能是width=0dp && weight=1造成,扒一扒LinearLayout測(cè)量代碼邏輯;
LinearLayout
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
LinearLayout的onMeasure()方法分為豎直方向和水平方向,我們這里選擇measureHorizontal()入手;
measureHorizontal()方法中通過(guò)判斷lp.width == 0 && lp.weight > 0斷定是否需要過(guò)渡加載useExcessSpace,下面的過(guò)渡加載就是采用UNSPECIFIED方式測(cè)量;
為何還要執(zhí)行一次MATCH_PARENT測(cè)量
這是由于LinearLayout的measureHorizontal()針對(duì)過(guò)渡加載useExcessSpace的布局,會(huì)進(jìn)行兩次測(cè)量,第二次就會(huì)傳遞實(shí)際的測(cè)量模式;
為何UNSPECIFIED模式下,MATCH_PARENT會(huì)失效
我們暫時(shí)只討論FrameLayout的情況,如果FrameLayout的父布局給該FrameLayout的測(cè)量模式是UNSPECIFIED,尺寸是自身的具體寬高,而且該FrameLayout的LayoutParams是MATCH_PARENT,試問(wèn)FrameLayout能測(cè)量出準(zhǔn)確的MATCH_PARENT尺寸嗎?
FrameLayout
FrameLayout會(huì)測(cè)量所有可見(jiàn)View的尺寸,然后算出最大的尺寸maxWidth和maxHeight,自身尺寸的測(cè)量調(diào)用setMeasuredDimension()方法,每個(gè)Dimension的設(shè)置調(diào)用resolveSizeAndState(maxWidth, widthMeasureSpec, childState)方法;
resolveSizeAndState()
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { //來(lái)自父布局建議的模式和尺寸 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;//這個(gè)size就是傳入的size } return result | (childMeasuredState & MEASURED_STATE_MASK); }
分析resolveSizeAndState(),如果measureSpec的specMode=UNSPECIFIED,結(jié)果返回傳入的size,在FrameLayout中是maxWidth和maxHeight,而并不是parent給予的specSize;
為何整體會(huì)測(cè)量?jī)杀?/p>
這是由于FrameLayout針對(duì)MATCH_PARENT的布局,會(huì)進(jìn)行二次測(cè)量,第一次測(cè)量為了找到最大尺寸maxsize,二次測(cè)量把用maxsize從新計(jì)算MATCH_PARENT的子View;
避免入坑
上訴講解就是為了說(shuō)明,UNSPECIFIED會(huì)影響MATCH_PARENT的測(cè)量,至少在FrameLayout上是影響的,FrameLayout會(huì)采取子View的最大尺寸,一旦失去MATCH_PARENT的意義,ViewPager2就失去了ItemView一屏顯示一個(gè)的特性,所以會(huì)出現(xiàn)開(kāi)頭說(shuō)的瞬間暴增多個(gè)Fragment現(xiàn)象;
由于ViewPager2配合Fragment使用時(shí),根布局是FrameLayout這個(gè)無(wú)法改變,解決辦法就是不允許出現(xiàn)跟滑動(dòng)方向相同的維度測(cè)量上,出現(xiàn)UNSPECIFIED;
如果父布局是LinearLayout,橫向滑動(dòng)時(shí)要避免layout_width="0dp"和layout_weight="1",縱向滑動(dòng)時(shí)要避免layout_height="0dp"和layout_weight="1",代碼的解決方案很簡(jiǎn)單,去掉layout_weight="1",吧layout_width設(shè)置成match_parent;
總結(jié)
注意ViewPager2配合Fragment使用時(shí),一旦發(fā)現(xiàn)Fragment瞬間暴增的情況,可能是Item尺寸測(cè)量的不對(duì),造成這個(gè)原因要優(yōu)先想到UNSPECIFIED,·如果用的LinearLayout可能是layout_weight="1"的原因,同理,RecyclerView+PagerSnapHelper+match_parent實(shí)現(xiàn)一屏一個(gè)Item的方案,也存在這個(gè)風(fēng)險(xiǎn);
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/7033.html
摘要:前言最近發(fā)布了版本,新增功能,該功能在上并不友好,現(xiàn)在官方將此功能延續(xù)下來(lái),這回是騾子是馬呢趕緊拉出來(lái)溜溜閱讀指南內(nèi)容基于版本講解,由于正式版還未發(fā)布,如有功能變動(dòng)有勞看官指出內(nèi)容重點(diǎn)介紹的特性和預(yù)加載機(jī)制,另外包括的狀態(tài)和的生命周前言 最近ViewPager2發(fā)布了1.0.0-alpha04版本,新增offscreenPageLimit功能,該功能在ViewPager上并不友好,現(xiàn)在官方將...
摘要:前言寫(xiě)上一篇軟文時(shí),我發(fā)現(xiàn)最新的代碼淘汰了方法,轉(zhuǎn)而支持用方法,言外之意是設(shè)置最大生命周期,懂行的人應(yīng)該知道,一直都是無(wú)法直接設(shè)置生命周期,必須通過(guò)方法間接干預(yù),本來(lái)就此功能,簡(jiǎn)單介紹一下的原理和上手效果閱讀指南本文基于版本的進(jìn)行,也是支前言 寫(xiě)上一篇ViewPager2軟文時(shí),我發(fā)現(xiàn)最新的Fragment代碼淘汰了setUserVisibleHint方法,轉(zhuǎn)而支持用setMaxLifecy...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00