安卓实现View的惯性滚动效果(Fling)

  • Post author:
  • Post category:其他


实现惯性滚动核心步骤就三步:

1.算出手指抬起时页面滚动的速度

2.根据这个速度算接下来每个时间段内应该滚动的距离

3.滚动这个距离

首先我们来算速度,速度怎么算?请回忆以前学物理的时候的打点计时器。我们需要借助一个工具类VelocityTracker,这个工具就很像打点计时器。我们在每次出发触摸事件的时候,调用这个类的addMovement(event)方法,打个点,当我们想要计算速度时,调用获取速度的方法,它能根据这些打点帮我们算出我们想要的速度

然后我们来算手指抬起后每个时间段滚动的距离,这个距离同样不需要我们自己手动算,我们借助工具Scroller类,将算得的速度传给它,然后在手指抬起时调用Scroller类的fling()方法,通知它开始计算距离的工作,然后在我们想要获取滚动距离时,调用一下它的getCurrX()方法或者getCurrY()方法,获取到的值就是我们需要的值

第三步,滚动到这个距离,直接scrollTo()到那个位置上即可

完整代码

import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
import android.widget.LinearLayout
import android.widget.Scroller

class HpScrollAndFlingView(context: Context, attrs: AttributeSet): LinearLayout(context, attrs) {

    // 上一次触摸事件的y坐标
    private var lastY = 0f
    // 用来算速度的工具(通过在每次的触摸事件中打点)
    private lateinit var mVelocityTracker: VelocityTracker
    // 用来根据传入的速度算当前应该滚动到的位置的工具
    private val scroller by lazy { Scroller(context) }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (!super.onTouchEvent(event)) {
            if (!this::mVelocityTracker.isInitialized) mVelocityTracker = VelocityTracker.obtain()
            var isActionUp = false
            // 拷贝一份触摸事件,我猜是为了防止污染原事件
            val eventCopy = MotionEvent.obtain(event)

            val eventY = event.y

            when(event.actionMasked) {
                MotionEvent.ACTION_DOWN -> lastY = eventY
                MotionEvent.ACTION_MOVE -> {
                    val dy = lastY - eventY
                    scrollTo(0, (scrollY + dy).toInt())
                    lastY = eventY
                }
                MotionEvent.ACTION_UP -> {
                    // 最后一次打点
                    mVelocityTracker.addMovement(eventCopy)
                    isActionUp = true
                    // 设定一个最大速度,速度太快体验也不好
                    val maxV = ViewConfiguration.get(context).scaledMaximumFlingVelocity.toFloat()
                    // 这里的 1000 是你想要的速度单位。值1提供像素/毫秒,1000提供像素/秒
                    mVelocityTracker.computeCurrentVelocity(1000, maxV)
                    val yVelocity = -mVelocityTracker.getYVelocity(event.getPointerId(0))

                    startFling(yVelocity.toInt())

                    mVelocityTracker.clear()
                }
                else -> {}
            }

            if (!isActionUp) {
                // 每次触摸事件打点
                mVelocityTracker.addMovement(eventCopy)
            }
            eventCopy.recycle()

            return true
        }

        return false
    }

    private val refreshRunnable = Runnable {
        if (scroller.computeScrollOffset()) {
            scrollTo(0, scroller.currY)
            postOnAnimationFun()
        }
    }

    private fun postOnAnimationFun() {
        // 使Runnable在下一个动画时间步长上执行
        postOnAnimation (refreshRunnable)
    }

    private fun startFling(velocity: Int) {
        // 通知scroller开始计算应该活动到的位置
        scroller.fling(0, scrollY, 0, velocity, Int.MIN_VALUE, Int.MAX_VALUE, Int.MIN_VALUE, Int.MAX_VALUE)

        postOnAnimationFun()
    }

    override fun scrollTo(x: Int, y: Int) {
        val realHeight = getRealHeight()
        val newY = if (y < 0) 0
        else if (y > realHeight) realHeight
        else y
        super.scrollTo(x, newY)
    }

    private fun getRealHeight(): Int {
        var height = 0
        for(index in 0 until childCount) {
            height += getChildAt(index).height
        }
        return height - this.height
    }

}

布局就是在这个类里面填充很多View(放多一些效果好),让它超出屏幕长度,就能滚起来

代码没有考虑很多边缘情况,主要是为了体现惯性滚动的主要实现,对意外情况的考虑需要自行实现



版权声明:本文为qq_45383789原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。