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
);
}
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)
PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题