[go: nahoru, domu]

blob: 36268aa99ed12c2099503af4edf1fe24e5b07858 [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.compose.material
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.ui.util.lerp
import kotlin.math.abs
import kotlin.math.roundToInt
import kotlin.math.sqrt
@Suppress("DEPRECATION")
internal class ScalingLazyColumnSnapFlingBehavior(
val state: ScalingLazyListState,
val snapOffset: Int = 0,
val decay: DecayAnimationSpec<Float> = exponentialDecay()
) : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
val animationState = AnimationState(
initialValue = 0f,
initialVelocity = initialVelocity,
)
var lastValue = 0f
val visibleItemsInfo = state.layoutInfo.visibleItemsInfo
val isAFling = abs(initialVelocity) > 1f && visibleItemsInfo.size > 1
val finalTarget = if (isAFling) {
// Target we will land on given initialVelocity & decay
val decayTarget = decay.calculateTargetValue(0f, initialVelocity)
var endOfListReached = false
animationState.animateDecay(decay) {
val delta = value - lastValue
val consumed = scrollBy(delta)
lastValue = value
// When we are "slow" enough, switch from decay to the final snap.
if (abs(velocity) < SNAP_SPEED_THRESHOLD) cancelAnimation()
// If we can't consume the scroll, also stop.
if (abs(delta - consumed) > 0.1f) {
endOfListReached = true
cancelAnimation()
}
}
if (endOfListReached) {
// We couldn't scroll as much as we wanted, likely we reached the end of the list,
// Snap to the current item and finish.
scrollBy((snapOffset - state.centerItemScrollOffset).toFloat())
return animationState.velocity
} else {
// Now that scrolling slowed down, adjust the animation to land in the item closest
// to the original target. Note that the target may be off-screen, in that case we
// will land on the last visible item in that direction.
(state.layoutInfo.visibleItemsInfo
.map { animationState.value + it.unadjustedOffset + snapOffset }
.minByOrNull { abs(it - decayTarget) } ?: decayTarget)
}
} else {
// Not a fling, just snap to the current item.
(snapOffset - state.centerItemScrollOffset).toFloat()
}
// We have a velocity (animationState.velocity), and a target (finalTarget),
// Construct a cubic bezier with the given initial velocity, and ending at 0 speed,
// unless that will mean that we need to accelerate and decelerate.
// We can also control the inertia of these speeds, i.e. how much it will accelerate/
// decelerate at the beginning and end.
val distance = finalTarget - animationState.value
// If the distance to fling is zero, nothing to do (and must avoid divide-by-zero below).
if (distance != 0.0f) {
val initialSpeed = animationState.velocity
// Inertia of the initial speed.
val initialInertia = 0.5f
// Compute how much time we want to spend on the final snap, depending on the speed
val finalSnapDuration = lerp(
FINAL_SNAP_DURATION_MIN, FINAL_SNAP_DURATION_MAX,
abs(initialSpeed) / SNAP_SPEED_THRESHOLD
)
// Initial control point. Has slope (velocity) adjustedSpeed and magnitude (inertia)
// initialInertia
val adjustedSpeed = initialSpeed * finalSnapDuration / distance
val easingX0 = initialInertia / sqrt(1f + adjustedSpeed * adjustedSpeed)
val easingY0 = easingX0 * adjustedSpeed
// Final control point. Has slope 0, unless that will make us accelerate then decelerate,
// in that case we set the slope to 1.
val easingX1 = 0.8f
val easingY1 = if (easingX0 > easingY0) 0.8f else 1f
animationState.animateTo(
finalTarget, tween(
(finalSnapDuration * 1000).roundToInt(),
easing = CubicBezierEasing(easingX0, easingY0, easingX1, easingY1)
)
) {
scrollBy(value - lastValue)
lastValue = value
}
}
return animationState.velocity
}
// Speed, in pixels per second, to switch between decay and final snap.
private val SNAP_SPEED_THRESHOLD = 1200
// Minimum duration for the final snap after the fling, in seconds, used when the initial speed
// is 0
private val FINAL_SNAP_DURATION_MIN = .1f
// Maximum duration for the final snap after the fling, in seconds, used when the speed is
// SNAP_SPEED_THRESHOLD
private val FINAL_SNAP_DURATION_MAX = .35f
}