Compose上使用FlingBehavior实现OverScroller

234 阅读2分钟

在实现特定的fling滚动效果时,时常需要计算用户产生的滚动的最终位置,在传统的View体系里用到的就是OverScrollerandroid.widget.OverScroller,传入初速和初始位置,限制边界,即可快速的计算得到这次滚动的最终位置和到达边界时的剩余速度。

而在Compose中可以使用FlingBehavior实现,直接上代码,很简单看看注释就懂了的。

核心是替换FlingBehaviorMotionDurationScale使其不会一直阻塞直到动画以真实世界的时长执行完毕,这个类还能延申用于控制动画的速率,不过还用不上。

其实没多大用处,主要是为了在迁移到CMP项目时能直接替换

/**
 * 用于计算滚动的最终位置和最终速度的实现
 */
class OverScroller(
    animationSpec: DecayAnimationSpec<Float> = exponentialDecay()
) {
    private val flingBehavior = NoMotionFlingBehavior(animationSpec)
    private var position: Float = 0f
    private var minPosition: Float = 0f
    private var maxPosition: Float = Float.MAX_VALUE
    private val scrollScope = object : ScrollScope {
        override fun scrollBy(pixels: Float): Float {
            val oldPosition = position
            position = (position + pixels).coerceIn(minPosition, maxPosition)
            return position - oldPosition
        }
    }

    /**
     * 滚动的最终位置,需要在调用[fling]函数后获取
     */
    val finalPosition: Float
        get() = position

    /**
     * fling实现
     *
     * @param initialVelocity  初始速度
     * @param startPosition    起始位置
     * @param min              最小值
     * @param max              最大值
     *
     * @return 到达边界时的最终速度,或未到达边界速度减至0
     */
    suspend fun fling(
        initialVelocity: Float,
        startPosition: Float = position,
        min: Float = minPosition,
        max: Float = maxPosition
    ): Float {
        position = startPosition
        minPosition = min
        maxPosition = max

        return with(flingBehavior) {
            scrollScope.performFling(initialVelocity = initialVelocity)
        }
    }
}

/**
 * 默认的DefaultFlingBehavior的Copy,
 * 替换了[motionDurationScale]为[DisableScrollMotionDurationScale]
 */
internal class NoMotionFlingBehavior(
    private val flingDecay: DecayAnimationSpec<Float>,
    private val motionDurationScale: MotionDurationScale = DisableScrollMotionDurationScale
) : FlingBehavior {

    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
        return withContext(motionDurationScale) {
            if (abs(initialVelocity) > 1f) {
                var velocityLeft = initialVelocity
                var lastValue = 0f
                val animationState =
                    AnimationState(
                        initialValue = 0f,
                        initialVelocity = initialVelocity,
                    )
                try {
                    animationState.animateDecay(flingDecay) {
                        val delta = value - lastValue
                        val consumed = scrollBy(delta)
                        lastValue = value
                        velocityLeft = this.velocity
                        // avoid rounding errors and stop if anything is unconsumed
                        if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
                    }
                } catch (exception: CancellationException) {
                    velocityLeft = animationState.velocity
                }
                velocityLeft
            } else {
                initialVelocity
            }
        }
    }
}

/**
 * [MotionDurationScale.scaleFactor]为0f时,动画会在下一帧内直接完成,而不会阻塞
 * 0f would cause motion to finish in the next frame callback.
 */
internal val DisableScrollMotionDurationScale =
    object : MotionDurationScale {
        override val scaleFactor: Float
            get() = 0f
    }