内容简介:最近我在关注为了观察横屏进入
最近我在关注 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 倍
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Learn Python the Hard Way
Zed A. Shaw / Addison-Wesley Professional / 2013-10-11 / USD 39.99
Master Python and become a programmer-even if you never thought you could! This breakthrough book and CD can help practically anyone get started in programming. It's called "The Hard Way," but it's re......一起来看看 《Learn Python the Hard Way》 这本书的介绍吧!