在掘金,CSDN和StackOverFlow中没有看到一篇好的、全面的LinearSmoothScroller使用教程,兹写下此文。
I. 滚动的RecyclerView
要让RecyclerView开始滚动,我们有以下几种方法(不全):
RecyclerView / LayoutManager类 中的scrollTo方法 (滚动至任意位置)
RecyclerView / LayoutManager类 中的scrollToPosition方法 (滚动至特定Item位置)
RecyclerView / LayoutManager类 中的smoothScrollBy方法 (平滑滚动至任意位置)
RecyclerView / LayoutManager类 中的smoothScrollTo方法 (平滑滚动至特定Item位置)
LayoutManager类 中的startSmoothScroll方法 (使用外部创建的RecyclerView.SmoothScroller,平滑滚动至特定Item位置)
现在我们可以来分析一下现有的实现滚动的方法了 ——
1, 2 都是非平滑滚动,也就是瞬间滚动至指定位置。3, 4 都是平滑滚动,且 4 可以特定滚动插值器。5 调用了外部的 SmoothScroller 实现平滑滚动。
与 1, 3 相比,2, 4, 5 的滚动至Item位置更加省事,但是可能就有朋友发现了,这几种方法都不能自定义插值器以及滚动时长,而唯一能指定插值器的 3 却无法帮助我们方便的滚动到特定Item位置、自定义时间,甚至如果滚动路程过长,3 的动画就会显得又臭又长。
那么,难道我们就没法方便的滚动到特定的Item位置吗?难道我们非要绞尽脑汁自己写出定位位置,自己实现动画过度,写出一个可以用的滚动实现么?不————
诸君,且留步,听我娓娓道来——
II. LinearSmoothScroller,堂堂登场!
LinearSmoothScroller,继承 RecyclerView.SmoothScroller,是一个现成的滚动工具类。 它已经帮我们写好了位置定位,动画过渡,那么我们来看看怎么使用它吧。
构建 LinearSmoothScroller
val mLinearSmoothScroller = object : LinearSmoothScroller(context) {}
mLinearSmoothScroller.targetPosition = seekPosition // 滚动目标在 adapter 中的位置
mLinearLayoutManager.startSmoothScroll(mLinearSmoothScroller)
调教 LinearSmoothScroller
好,那么我们先写一个实例应用试试吧!
5个 ViewHolder,15 个 ViewHolder,滚动顺利...... 不对!出问题了!在 ViewHolder 过长的时候, smoothScroller 的动画会显得拖沓!
没有关系,让我们来复写滚动时间吧,让动画看起来更顺畅一点。
private fun createSmoothScroller(): RecyclerView.SmoothScroller {
return object : CustomSmoothScroller(context) {
override fun calculateTimeForDeceleration(dx: Int): Int {
return 500
}
}
}
再次实验,是不是顺畅多了?
这个时候,有的朋友又有新问题了:我如何让指定的 ViewHolder 出现在屏幕顶端?底端?或者是我想要的任意位置?
别着急,这个也简单,让我们再来复写几个 method 就是了:
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
返回值可以有这几种:
public static final int SNAP_TO_START = -1;
public static final int SNAP_TO_END = 1;
public static final int SNAP_TO_ANY = 0;
第一种返回值 SNAP_TO_START,表示一直滚动,直到目标在屏幕顶端为止。 第二种返回值 SNAP_TO_END,表示一直滚动,直到目标在屏幕底端为止。 第三种返回值 SNAP_TO_ANY,表示一直滚动,直到目标出现在屏幕里为止。
如果我们想要滚动直到目标出现在距屏幕顶端 72px 的地方,怎么处理?
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
return super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference) + 72
}
好,近乎完美,现在你已经学会了如何使用基础的 LinearSmoothScroller,而网上的教程大多到此为止——下面只剩下我夹带的干货了。
III. LinearSmoothScroller 到底是如何工作的?
通过分析源码,我们可以得知,LinearSmoothScroller 会先计算目标 ViewHolder 距离目的地的位置,然后再做出情况判断:
- 第一种情况:如果目标在屏幕上可视,调用
onTargetFound,利用mDecelerateInterpolator这个插值器来进行平滑的减速。 - 第二种情况:如果目标在屏幕上不可视,调用
updateActionForInterimTarget,利用mLinearInterpolator线性滚动直到目标在屏幕中可见,再利用onTargetFound以及mDecelerateInterpolator来平滑减速到目的地。
在上一段里,我们提到了 在 ViewHolder 过长的时候, smoothScroller 的动画会显得拖沓 的问题,那么我们再次深入源码来分析看看:
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * getSpeedPerPixel());
}
private float getSpeedPerPixel() {
if (!mHasCalculatedMillisPerPixel) {
mMillisPerPixel = calculateSpeedPerPixel(mDisplayMetrics);
mHasCalculatedMillisPerPixel = true;
}
return mMillisPerPixel;
}
/**
* Calculates the scroll speed.
*
* <p>By default, LinearSmoothScroller assumes this method always returns the same value and
* caches the result of calling it.
*
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
好了,那么现在我们知道为何会出问题了:MILLISECONDS_PER_INCH 永远是一个定值,calculateSpeedPerPixel 虽然是按照 DPI 来计算滚动时间了,但并没有将滚动长度考虑在内,因为 LinearSmoothScroller 会假设 calculateSpeedPerPixel 的返回值永远不会变,缓存这个值。
网上很多教程都会叫你去复写 calculateSpeedPerPixel ,来将速度提高,实际上我测试的效果也不理想,在 RecyclerView 项目过多的时候还是会出现卡顿的现象,而最理想,最简单的 Workaround 便是直接将 calculateTimeForScrolling 设置为一个定值,这样滚动的速度会按照滚动时间来动态计算。
IV. 美化 LinearSmoothScroller 的滚动过程
什么?你说 LinearSmoothScroller 的滚动过程不动感,不美观?你在做 LyricView,想要让滚动过程和你的 View 动画过程变得统一?
小菜一碟~
前面我们说了,在进行滚动的时候 LinearSmoothScroller 会调用两个插值器,那么修改这两个插值器就可以了。mLinearInterpolator 其实如果没有特别的需求不用修改,因为在长时间线性滚动的时候用插值器和不用插值器其实区别不大。我们只需要修改 mDecelerateInterpolator 便能达到很好的效果。
当然,LinearSmoothScroller 不允许你直接复写这两个插值器。幸运的是 LinearSmoothScroller 不会调用什么奇怪的内部组件和受限的 API,我们只需复制粘贴 LinearSmoothScroller 的内容,改个名字就可以了。
在这里推荐使用 material-components 的 motionEasingStandardInterpolator。
protected TimeInterpolator mDecelerateInterpolator;
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public CustomSmoothScroller(Context context) {
mDisplayMetrics = context.getResources().getDisplayMetrics();
mDecelerateInterpolator = MotionUtils.resolveThemeInterpolator(
context,
R.attr.motionEasingStandardInterpolator, // interpolator theme attribute
FastOutSlowInInterpolator() // default fallback interpolator
);
}
Voila!
V.结尾
本篇教程就到此结束了,如果有帮助,不妨给我点个赞,也可以给我的 Github 点个关注,谢谢你。
如果你想要一个现有的示例程序,欢迎到这里看看
下一次我们将讨论如何在 RecyclerView 中使用纯数学方法(三次贝塞尔曲线拟合)来实现 Apple Music 中的延迟歌词效果,如果感兴趣请持续关注。