5年了,ViewPager2 终于支持 overScrollMode,没错,我干的。

这两周给 androidx 做了一点微小的贡献,可算是把多年来的一个小坑给填上了,今天有时间就写一篇文章,详细记录一下整个过程。

首先,什么问题

我从 ViewPager2 这个组件还在 alpha 阶段的时候就已经开始使用了,一直以来它都存在一个问题,就是会忽略开发者设置的 overScrollMode 属性,不管你在 xml 还是在代码里设置,都不好使。什么是 overScrollMode 属性?看下图:

图片

这个图是我在网上找的,不算准确,但开发者一看就能明白什么意思。在 Android 的控件里,无论是上下滑动的列表,还是左右滑动的 ViewPager,当你滑到头之后,再次同方向滑动,就会出现一个水波纹一样的效果(如果运用了 Material Design,则是果冻效应一样的效果),这个效果被称为 ripple,用来告诉用户列表已经到头了,没有了,到底了,你该往回滑了。

这个效果的本意是好的,我个人也非常喜欢,但是不知道为什么,国内的设计师们似乎都不太喜欢。4年来,我经手了无数个项目,就没有哪个项目设计师让把这个效果留着的,统统都要求去掉。

在 ViewPager2 出来之前,大家都在用 ViewPager,要去掉这个效果非常简单,只需要多设置一行属性即可:

<androidx.viewpager.widget.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:overScrollMode="never"/>

而当你迁移到 ViewPager2 之后,如果同样的方式设置这个属性,你会发现这个属性失效了。有同学会怀疑可能是 xml 初始化的问题,于是跑去代码里再设置一次,会发现同样无效。

Google,你做了啥

说起来也是哭笑不得,这个问题最早在社区被提出,已经是5年前了。

彼时 androidx 还在 github 积极开发,有开发者发现了这个问题,先提了 issue(至今还是 Open 状态),而后过了大半年没人管,大家觉得可能这样还不够引起重视,于是有人直接提到了 Issue tacker,这个 Google 内部拿来跟踪 bug 的。

然后一恍就是5年,5年了,没人管。

Read the FXXKING SOURCE CODE

要找到这个问题的本质原因,肯定还是得看代码。

既然是 ViewPager 是好的,而 ViewPager2不行,我们没理由不去看一下前者的代码:

<androidx.viewpager.widget.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_pare再@NonNull
public EdgeEffect mLeftEdge;
@NonNull
public EdgeEffect mRightEdge;

@Override
public void draw(@NonNull Canvas canvas) {
    super.draw(canvas);
    boolean needsInvalidate = false;

    final int overScrollMode = getOverScrollMode();
    if (overScrollMode == View.OVER_SCROLL_ALWAYS
            || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS
                    && mAdapter != null && mAdapter.getCount() > 1)) {
        if (!mLeftEdge.isFinished()) {
            // ...
        }
        if (!mRightEdge.isFinished()) {
            // ...
        }
    } else {
        mLeftEdge.finish();
        mRightEdge.finish();
    }
nt"
    android:layout_height="match_parent"
    android:overScrollMode="never"/>

可以清楚地看到,在 ViewPager 维持了2个EdgeEffect 对象,分别对应左右的 OverScroll 效果,在 draw() 方法,会根据获取到的 getOverScrollMode() 来决定要不要绘制。

那么 ViewPager2 是怎么做的?打开它的类,搜索 getOverScrollMode(),居然是 0 results。

图片

我们都知道 ViewPager2 继承的是 ViewGroup,本质是靠内部维护的一个 RecyclerView 来实现的,而 RecyclerView 是对 overScrollMode 有处理的,如果你不想在一个列表上见到 ripple 效果,只要对应设置即可,这部分逻辑在 RecyclerView 源码的 androidx.recyclerview.widget.RecyclerView.ViewFlinger 部分:

// Based on movement, we may want to trigger the hiding of existing over scroll
// glows.
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
    considerReleasingGlowsOnScroll(unconsumedX, unconsumedY);
}

社区是如何建议的

在知道了原理之后,社区已经有一些开发者给出了 workaround,比如下面这种:

View child = viewPager2.getChildAt(0);
if (child instanceof RecyclerView) {
    child.setOverScrollMode(View.OVER_SCROLL_NEVER);
}

一目了然,既然已经知道了原理,我们只要获取到 ViewPager2 的第一个子 View,那必定是内部的这个 RecyclerView,然后再对它调用 setOverScrollMode(View.OVER_SCROLL_NEVER) 即可。

或者,如果你像我一样喜欢 Kotlin,我会直接给 ViewPager2 扩展一个方法,这个方法已经被我在无数个项目之间拷来拷去了:

fun ViewPager2.setOverScrollModeExt(overScrollMode: Int) {
    val view = getChildAt(0)
    if (view is RecyclerView) {
        (view as RecyclerView).overScrollMode = overScrollMode
    }
}

这个解决方案一定程度还是比较稳妥的,但有一个很大风险点就是它假定了一个前提,那就是:

“ViewPager2 的第一个子 View 一定是 RecyclerView”

如果哪天 Google 换了设计或者改了方案,在 RecyclerView 外面再套一层,这个方法就会失效。

我看不下去了

来到今年下半年,由于项目的关系又用到了这里,我实在看不下去了,翻出来前面所有这些 bug,给 Google 提了一个 CL(Google 把每一个 Gerrit 上的提交称为 CL,即 Change Line):

android-review.googlesource.com/c/platform/…

我的思路也很简单,分两步:

  1. ViewPager2 在初始化的时候,维护了一个 initialize() 方法,在这个方法里去初始化了 RecyclerView,并将其 add 到了自己的 ViewGroup,因此,我们需要在这一步开始就关心一下 overScrollMode,并且透传给 RecyclerView 设下去。

  2. ViewPager2 必须重写 setOverScrollMode(int overScrollMode) 方法,这确保了开发者在手动在代码里调方法设置的时候也能生效。不要忘记 super.setOverScrollMode(overScrollMode);,这确保了你不用自己维护 android.view.View#mOverScrollMode,从而能确保 android.view.View#getOverScrollMode() 的返回值正确。

单元测试啊,单元测试

在大约半年前,我写了一篇关于单元测试的文章,向大家详细介绍了单元测试在 Google Android 项目中的重要性,如果你有兴趣,可以再次阅读:

juejin.cn/post/732339…

同样,androidx 项目也遍布着大量的单元测试。如果你也想给 androidx 做贡献,只改源码,不修改单元测试用例,Google 大概率是不会认可的。

由于是新增了对 overScrollMode 属性的支持,我不希望后续的维护者在修改的时候把这块改坏(regression),因此我必须使用单元测试来保证这块的基本正常。

在我新增的单元测试用例里,我主要保证了这两件事:

  1. 确保开发者从 xml 初始化,和从代码初始化 ViewPager2 的时候,设置的 overScrollMode 能被正确读取,且设置下去。
  2. 确保 ViewPager2 的 overScrollMode 与内部的 RecyclerView2 的 overScrollMode 保持同步,这样就能确保设置是生效的。

大块的代码就不贴了,如果大家有兴趣,可以直接这里阅读。

结果还是令人满意的

从时间线上可以看出,只要代码质量过硬,符合贡献标准,其实 Google 的 androidx 团队成员还是很乐于跟进的,我在周五下班前提交了代码,经过了一系列 review、CI,和一个愉快的周末,这笔提交已经在周二 Merge。按照以往的节奏,大概率在3个月之后的 androidx 新版本里面就可以体现。

简单总结

这个 bug 被扔进了 backlog 将近5年,现在这个坑总算被填上了,我自己很开心,开发者后面更新版本后发现这个属性能用了肯定也会很开心,可能这就是开源的乐趣所在吧。

androidx 从最开始提出到现在,其实一直都是开源,并且鼓励开发者贡献的。希望各位小伙伴平时发现问题,分析问题,解决问题之后,都可以慷慨将自己的方案提交给 Google,这除了能帮到全球数以万计的开发者之外,自己也能获得一份满满的成就感,何乐而不为呢?

作者:Mr_万能胶
链接:https://juejin.cn/post/7394463308379045951
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

### 回答1: ViewPager和ViewPager2都是Android中的视图控件,用于实现滑动切换不同页面的功能。 ViewPager是Android SDK中的一个类,它可以在同一个Activity中展示多个Fragment,通过左右滑动来切换不同的Fragment。ViewPager可以实现无限循环滑动,但是它的性能不够好,存在一些问题,比如在嵌套使用时会出现滑动冲突等。 ViewPager2ViewPager的升级版,它是在AndroidX库中的一个类,它解决了ViewPager存在的一些问题,比如滑动冲突、性能问题等。ViewPager2支持嵌套滑动,可以实现更加灵活的布局,同时还支持横向和纵向滑动。因此,ViewPager2是更加推荐使用的视图控件。 ### 回答2: ViewPager 和 ViewPager2 都是 Android 平台上的视图容器,它们都用于实现左右滑动切换多个视图的效果。不过,它们也有一些不同的特点。 ViewPager 是 Android 系统自带的视图容器,它主要用于在同一个 Activity 中切换多个 Fragment。ViewPager 会将多个 Fragment 放置在同一个视图中,通过滑动切换 Fragment 来实现左右滑动的效果。ViewPager 比较易用、稳定,使用起来也比较简单,但是在一些功能上有一定的局限性。 ViewPager2ViewPager 的升级版,它是在 AndroidX 中新增加的一个控件。相较于 ViewPager,ViewPager2 有一些更加高级和灵活的功能。首先,ViewPager2 支持 RecyclerView.Adapter,这样用户可以通过 RecyclerView.Adapter 来实现 ViewPager2 中的数据管理,这大大提高了数据操作的灵活性。其次,ViewPager2 支持垂直滑动的效果,这使得用户可以通过上下滑动切换多个视图。此外,ViewPager2支持滑块(PageTransformer)和视图预加载(OffscreenPageLimit)等高级功能,让用户可以更加方便地自定义 ViewPager2 的效果和行为。 总的来说,如果只是想要简单实现左右滑动切换多个 Fragment 的效果,可以使用 ViewPager。如果需要更加高级、灵活的功能,或者需要在 ViewPager 中嵌套 RecyclerView 或其他视图控件,则可以选择 ViewPager2。同时,最好在使用 ViewPager2 时,将所有 Fragment 替换为 RecyclerView,这样能够充分利用 ViewPager2 的强大功能。 ### 回答3: ViewPager和ViewPager2是Android平台上常用的 View容器 组件。它们最主要的作用是管理多个子view的滑动显示,类似于滑动的页面。 ViewPager从Android API Level 11就被引入,它支持从左往右滑动查看多个子视图,以轻松实现流畅的“屏幕滑动”效果,常见的使用场景包括相册、图库、图片轮播图等。在使用ViewPager时,开发者需要自己实现适配器,根据需要返回子View。且ViewPager中每个页面的宽度是相等的,无法进行自由的布局。 而ViewPager2是新增的一个组件,它是AndroidX中的一部分,于2019发布。ViewPager2相对于ViewPager的最大改进就在于支持不同宽度的页面。除了滑动方向以外,ViewPager2支持从RecyclerView中使用适配器,从而不仅仅可以使用View,还可以使用任何RecyclerView的特性和布局(如GridLayoutManager等)。另一个重要的改进是支持了多层嵌套,并且同步了更多的触摸事件,增强了原生的滑动手势支持。 总之,ViewPager2ViewPager的升级版,它具有更多灵活的布局和更好的性能。开发者可以根据自己的需求选择使用ViewPager或ViewPager2,相信在未来的Android开发中,ViewPager2会成为首选。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值