【ViewPager2避坑系列】瞬间暴增多个Fragment

栏目: Android · 发布时间: 5年前

内容简介:最近我在关注为了观察横屏进入

最近我在关注 ViewPager2 的使用,期间一直基于官方的Demo调试 android-viewpager2 ,今天遇到一个奇葩的问题,捉摸了半天最终找到原因,原来是Demo中布局的问题,事后感觉有必要分享一下这个过程,一来可以巩固View测量的知识,二来希望大家能避开这个坑;

阅读指南

入坑现场

为了观察 Fragment 的生命周期,我事先在 CardFragment 类中,对生命周期方法进行埋点Log;

异常发生的操作步骤:

横屏进入 CardFragmentActivity 或者 CardFragmentActivity 竖屏切到横屏,控制台瞬间打印多个Fragment的生命周期Log,场面让人惊呆;

CardFragmentActivity横屏下布局

【ViewPager2避坑系列】瞬间暴增多个Fragment

控制台Log输出

【ViewPager2避坑系列】瞬间暴增多个Fragment

由于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() 方法的调用栈;

【ViewPager2避坑系列】瞬间暴增多个Fragment

主要分析 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 是什么呢,通过断点打印输出,这里的 parentModeMeasureSpec.UNSPECIFIEDMeasureSpec.EXACTLY 交替出现;

刚开始一直在关注子View计算流程,发现 MeasureSpecMode 异常,总是出现 MeasureSpec.UNSPECIFIEDMeasureSpec.EXACTLY 交替,最后直接打印 RecyclerViewonMeasure 输出;

RecyclerView.onMeasure输出日志

【ViewPager2避坑系列】瞬间暴增多个Fragment

在竖屏时, 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);
    }
}
复制代码

LinearLayoutonMeasure() 方法分为竖直方向和水平方向,我们这里选择 measureHorizontal() 入手;

【ViewPager2避坑系列】瞬间暴增多个Fragment

measureHorizontal() 方法中通过判断 lp.width == 0 && lp.weight > 0 断定是否需要过渡加载 useExcessSpace ,下面的过渡加载就是采用 UNSPECIFIED 方式测量;

为何还要执行一次 MATCH_PARENT 测量

这是由于 LinearLayoutmeasureHorizontal() 针对过渡加载 useExcessSpace 的布局,会进行两次测量,第二次就会传递实际的测量模式;

为何 UNSPECIFIED 模式下, MATCH_PARENT 会失效

我们暂时只讨论 FrameLayout 的情况,如果 FrameLayout 的父布局给该 FrameLayout 的测量模式是 UNSPECIFIED ,尺寸是自身的具体宽高,而且该 FrameLayoutLayoutParamsMATCH_PARENT ,试问 FrameLayout 能测量出准确的 MATCH_PARENT 尺寸吗?

FrameLayout

【ViewPager2避坑系列】瞬间暴增多个Fragment

FrameLayout 会测量所有可见 View 的尺寸,然后算出最大的尺寸 maxWidthmaxHeight ,自身尺寸的测量调用 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() ,如果 measureSpecspecMode=UNSPECIFIED ,结果返回传入的 size ,在 FrameLayout 中是 maxWidthmaxHeight ,而并不是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

总结

注意 ViewPager2 配合 Fragment 使用时,一旦发现 Fragment 瞬间暴增的情况,可能是Item尺寸测量的不对,造成这个原因要优先想到 UNSPECIFIED ,·如果用的 LinearLayout 可能是 layout_weight="1" 的原因,同理, RecyclerView + PagerSnapHelper + match_parent 实现一屏一个Item的方案,也存在这个风险;


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python Cookbook 中文版,第 3 版

Python Cookbook 中文版,第 3 版

David M. Beazley、Brian K. Jones / 陈舸 / 人民邮电出版社 / 2015-5-1 / 108.00元

《Python Cookbook(第3版)中文版》介绍了Python应用在各个领域中的一些使用技巧和方法,其主题涵盖了数据结构和算法,字符串和文本,数字、日期和时间,迭代器和生成器,文件和I/O,数据编码与处理,函数,类与对象,元编程,模块和包,网络和Web编程,并发,实用脚本和系统管理,测试、调试以及异常,C语言扩展等。 本书覆盖了Python应用中的很多常见问题,并提出了通用的解决方案。......一起来看看 《Python Cookbook 中文版,第 3 版》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具