RecyclerView实现只滑不动效果

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
    }
}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容