内容简介:最近我在关注为了观察横屏进入
最近我在关注 ViewPager2
的使用,期间一直基于官方的Demo调试 android-viewpager2 ,今天遇到一个奇葩的问题,捉摸了半天最终找到原因,原来是Demo中布局的问题,事后感觉有必要分享一下这个过程,一来可以巩固View测量的知识,二来希望大家能避开这个坑;
阅读指南
- 代码基于 android-viewpager2 ,看官老爷最好能下载源码亲身体会;
入坑现场
为了观察 Fragment
的生命周期,我事先在 CardFragment
类中,对生命周期方法进行埋点Log;
异常发生的操作步骤:
横屏进入 CardFragmentActivity
或者 CardFragmentActivity
竖屏切到横屏,控制台瞬间打印多个Fragment的生命周期Log,场面让人惊呆;
CardFragmentActivity横屏下布局
控制台Log输出
由于Log太长,一屏根本截不完,反正就是很多个 Fragment
经历了 onCreate
-> onDestory
的所有过程;
操作前,只有 Fragment2
创建并显示,理论上旋转屏幕之后,只有 Fragment2
销毁并重建,不会调用其他 Fragment
;现在问题发生在了,旋转之后有一堆 Fragment
创建并且销毁,最终保留的也只有 Fragment2
,这肯定是个Bug,虽然发生在一行代码都没有改的官方Demo上;
初步原因 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的方案,也存在这个风险;
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 战疫期,钉钉如何扛起暴增百倍的流量?
- 付费用户暴增7倍,这项技术是如何对用户产生影响的?
- 应用流畅度暴增60%!华为:更多老机型将升级方舟编译器
- Visual Studio Code使用中CPU占用率异常暴增过高原因
- 全球AI人才需求两年暴增35倍 中国机器人部署量涨500%
- 斯坦福全球AI报告:清华AI课程人数增16倍,人才需求暴增 35 倍
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Pro JavaScript Design Patterns
Dustin Diaz、Ross Harmes / Apress / 2007-12-16 / USD 44.99
As a web developer, you’ll already know that JavaScript™ is a powerful language, allowing you to add an impressive array of dynamic functionality to otherwise static web sites. But there is more power......一起来看看 《Pro JavaScript Design Patterns》 这本书的介绍吧!