【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的方案,也存在这个风险;


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

查看所有标签

猜你喜欢:

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

Pro JavaScript Design Patterns

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》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

SHA 加密
SHA 加密

SHA 加密工具

html转js在线工具
html转js在线工具

html转js在线工具