作者:Mr_万能胶
链接:
https://juejin.cn/post/7394463308379045951
本文由作者授权发布。
注意标题中的我,指的是本文作者Mr_万能胶,大家可以去掘金瞻仰。
这两周给 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 初始化的问题,于是跑去代码里再设置一次,会发现同样无效。
说起来也是哭笑不得,这个问题最早在社区被提出,已经是5年前了。彼时 androidx 还在 github 积极开发,有开发者发现了这个问题,先提了 issue(至今还是 Open 状态),而后过了大半年没人管,大家觉得可能这样还不够引起重视,于是有人直接提到了 Issue tacker,这个 Google 内部拿来跟踪 bug 的。
https://github.com/material-components/material-components-android/issues/459
https://issuetracker.google.com/issues?q=158234055
然后一恍就是5年,5年了,没人管。
Read the FXXKING SOURCE CODE
既然是 ViewPager 是好的,而 ViewPager2不行,我们没理由不去看一下前者的代码: 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):
https://android-review.googlesource.com/c/platform/frameworks/support/+/3184300/7/viewpager2/viewpager2/src/main/java/androidx/viewpager2/widget/ViewPager2.java- ViewPager2 在初始化的时候,维护了一个 initialize() 方法,在这个方法里去初始化了 RecyclerView,并将其 add 到了自己的 ViewGroup,因此,我们需要在这一步开始就关心一下 overScrollMode,并且透传给 RecyclerView 设下去。
ViewPager2 必须重写 setOverScrollMode(int overScrollMode) 方法,这确保了开发者在手动在代码里调方法设置的时候也能生效。不要忘记 super.setOverScrollMode(overScrollMode);,这确保了你不用自己维护 android.view.View#mOverScrollMode,从而能确保 android.view.View#getOverScrollMode() 的返回值正确。
在大约半年前,我写了一篇关于单元测试的文章,向大家详细介绍了单元测试在 Google Android 项目中的重要性,如果你有兴趣,可以再次阅读:
https://juejin.cn/post/7323399314549145600
同样,androidx 项目也遍布着大量的单元测试。如果你也想给 androidx 做贡献,只改源码,不修改单元测试用例,Google 大概率是不会认可的。由于是新增了对 overScrollMode 属性的支持,我不希望后续的维护者在修改的时候把这块改坏(regression),因此我必须使用单元测试来保证这块的基本正常。- 确保开发者从 xml 初始化,和从代码初始化 ViewPager2 的时候,设置的 overScrollMode 能被正确读取,且设置下去。
- 确保 ViewPager2 的 overScrollMode 与内部的 RecyclerView2 的 overScrollMode 保持同步,这样就能确保设置是生效的。
大块的代码就不贴了,如果大家有兴趣,可以直接这里阅读。
https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager2/viewpager2/src/androidTest/java/androidx/viewpager2/widget/OverScrollModeTest.java
从时间线上可以看出,只要代码质量过硬,符合贡献标准,其实 Google 的 androidx 团队成员还是很乐于跟进的,我在周五下班前提交了代码,经过了一系列 review、CI,和一个愉快的周末,这笔提交已经在周二 Merge。按照以往的节奏,大概率在3个月之后的 androidx 新版本里面就可以体现。
这个 bug 被扔进了 backlog 将近5年,现在这个坑总算被填上了,我自己很开心,开发者后面更新版本后发现这个属性能用了肯定也会很开心,可能这就是开源的乐趣所在吧。androidx 从最开始提出到现在,其实一直都是开源,并且鼓励开发者贡献的。希望各位小伙伴平时发现问题,分析问题,解决问题之后,都可以慷慨将自己的方案提交给 Google,这除了能帮到全球数以万计的开发者之外,自己也能获得一份满满的成就感,何乐而不为呢?
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!