Skip to content

Instantly share code, notes, and snippets.

@sinasamaki
Created July 12, 2024 07:04
Show Gist options
  • Save sinasamaki/e138ba999c575796471a0e2533367aba to your computer and use it in GitHub Desktop.
Save sinasamaki/e138ba999c575796471a0e2533367aba to your computer and use it in GitHub Desktop.
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.Velocity
import kotlinx.coroutines.launch
import kotlin.math.sign
// Overrides to handle fetching the orientation from LazyListState or PagerState
@Composable
fun Modifier.customOverscroll(
listState: LazyListState,
onNewOverscrollAmount: (Float) -> Unit,
animationSpec: SpringSpec<Float> = spring(stiffness = Spring.StiffnessLow)
) = customOverscroll(
orientation = remember { listState.layoutInfo.orientation },
onNewOverscrollAmount = onNewOverscrollAmount,
animationSpec = animationSpec
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Modifier.customOverscroll(
pagerState: PagerState,
onNewOverscrollAmount: (Float) -> Unit,
animationSpec: SpringSpec<Float> = spring(stiffness = Spring.StiffnessLow)
) = customOverscroll(
orientation = remember { pagerState.layoutInfo.orientation },
onNewOverscrollAmount = onNewOverscrollAmount,
animationSpec = animationSpec
)
@Composable
fun Modifier.customOverscroll(
orientation: Orientation,
onNewOverscrollAmount: (Float) -> Unit,
animationSpec: SpringSpec<Float> = spring(stiffness = Spring.StiffnessLow)
): Modifier {
val overscrollAmountAnimatable = remember { Animatable(0f) }
var length by remember { mutableFloatStateOf(1f) }
LaunchedEffect(Unit) {
snapshotFlow { overscrollAmountAnimatable.value }.collect {
onNewOverscrollAmount(
// Change the multiplier to increase or decrease the strength of the value
CustomEasing.transform(it / (length * 1.5f)) * length
)
}
}
val scope = rememberCoroutineScope()
val nestedScrollConnection = remember {
object : NestedScrollConnection {
private fun calculateOverscroll(available: Offset): Float {
val previous = overscrollAmountAnimatable.value
val newValue = previous + when (orientation) {
Orientation.Vertical -> available.y
Orientation.Horizontal -> available.x
}
return when {
previous > 0 -> newValue.coerceAtLeast(0f)
previous < 0 -> newValue.coerceAtMost(0f)
else -> newValue
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
scope.launch {
overscrollAmountAnimatable.snapTo(targetValue = calculateOverscroll(available))
}
return Offset.Zero
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
val availableVelocity = when (orientation) {
Orientation.Vertical -> available.y
Orientation.Horizontal -> available.x
}
overscrollAmountAnimatable.animateTo(
targetValue = 0f,
initialVelocity = availableVelocity,
animationSpec = animationSpec
)
return available
}
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (overscrollAmountAnimatable.value != 0f && source != NestedScrollSource.SideEffect) {
scope.launch {
overscrollAmountAnimatable.snapTo(calculateOverscroll(available))
}
return available
}
return super.onPreScroll(available, source)
}
override suspend fun onPreFling(available: Velocity): Velocity {
val availableVelocity = when (orientation) {
Orientation.Vertical -> available.y
Orientation.Horizontal -> available.x
}
if (overscrollAmountAnimatable.value != 0f && availableVelocity != 0f) {
var consumedVelocity = availableVelocity
val previousSign = overscrollAmountAnimatable.value.sign
val predictedEndValue = exponentialDecay<Float>().calculateTargetValue(
initialValue = overscrollAmountAnimatable.value,
initialVelocity = availableVelocity,
)
if (predictedEndValue.sign == previousSign) {
overscrollAmountAnimatable.animateTo(
targetValue = 0f,
initialVelocity = availableVelocity,
animationSpec = animationSpec,
)
} else {
try {
overscrollAmountAnimatable.animateDecay(
initialVelocity = availableVelocity,
animationSpec = exponentialDecay()
) {
if (value.sign != previousSign) {
consumedVelocity -= velocity
scope.launch {
overscrollAmountAnimatable.snapTo(0f)
}
}
}
} catch (e: Exception) {
// e will probably always be a MutationInterruptedException
// You could throw e if it isn't just to be absolutely sure
}
}
return when (orientation) {
Orientation.Vertical -> Velocity(0f, consumedVelocity)
Orientation.Horizontal -> Velocity(consumedVelocity, 0f)
}
}
return super.onPreFling(available)
}
}
}
return this
.onSizeChanged {
length = when (orientation) {
Orientation.Vertical -> it.height.toFloat()
Orientation.Horizontal -> it.width.toFloat()
}
}
.nestedScroll(nestedScrollConnection)
}
val CustomEasing: Easing = CubicBezierEasing(0.5f, 0.5f, 1.0f, 0.25f)
@yerlansarsenov
Copy link

I could not find the value among availables: NestedScrollSource.SideEffect

@smoralb
Copy link

smoralb commented Aug 9, 2024

@yerlansarsenov That´s because you need to use the last version of the dependency compose ui and use the last beta version -> androidx.compose.ui:ui:1.7.0-beta06 or androidx.compose.ui:ui-tooling:1.7.0-beta06

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment