RecyclerView的smooth scroller -- 诸多案例

news2024/12/28 19:12:45

作者:snwrking

最近碰到好几个使用LinearSmoothScroll(下方简称为LSS)的场景, 让我对这个类的了解更加进一步, 所以分享在这, 希望对有需要的同学有所帮助. 我个人不太喜欢太理论的东西, 所以整篇文章几乎全是我做过的案例, 也方便也有类似需求的同学对号入座地取用.

案例一: 提高smooth scroll速度

SmoothScroll(下方简称SS)是最常见的一个需求. 我们一般是使用

recyclerView.smoothScrollToPosition(p);

但要是你的RecyclerView(下方简称rv)很多内容, 这样你从第0页, SmoothScroll(SS)到第100多项, 可能会耗时很久. 所以产品可能就有了想提升一下这个SS速度的需求.

要提升速度, 就要重写 LienarSmoothScroll(LSS)的calculateSpeedPerPixel方法, 它的源码是:

protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
    return 25f / displayMetrics.densityDpi;
} 

这个函数返回的就是"经过第一个像素所要花费的时间", 那自然是这个返回值越大, 说明同样滑过100个像素, 所花费的时间就更多. 所以这个方法的返回值越大, 那说明SS的速度越慢

那要提升速度就容易了, 就是让这个返回值变小:

    // 取代了原来的 rv.smoothScrollToPosition(pos)
    private fun RecyclerView.fastSmoothScrollTo(pos: Int) {
        val scroller = object : LinearSmoothScroller(this.context) {
            //经过每个pixel的时间越长(即本函数返回的float), 表示速度就越慢
            override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
                // 默认是 return 25f / displayMetrics.densityDpi;
                return 6f / displayMetrics.densityDpi //这里值更小了, 所以速度更快了
            } //这里改25f为6f, 那速度就相当快了. 滑动距离小于两屏的, 几乎瞬时就到了
        }
        scroller.targetPosition = pos
        this.layoutManager?.startSmoothScroll(scroller)
    }

引申

要是对LSS有些了解的同学, 就会发现LSS还有两个函数, 也跟滑动时间, 也可以说滑动速度相关啦, 这两个方法是:

override fun calculateTimeForScrolling(dx: Int): Int 

override fun calculateTimeForDeceleration(dx: Int): Int 

这个具体的分别, 要到下面的案例中才会讲到. 现在就掰开来细讲, 就会太枯燥, 要结合下面案例来讲才能理解. 总之, 结论就是, 重写这两个函数并不能帮我们调整SS的速度

案例二: 将SS的总时间调整为一致

上面的案例一提升了SS的速度, 但有一个问题, 那就是从第0项滑到第5项时, 几乎没有SS的效果, 相当于直接到位, 类似于 scrollTo(position)的效果.

这时产品想要从第0项滑到第5项, 与从第0项滑到第25项的时间, 要基本相同, 这样免得后者要滑好久, 或者前者几乎没怎么滑就到了(少了动画的顺畅感).

作用肯定是行的, 但我个人觉得稍有点麻烦, 所以我想到了另一个折中方案, 也就是看当前item与target position差多少, 然后根据这个差值来做速度的调整

  • 若target position离当前项很远, 那速度就快一些
  • 若是二者离得较近, 那速度就慢一点

这样不就变相地达到了需求嘛.

所以我的折中方案是:

    private fun RecyclerView.smoothScrollEquallyTo(pos: Int) {
        val scroller = object : LinearSmoothScroller(context) {
            override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
                // 源码是: return 25f / displayMetrics.densityDpi;
                val layoutMgr = this.layoutManager
                if(layoutMgr !is LinearLayoutManager) return super.calculateSpeedPerPixel(displayMetrics)
                val first = layoutMgr.findFirstVisibleItemPosition()
                val diff = abs(pos - first) //来看这个远不远
                val speed = 25f / diff * 5 //diff越大, 那25f/diff就越小, 那速度就越快.  (25f/diff就太快了, 根本没有SS效果, 所以再 * 5)
                val ret = speed / displayMetrics.densityDpi
                return ret
            }
        }

        scroller.targetPosition = pos
        layoutManager?.startSmoothScroll(scroller)
    }

我实测了一下, 发现确实比较合适. 但考虑到每个RV的item可能不一样, 有些item很短, 有些item很长, 所以这个速度不能订死了. 于是我把这个速度参数(speedFactor)给提取出来, 这样不同的rv可以指定不同的速度参数.

    // speedFactor越大, 那速度越慢
    private fun RecyclerView.smoothScrollEquallyTo(pos: Int, speedFactor: Int = 5) {
        val scroller = object : LinearSmoothScroller(context) {
            override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
                // 源码是: return 25f / displayMetrics.densityDpi;
                val layoutMgr = this.layoutManager
                if(layoutMgr !is LinearLayoutManager) return super.calculateSpeedPerPixel(displayMetrics)
                val first = layoutMgr.findFirstVisibleItemPosition()
                val diff = abs(pos - first) //来看这个远不远
                val speed = 25f / diff * speedFactor //diff越大, 那25f/diff就越小, 那速度就越快. 
                val ret = speed / displayMetrics.densityDpi
                return ret
            }
        }

        scroller.targetPosition = pos
        layoutManager?.startSmoothScroll(scroller)
    }

案例三: 想让SS到的targetPosition最终是居于顶端

这个需求也常见吧, 让用户想要跳到的那项被滚动到顶端. 但是rv.smoothScrollToPosition(pos)却不能完全做到这一点. 原因是这个方法在以下三种情况下, 具体的SS行为是不一样的:

1). 当前页面展示0-3项; 这时SS到5项, 那就是滑动到第5项刚好完可见
(这时自然5项是在最底下).
–> PO一般都是想说要滑到最开头

2). 当前页面展示的是7-10项, 这时SS到5, 结果也是滑动到第5项刚好可见
这时项5自然在最开头
–> 综合1)与2), 发现就是表相是滑动方向不同
(一个往下滑到第5项, 一个往上滑到第5项)导致了这个问题.
里子却是一一个"最少滑动量, 导致target项完全显示就停了, 不再SS了"的原则

3). 当前页面展示3-7项时, 这时SS到5, 结果是: 什么都没发生!
是的, 若已完全可见, 那调用smoothScroll都不会有任何事发现.

–> PO一般是想把项5放到最开头去`

总结下, 那就是rv.smoothScrollToPosition(pos)遵循的是滑动最少的原则, 按不同的滑动方向, 只要target position那项item已经完全可见了, 就马上停止滑动; 要是target position已经可见了, 那根本不滑动.

他们的方案都是针对上面的三种做法, 分别调用smoothScroll或是scrollBy来强制变更SS行为, 甚至还要添加OnScrollListener来保证滑动的持续性. (代码可见上面链接; 这里的贴图只是给出个大概的意思)

这样肯定能成功, 但我仍是在想有没有更简单的方案.

经过我的查找, 果然找到了更好的方案, 而且代码只有简单的方案. 其实就是要指定LSS中的snap mode, 我们只要指定其为snap_to_start, 那SS就会自动强制地将target position那项item给放顶端.

具体代码如下:

    private fun RecyclerView.smoothScrollAndSnapStartTo(pos: Int) {
        val scroller = object : LinearSmoothScroller(context) {
            // 若不指定这个SNAP_TO_START, 那默认就是 rv.smoothScrollTo(position)的效果, 即仍有RvScrollToPosition_Issue_Page.kt中所说的几个问题
            override fun getVerticalSnapPreference() = LinearSmoothScroller.SNAP_TO_START
        }
        scroller.targetPosition = pos
        layoutManager?.startSmoothScroll(scroller)
    }

备注: 要是你的rv是一个水平rv, 那就请重写getHorizontalSnapPreference() = SNAP_TO_START

案例四: snap_to_start还有一点点偏移量

好吧, 现在我们的UX给的设计稿是, RV上方(Z轴上)还有一层导航条. 这个导航条占住了rv的top一些部分

<FrameLayout>
     <RecyclerView top = 0/>
     <Buttons top = 0/>

类似这样的效果(即4个button代表的导航条, 位于rv之上):

那这时的SS, 如跳到第5项就不太如意, 因为被遮住了一部分. UX想让target position项能完全展示出来:

SS的原理

这时其实就是要看RV是如何SS的了.

  • 当我们指定要SS到target position去, 那RV就会一直滑动
  • 直到当target position出现了, 上述的普通滑动就完结了, 这时就开始进入了减速期
  • 在减速期里, 滑动(速度与时间)跟刚刚的滑动不一样, 而且要滑动多少能让target position的item正好completely visible, 这些都在LSS的onTargetFound方法的源码里:
// LSS的源码
@override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
    final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
    final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
    final int distance = (int) Math.sqrt(dx * dx + dy * dy);
    final int time = calculateTimeForDeceleration(distance);
    if (time > 0) {
        action.update(-dx, -dy, time, mDecelerateInterpolator);
    }
}

也就是说, 在上面第二步中, target item出现了时, 这时就马上计算距离最终还要滑动多少, 以及要减速所花的时间. 而这个"要滑动多少", 则在于calculateDxToMakeVisible, calculateDyToMakeVisible两个方法里

计算减速期的滑动距离

calculateDxToMakeVisible, calculateDyToMakeVisible两个方法里其实都是在调用calculateDtToFit方法, 只不过参数不同而已.

其实就可以理解calculateDtToFit方法就是一个计算在SS减速期, 到底还要滑动多少距离的函数.

下面来看LSS的源码, 就能理解calculateDtToFit的使用场景了:

// LSS源码

// 下面的view参数, 就是指rv中某一item
    public int calculateDxToMakeVisible(View view, int snapPreference) {
        ...
        return calculateDtToFit(viewLeft, viewRight, rvLeft, rvRight, snapPreference);
    }
    
    public int calculateDyToMakeVisible(View view, int snapPreference) {
        ...
        return calculateDtToFit(viewTop, viewBottom, rvTop, rvBottom, snapPreference);
    }    

解决方案

现在看完这些源码就知道了, 我们只要在snap_to_start的基础上, 再在calculateDtToFit中提供一定的offset偏移量就行了.

   private fun RecyclerView.smoothScrollWithOffsetTo(pos: Int) {
        val topButtonsHeight = topButtonsLayout.height //=> 126

        val scroller = object : LinearSmoothScroller(context) {

            override fun getVerticalSnapPreference() = LinearSmoothScroller.SNAP_TO_START
            
            override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int {
                val computed = super.calculateDtToFit(viewStart, viewEnd, boxStart, boxEnd, snapPreference)
                if (snapPreference == SNAP_TO_START) {
                    return computed + topButtonsHeight
                }
                return computed
            }
        }
        scroller.targetPosition = pos
        layoutManager?.startSmoothScroll(scroller)
    }

这样当调用rv.smoothScrollWithOffsetTo(7)时, 结果就是:

案例五: 始终让SS的target item出现在页面中间

有时我们确实有这种需求, 比如说你用RV做一个WheelSelection时, 需求就是让selected项居于中间:

可惜的是LSS并没有snap_to_center的preference. 不过也不麻烦, 同样地我们只要重写calculateDtToFit即可:

    private fun RecyclerView.smoothScrollInCenterTo(pos: Int) {
        val scroller = object : LinearSmoothScroller(context) {
            override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int {
                return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
            }
        }
        scroller.targetPosition = pos
        layoutManager?.startSmoothScroll(scroller)
    }

这里的参数就是找到中间点的位置, 也就是rv与item的位置一比较, 即得出了中间点.

总结

通过上面四个案例, 我们了解了如何调整Smooth Scroll的速度, 最终位置等. 主要就是重写LinearSmoothScroll的这几个方法:

  • calculateSpeedPerPixel()
  • getVerticalSnapPreference()
  • getHorizontalSnapPreference()
  • calculateDtToFit()

以及了解了LinearSmoothScroll到底是如何进行smooth scroll的, 即 “普通smooth scroll + 当发现了target时就开始减速smooth”. 这个能帮助我们理解何时要重写哪些方法.

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/972523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【用unity实现100个游戏之8】用Unity制作一个炸弹人游戏

文章目录 前言素材开始一、绘制地图二、玩家设置三、玩家移动四、玩家四方向动画运动切换 五、放置炸弹六、生成爆炸效果七、墙壁和可破坏障碍物的判断八、道具生成和效果九、玩家死亡十、简单的敌人AI十一、虚拟摇杆 待续源码完结 前言 我们将在这个视频中&#xff0c;学习如…

Oracle 遍历变量游标

背景 由于我们的数据库系统中的游标特别多&#xff0c;DBA让我们优化&#xff0c;减少游标的使用。 电脑系统&#xff1a;windows数据库&#xff1a;Oracle数据库图形化界面工具&#xff1a;Toad&#xff0c;DBeaver(我測試的時候用的)记录日期&#xff1a;2023-09-04 具体实…

macbookpro怎么删除软件没有鼠标

macbookpro怎么删除软件没有鼠标,macbookpro触摸板可以替代鼠标进行操作。左右键功能与鼠标相同&#xff0c;可用于执行删除操作。此外&#xff0c;还可以利用键盘上的Delete键来删除选中的文件。 删除软件方法 方法1、打开应用程序&#xff0c;键盘按住control&#xff0c;加点…

解决微信小程序recycle-view使用百分比单位控制宽高时出现的内容溢出问题

recycle-view是微信小程序官方推出的一个经过优化的长列表组件&#xff0c;但是在使用百分比单位控制高宽时有个内容溢出问题&#xff0c;虽然它提供了height和width的参数可以设置宽高&#xff0c;但每次写列表都需要去js里获取宽高并设置是较为麻烦的&#xff0c;所以现在来着…

Vue 3 基础(二)基础 1

API 参考 1、创建一个 Vue 应用 1.1 应用实例 每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例&#xff1a; import { createApp } from vueconst app createApp({/* 根组件选项 */ })1.2 根组件 我们传入 createApp 的对象实际上是一个组件&#xff0c;每个…

uni-app 之 图片

uni-app 之 图片 获取图片 v-bind 动态绑定 image.png <template><view><view>--- 获取图片1 ---<image src"../../static/img/tabbar_home1.png"></image></view><view>--- 获取图片2 v-bind 动态绑定---<image v-bi…

SolVES4.1学习1——安装与使用教程

1、下载并安装 SolVES 4版本是QGIS插件&#xff0c;但实际使用过程中发现在最新版的QGIS安装该插件过程中&#xff0c;会报错或异常。因此需安装特定版本的软件。共需安装如下图软件及Java环境等。 根据官方文档安装好后&#xff0c;可以进行相关操作。 2、设置QGIS环境 QG…

AutoSAR配置与实践(基础篇)3.7 BSW的WatchDog功能(下)

AutoSAR配置与实践(基础篇)3.7 BSW的WatchDog功能(下) BSW的WatchDog功能(下)一、WDG和其他模块交互BSW的WatchDog功能(下) ->返回总目录<- 一、WDG和其他模块交互 模块交互 看门狗模块由WdgM统一管理,这里围绕WdgM模块分析与其他模块交互。通过交互的说明,可以…

css画箭头图标放标题前面,旋转,border的单个边框设置

CSS边框属性_css border dotted_小张biubiu的博客-CSDN博客 你还不知道css的旋转效果怎么实现&#xff1f;来这里看看吧_css旋转效果_我糖呢的博客-CSDN博客 .sub-title{position: relative;margin-left: 59px;& span{color: #1CDBFE;};& span::before{content: "…

MyBatis-Plus深入 —— 条件构造器与插件管理

前言 在前面的文章中&#xff0c;荔枝梳理了一个MyBatis-Plus的基本使用、配置和通用Service接口&#xff0c;我们发现在MyBatis-Plus的辅助增强下我们不再需要通过配置xml文件中的sql语句来实现基本的sql操作了&#xff0c;不愧是最佳搭档&#xff01;在这篇文章中&#xff0c…

ping: www.baidu.com: Name or service not known 写了DNS还是不行

环境描述&#xff1a;ESXI平台上&#xff0c;一台Centos7虚拟主机。 问题描述&#xff1a;平台上的其他的虚拟机可以正常ping通&#xff0c;就这台ping IP地址可以通&#xff0c;ping域名解析失败。 排查过程&#xff1a; 1、检查网卡配置文件和/etc/resolv.conf配置文件是否…

【综述+3D】基于NeRF的三维视觉2023年度进展报告(截止2023.06.10)

论文&#xff1a;2003.Representing Scenes as Neural Radiance Fields for View Synthesis 官方网站&#xff1a;https://www.matthewtancik.com/nerf 突破性后续改进&#xff1a; Instant Neural Graphics Primitives with a Multiresolution Hash Encoding | 展示官网&#…

K8S的介绍和架构

仅供入门 K8S的介绍和架构 一. 什么是kubernetes二、Kubernetes架构和组件 2.1 核心组件 2.1.1 Kubernetes Master控制组件&#xff0c;调度管理整个系统&#xff08;集群&#xff09;&#xff0c;包含如下组件: a、Kubernetes API Serverb、Kubernetes Schedulerc、Kubernet…

【前端】CSS-Grid网格布局

目录 一、grid布局是什么二、grid布局的属性三、容器属性1、display①、语句②、属性值 2、grid-template-columns属性、grid-template-rows属性①、定义②、属性值1&#xff09;、固定的列宽和行高2&#xff09;、repeat()函数3&#xff09;、auto-fill关键字4&#xff09;、f…

Vue指令之战:v-if vs. v-show -你应该使用哪一个?

在Vue.js中&#xff0c;条件渲染是一项常见任务&#xff0c;而v-if和v-show是两个最常用的指令。这两个指令在实现方式上有所不同&#xff0c;对于开发者来说选择正确的指令可能具有挑战性。本文将深入探讨Vue 2和Vue 3中的v-if和v-show指令的区别&#xff0c;并结合实际应用场…

Linux的命令

Linux的命令分为四个类型&#xff1a;文件操作命令、系统操作命令、文本处理命令和网络操作命令。下面简单介绍一下常用的Linux命令&#xff1a; 文件操作命令 ls&#xff1a;列出目录下的所有文件和目录。 cd&#xff1a;切换当前目录。 mkdir&#xff1a;创建一个新目录。…

<图像处理> 可分离滤波器核

可分离滤波器核 空间滤波器核是一个二维矩阵&#xff0c;若它能够表示为两个一维矩阵的乘积时&#xff0c;则表示该滤波器核是可分离的。 例如&#xff0c;一个3x3的核&#xff0c; w [ 1 1 1 1 1 1 1 1 1 ] w\begin{bmatrix} 1 & 1 & 1\\ 1 & 1& 1\\ 1 &am…

操作系统(OS)与系统进程

操作系统&#xff08;OS&#xff09;与系统进程 冯诺依曼体系结构操作系统(Operator System)进程基本概念进程的描述&#xff08;PCB&#xff09;查看进程通过系统调用获取进程标示符&#xff08;PID&#xff09;通过系统调用创建进程&#xff08;fork&#xff09;进程状态&…

安防监控/视频汇聚/云存储/AI智能视频融合平台页面新增地图展示功能

AI智能分析网关包含有20多种算法&#xff0c;包括人脸、人体、车辆、车牌、行为分析、烟火、入侵、聚集、安全帽、反光衣等等&#xff0c;可应用在安全生产、通用园区、智慧食安、智慧城管、智慧煤矿等场景中。将网关硬件结合我们的视频汇聚/安防监控/视频融合平台EasyCVR一起使…