1.背景
墨水屏上RecyclerView滚动效果体验差,因此想出当手指滑动时 RecyclerView 页面不动,抬起后直接跳转到目标位置(无动画)的方案,可以很好的解决该问题。
2.方案简介
核心思路:拦截触摸滑动事件,记录滑动偏移 / 目标位置,手指抬起时通过scrollBy()直接定位。
3.方案拆解
实现逻辑大致可分为以下四步:
拦截触摸事件:手指拖动(ACTION_MOVE)时不触发滚动,仅记录滑动方向 / 距离,计算目标位置;
阻断默认滚动:禁用 RecyclerView 原生的触摸滚动逻辑,避免拖动时页面移动;
抬起时精准定位:手指抬起(ACTION_UP)时,根据记录的滑动信息计算目标位置,通过scrollBy()直接跳转(无动画);
禁用平滑滚动:确保跳转过程无动画,仅瞬间定位。
4.具体实现
分析以上方案,很容易想到自定义一个RecyclerView子类并实现以上四步,就可以实现想要的只滑不动的效果。那么话不多说,直接开整...
4.1.拦截触摸事件
监听手指滑动事件(ACTION_MOVE )不触发滚动,仅记录滑动的方向和距离
override fun onTouchEvent(e: MotionEvent): Boolean {
// 初始化速度追踪器
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
velocityTracker?.addMovement(e)
val layoutManager = layoutManager as? LinearLayoutManager ?: return super.onTouchEvent(e)
val orientation = layoutManager.orientation
MotionEvent.ACTION_MOVE -> {
velocityTracker?.addMovement(e)
val currentY = e.y
val deltaY = lastTouchY - currentY // 滑动偏移量(向上滑为正)
lastTouchY = currentY
if (!isDragging && abs(deltaY) > 10) {
setIsDragging(true, "2")
}
// 仅记录滑动轨迹,不触发RecyclerView滚动
// 此处可添加“视觉反馈”(如item轻微偏移),保持触摸手感
}
4.2.阻断默认滚动
fling()返回值设置为false禁用默认的fling
// 禁用默认的fling(快速滑动),避免冲突
override fun fling(velocityX: Int, velocityY: Int): Boolean {
return false
}
4.3.抬起时精准定位
手指抬起时计算滚动距离,通过VelocityTracker工具可以支持计算惯性滚动距离
override fun onTouchEvent(e: MotionEvent?): Boolean {
e ?: return false
// 绑定速度追踪器
velocityTracker?.addMovement(e)
when (e.action) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!isDragging) return super.onTouchEvent(e)
isDragging = false
velocityTracker?.addMovement(e)
// 1. 计算滑动速度(仅惯性滚动需要)
velocityTracker?.computeCurrentVelocity(1000, maxVelocity.toFloat())
val velocityY = velocityTracker?.yVelocity?.toInt() ?: 0 // 垂直速度(像素/秒)
// 2. 计算目标滚动距离
scrollDistance =
calculateInertialDistance(velocityY) + ((startTouchY - lastTouchY) * 0.8).toInt()
// 3. 触发滚动(延迟100ms启动,模拟“抬起后延迟滚动”)
/*postDelayed({
startScrollToTarget(scrollDistance)
}, 100)*/
startScrollToTarget(scrollDistance)
// 重置速度追踪器
cleanupVelocityTracker()
}
}
return isDragging
}
4.4.快速滚动到对应位置
使用scrollBy()实现快速滚动到对应的位置
scrollBy(0, distance)
5.注意事项
5.1.资源释放
释放资源,避免内存泄漏
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scroller.forceFinished(true)
}
6.效果展示

Screen_recording_20260105_163433-ezgif.com-video-to-gif-converter.gif
7.总结
该方案兼顾了滑动时不动,抬起后精准定位,保留交互三大核心需求,且可根据业务灵活调整目标位置的计算逻辑。另外采用自定义RecyclerView方式,使用其他非常方便。
附件
完整源码
import android.content.Context
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
import android.widget.Scroller
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
open class DelayScrollRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "DelayScrollRecyclerView"
}
// 滑动相关配置
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop // 最小滑动判定距离
private val maxVelocity: Int =
ViewConfiguration.get(context).scaledMaximumFlingVelocity // 最大滑动速度
private val minVelocity: Int =
ViewConfiguration.get(context).scaledMinimumFlingVelocity // 最小惯性速度
private val scroller = Scroller(context) // 计算惯性滚动轨迹
private var velocityTracker: VelocityTracker? = null // 速度追踪
// 触摸状态记录
private var isDragging = false // 是否正在拖拽
private var lastTouchY = 0f // 上一次触摸Y坐标
private var startTouchY = 0f // 触摸起始Y坐标
private var lastTouchTime = 0L // 上一次触摸时间(ms)
private var scrollDistance = 0 // 最终滚动距离(px)
// 滚动模式:true=惯性滚动,false=非惯性滚动
var isInertialScroll = true
set(value) {
field = value
// 重置Scroller参数(惯性/非惯性阻尼不同)
scroller.forceFinished(true)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
ev ?: return false
val action = ev.action
when (action) {
MotionEvent.ACTION_DOWN -> {
startTouchY = ev.y
initVelocityTracker(ev)
setIsDragging(false, "1")
}
MotionEvent.ACTION_MOVE -> {
velocityTracker?.addMovement(ev)
if (!isDragging) {
val deltaY = ev.y - startTouchY
if (abs(deltaY) > 10) {
// 当垂直滑动距离超过阈值时,拦截事件
setIsDragging(true, "2")
Log.i(TAG, "开始拦截事件")
}
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
setIsDragging(false, "3")
cleanupVelocityTracker()
}
}
Log.i(
TAG,
"onInterceptTouchEvent: action=${ev.action} onInterceptTouchEvent=$isDragging"
)
return isDragging
}
private fun setIsDragging(isDragging: Boolean, from: String = "") {
this.isDragging = isDragging
Log.i(TAG, "setIsDragging: isDragging=$isDragging from=$from")
}
override fun onTouchEvent(e: MotionEvent?): Boolean {
e ?: return false
// 绑定速度追踪器
velocityTracker?.addMovement(e)
when (e.action) {
MotionEvent.ACTION_DOWN -> {
// 初始化触摸状态
startTouchY = e.y
lastTouchY = e.y
lastTouchTime = System.currentTimeMillis()
initVelocityTracker(e)
// 停止正在进行的滚动
scroller.forceFinished(true)
stopScroll()
return true // 消费DOWN事件,确保后续事件能接收
}
MotionEvent.ACTION_MOVE -> {
velocityTracker?.addMovement(e)
val currentY = e.y
val deltaY = lastTouchY - currentY // 滑动偏移量(向上滑为正)
lastTouchY = currentY
if (!isDragging && abs(deltaY) > 10) {
setIsDragging(true, "2")
}
// 仅记录滑动轨迹,不触发RecyclerView滚动
// 此处可添加“视觉反馈”(如item轻微偏移),保持触摸手感
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!isDragging) return super.onTouchEvent(e)
isDragging = false
velocityTracker?.addMovement(e)
// 1. 计算滑动速度(仅惯性滚动需要)
velocityTracker?.computeCurrentVelocity(1000, maxVelocity.toFloat())
val velocityY = velocityTracker?.yVelocity?.toInt() ?: 0 // 垂直速度(像素/秒)
// 2. 计算目标滚动距离
scrollDistance =
calculateInertialDistance(velocityY) + ((startTouchY - lastTouchY) * 0.8).toInt()
// 3. 触发滚动(延迟100ms启动,模拟“抬起后延迟滚动”)
/*postDelayed({
startScrollToTarget(scrollDistance)
}, 100)*/
startScrollToTarget(scrollDistance)
// 重置速度追踪器
cleanupVelocityTracker()
}
}
return isDragging
}
/**
* 计算惯性滚动距离(根据滑动速度 + Scroller 阻尼)
*/
private fun calculateInertialDistance(velocityY: Int): Int {
// 仅当速度≥最小惯性速度时,计算惯性
if (Math.abs(velocityY) < minVelocity) return 0
// 通过Scroller计算惯性滚动的总距离
scroller.fling(
0, 0, // 初始X/Y坐标
0, -velocityY, // 初始X/Y速度(Y轴反向,因为Scroller的Y正方向与触摸相反)
0, 0, // X轴滚动范围(无水平滚动)
-Int.MAX_VALUE, Int.MAX_VALUE // Y轴滚动范围
)
// 获取惯性滚动的总偏移量
return scroller.finalY
}
private fun initVelocityTracker(event: MotionEvent) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
velocityTracker?.addMovement(event)
}
private fun cleanupVelocityTracker() {
velocityTracker?.recycle()
velocityTracker = null
}
/**
* 启动滚动到目标位置(映射到RecyclerView的item位置)
*/
private fun startScrollToTarget(distance: Int) {
val layoutManager = layoutManager as? LinearLayoutManager ?: return
val adapter = adapter ?: return
// 1. 计算当前可见的第一个item位置
val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
if (firstVisiblePos == -1) return
// 2. 计算第一个可见item的顶部偏移量
val firstVisibleView = layoutManager.findViewByPosition(firstVisiblePos)
val topOffset = firstVisibleView?.top ?: 0
// 3. 计算目标滚动的item位置(偏移量→item索引)
val targetScrollY = scrollY + distance
val targetPos =
layoutManager.findFirstVisibleItemPosition() + (targetScrollY / (firstVisibleView?.height
?: 100)) // 按item高度换算
// 4. 校准目标位置(避免越界)
val finalTargetPos = targetPos.coerceIn(0, adapter.itemCount - 1)
// 5. 自定义平滑滚动器,控制滚动速度(模拟惯性/非惯性手感)
val smoothScroller = object : LinearSmoothScroller(context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
// 惯性滚动:速度快→滚动快;非惯性滚动:匀速
return if (isInertialScroll) {
0.3f // 每像素耗时0.3ms(更快)
} else {
0.8f // 每像素耗时0.8ms(更慢)
}
}
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START // 目标item对齐列表顶部
}
}
smoothScroller.targetPosition = finalTargetPos
scrollBy(0, distance)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// 释放资源,避免内存泄漏
/*try {
velocityTracker.recycle()
} catch (e: IllegalStateException) {
// 忽略"Already in the pool"异常
if (e.message?.contains("Already in the pool") != true) {
throw e
}
// 记录日志但不崩溃
Log.w("DelayScrollRecyclerView", "VelocityTracker already recycled")
} catch (e: Exception) {
// 其他异常正常抛出
throw e
}*/
scroller.forceFinished(true)
}
// 禁用默认的fling(快速滑动),避免冲突
override fun fling(velocityX: Int, velocityY: Int): Boolean {
return false
}
}