前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【ViewPager2避坑系列】瞬间暴增数个Fragment

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

作者头像
Rouse
发布2019-07-30 17:17:25
7.4K0
发布2019-07-30 17:17:25
举报
文章被收录于专栏:Android补给站

作者:HitenDev 链接:https://juejin.im/post/5ce15668f265da1bbf68ed52

1

前言

最近我在关注ViewPager2的使用,期间一直基于官方的Demo调试android-viewpager2

https://github.com/googlesamples/android-viewpager2

今天遇到一个奇葩的问题,捉摸了半天最终找到原因,原来是Demo中布局的问题,事后感觉有必要分享一下这个过程,一来可以巩固View测量的知识,二来希望大家能避开这个坑

代码基于android-viewpager2,看官老爷最好能下载源码亲身体会

2

入坑现场

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

异常发生的操作步骤:

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

CardFragmentActivity横屏下布局
控制台Log输出

由于Log太长,一屏根本截不完,反正就是很多个Fragment经历了onCreate->onDestory的所有过程;

操作前,只有Fragment2创建并显示,理论上旋转屏幕之后,只有Fragment2销毁并重建,不会调用其他Fragment;现在问题发生在了,旋转之后有一堆Fragment创建并且销毁,最终保留的也只有Fragment2,这肯定是个Bug,虽然发生在一行代码都没有改的官方Demo上;

3

初步原因MATCH_PARENT计算失效

ViewPager2目前只支持ItemView的布局参数是MATCH_PARENT,就是填充父布局的效果;由于ViewPager2是基于RecyclerView,理论上每个ItemView一定会是MATCH_PARENT,控制一屏只加载一个Item,但是一旦MATCH_PARENT计算失效,那么ViewPager2基本上就是RecyclerView的效果,瞬间多个Fragment是可以解释通的;

3.1

ViewPager2测量流程

ViewPager2
代码语言:javascript
复制
代码语言:javascript
复制
@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
代码语言:javascript
复制
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);
    }
}
获取宽高测量信息的代码
代码语言:javascript
复制
代码语言:javascript
复制
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上;

4

如何产生的UNSPECIFIED

代码语言:javascript
复制
代码语言:javascript
复制
<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
代码语言:javascript
复制
代码语言:javascript
复制
@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方式测量;

4.1

为何还要执行一次MATCH_PARENT测量

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

4.2

为何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()
代码语言:javascript
复制
代码语言:javascript
复制
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;

4.3

为何整体会测量两遍

这是由于FrameLayout针对MATCH_PARENT的布局,会进行二次测量,第一次测量为了找到最大尺寸maxsize,二次测量把用maxsize从新计算MATCH_PARENT的子View;

5

避免入坑

上诉讲解就是为了说明,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;

6

总结

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 异常发生的操作步骤:
  • CardFragmentActivity横屏下布局
  • 控制台Log输出
  • ViewPager2
  • RecyclerView.LayoutManager
  • 获取宽高测量信息的代码
  • RecyclerView.onMeasure输出日志
  • LinearLayout
  • FrameLayout
  • resolveSizeAndState()
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档