内容简介:最近我在关注为了观察横屏进入
最近我在关注 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 倍
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
算法导论
[美] Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest、Clifford Stein / 高等教育出版社 / 2002-5 / 68.00元
《算法导论》自第一版出版以来,已经成为世界范围内广泛使用的大学教材和专业人员的标准参考手册。 这本书全面论述了算法的内容,从一定深度上涵盖了算法的诸多方面,同时其讲授和分析方法又兼顾了各个层次读者的接受能力。各章内容自成体系,可作为独立单元学习。所有算法都用英文和伪码描述,使具备初步编程经验的人也可读懂。全书讲解通俗易懂,且不失深度和数学上的严谨性。第二版增加了新的章节,如算法作用、概率分析......一起来看看 《算法导论》 这本书的介绍吧!