Skip to content

Commit

Permalink
Reimplement Cupertino Overscroll animation cancellation (#1650)
Browse files Browse the repository at this point in the history
  • Loading branch information
ASalavei authored Oct 29, 2024
1 parent abf3e26 commit c23dd66
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -732,59 +732,58 @@ internal class ScrollingLogic(
}
val availableVelocity = initialVelocity.singleAxisVelocity()

scroll {
val performFling: suspend (Velocity) -> Velocity = { velocity ->
val preConsumedByParent = nestedScrollDispatcher.dispatchPreFling(velocity)
val available = velocity - preConsumedByParent
val velocityLeft = doFlingAnimation(available)
val consumedPost =
nestedScrollDispatcher.dispatchPostFling((available - velocityLeft), velocityLeft)
val totalLeft = velocityLeft - consumedPost
velocity - totalLeft
}
val performFling: suspend (Velocity) -> Velocity = { velocity ->
val preConsumedByParent = nestedScrollDispatcher.dispatchPreFling(velocity)
val available = velocity - preConsumedByParent
val velocityLeft = doFlingAnimation(available)
val consumedPost =
nestedScrollDispatcher.dispatchPostFling((available - velocityLeft), velocityLeft)
val totalLeft = velocityLeft - consumedPost
velocity - totalLeft
}

val overscroll = overscrollEffect
if (overscroll != null && shouldDispatchOverscroll) {
overscroll.applyToFling(availableVelocity, performFling)
} else {
performFling(availableVelocity)
}
val overscroll = overscrollEffect
if (overscroll != null && shouldDispatchOverscroll) {
overscroll.applyToFling(availableVelocity, performFling)
} else {
performFling(availableVelocity)
}
}

@OptIn(ExperimentalFoundationApi::class)
suspend fun NestedScrollScope.doFlingAnimation(available: Velocity): Velocity {
suspend fun doFlingAnimation(available: Velocity): Velocity {
var result: Velocity = available
val nestedScrollScope = this
val reverseScope =
object : ScrollScope {
override fun scrollBy(pixels: Float): Float {
// Fling has hit the bounds or node left composition,
// cancel it to allow continuation. This will conclude this node's fling,
// allowing the onPostFling signal to be called
// with the leftover velocity from the fling animation. Any nested scroll
// node above will be able to pick up the left over velocity and continue
// the fling.
if (NewNestedFlingPropagationEnabled && shouldCancelFling(pixels)) {
throw FlingCancellationException()
}
scroll {
val nestedScrollScope = this
val reverseScope =
object : ScrollScope {
override fun scrollBy(pixels: Float): Float {
// Fling has hit the bounds or node left composition,
// cancel it to allow continuation. This will conclude this node's fling,
// allowing the onPostFling signal to be called
// with the leftover velocity from the fling animation. Any nested scroll
// node above will be able to pick up the left over velocity and continue
// the fling.
if (NewNestedFlingPropagationEnabled && shouldCancelFling(pixels)) {
throw FlingCancellationException()
}

return nestedScrollScope
.scrollByWithOverscroll(
offset = pixels.toOffset().reverseIfNeeded(),
source = SideEffect
return nestedScrollScope
.scrollByWithOverscroll(
offset = pixels.toOffset().reverseIfNeeded(),
source = SideEffect
)
.toFloat()
.reverseIfNeeded()
}
}
with(reverseScope) {
with(flingBehavior) {
result =
result.update(
performFling(available.toFloat().reverseIfNeeded()).reverseIfNeeded()
)
.toFloat()
.reverseIfNeeded()
}
}
with(reverseScope) {
with(flingBehavior) {
result =
result.update(
performFling(available.toFloat().reverseIfNeeded()).reverseIfNeeded()
)
}
}
return result
}
Expand Down Expand Up @@ -858,12 +857,7 @@ private class ScrollableNestedScrollConnection(

override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return if (enabled) {
var velocityLeft: Velocity = available
with(scrollingLogic) {
scroll {
velocityLeft = doFlingAnimation(available)
}
}
val velocityLeft = scrollingLogic.doFlingAnimation(available)
available - velocityLeft
} else {
Velocity.Zero
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

package androidx.compose.foundation.cupertino

import androidx.compose.animation.core.*
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.layout.offset
Expand All @@ -30,10 +33,17 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import kotlin.coroutines.coroutineContext
import kotlin.math.abs
import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive

private enum class CupertinoScrollSource {
Expand Down Expand Up @@ -109,7 +119,7 @@ class CupertinoOverscrollEffect(
*/
private var overscrollOffset: Offset by mutableStateOf(Offset.Zero)

private var lastFlingUncosumedDelta: Offset = Offset.Zero
private var lastFlingUnconsumedDelta: Offset = Offset.Zero
private val visibleOverscrollOffset: IntOffset
get() =
overscrollOffset.reverseHorizontalIfNeeded().rubberBanded().round()
Expand Down Expand Up @@ -215,14 +225,14 @@ class CupertinoOverscrollEffect(
// [unconsumedDelta] is going into overscroll again in case a user drags and hits the
// overscroll->content->overscroll or content->overscroll scenario within single frame
overscrollOffset += unconsumedDelta
lastFlingUncosumedDelta = Offset.Zero
lastFlingUnconsumedDelta = Offset.Zero
delta - unconsumedDelta
}

CupertinoScrollSource.FLING -> {
// If unconsumedDelta is not Zero, [CupertinoFlingEffect] will cancel fling and
// start spring animation instead
lastFlingUncosumedDelta = unconsumedDelta
lastFlingUnconsumedDelta = unconsumedDelta
delta - unconsumedDelta
}
}
Expand All @@ -233,6 +243,9 @@ class CupertinoOverscrollEffect(
source: NestedScrollSource,
performScroll: (Offset) -> Offset
): Offset {
springAnimationScope?.cancel()
springAnimationScope = null

direction = direction.combinedWith(delta.toCupertinoOverscrollDirection())

return source.toCupertinoScrollSource()?.let {
Expand All @@ -249,7 +262,7 @@ class CupertinoOverscrollEffect(
val postFlingVelocity = availableFlingVelocity - velocityConsumedByFling

playSpringAnimation(
lastFlingUncosumedDelta.toFloat(),
lastFlingUnconsumedDelta.toFloat(),
postFlingVelocity.toFloat(),
CupertinoSpringAnimationReason.POSSIBLE_SPRING_IN_THE_END
)
Expand Down Expand Up @@ -326,6 +339,8 @@ class CupertinoOverscrollEffect(
}
}

private var springAnimationScope: CoroutineScope? = null

private suspend fun playSpringAnimation(
unconsumedDelta: Float,
initialVelocity: Float,
Expand All @@ -346,6 +361,7 @@ class CupertinoOverscrollEffect(
visibilityThreshold = visibilityThreshold
)
}

CupertinoSpringAnimationReason.POSSIBLE_SPRING_IN_THE_END -> {
spring(
stiffness = 120f,
Expand All @@ -354,26 +370,34 @@ class CupertinoOverscrollEffect(
}
}

AnimationState(
Float.VectorConverter,
initialValue / density,
initialVelocity / density
).animateTo(
targetValue = 0f,
animationSpec = spec
) {
overscrollOffset = (value * density).toOffset()
currentVelocity = velocity * density

// If it was fling from overscroll, cancel animation and return velocity
if (reason == CupertinoSpringAnimationReason.FLING_FROM_OVERSCROLL && initialSign != 0f && sign(value) != initialSign) {
this.cancelAnimation()
springAnimationScope?.cancel()
springAnimationScope = CoroutineScope(coroutineContext)
springAnimationScope?.run {
AnimationState(
Float.VectorConverter,
initialValue / density,
initialVelocity / density
).animateTo(
targetValue = 0f,
animationSpec = spec
) {
overscrollOffset = (value * density).toOffset()
currentVelocity = velocity * density

// If it was fling from overscroll, cancel animation and return velocity
if (reason == CupertinoSpringAnimationReason.FLING_FROM_OVERSCROLL &&
initialSign != 0f &&
sign(value) != initialSign
) {
this.cancelAnimation()
}
}
springAnimationScope = null
}

if (coroutineContext.isActive) {
// The spring is critically damped, so in case spring-fling-spring sequence
// is slightly offset and velocity is of the opposite sign, it will end up with no animation
// The spring is critically damped, so in case spring-fling-spring sequence is slightly
// offset and velocity is of the opposite sign, it will end up with no animation
overscrollOffset = Offset.Zero
}

Expand Down

0 comments on commit c23dd66

Please sign in to comment.