在实现特定的fling滚动效果时,时常需要计算用户产生的滚动的最终位置,在传统的View体系里用到的就是OverScroller即android.widget.OverScroller,传入初速和初始位置,限制边界,即可快速的计算得到这次滚动的最终位置和到达边界时的剩余速度。
而在Compose中可以使用FlingBehavior实现,直接上代码,很简单看看注释就懂了的。
核心是替换FlingBehavior的MotionDurationScale使其不会一直阻塞直到动画以真实世界的时长执行完毕,这个类还能延申用于控制动画的速率,不过还用不上。
其实没多大用处,主要是为了在迁移到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
}