Skip to content

Instantly share code, notes, and snippets.

@Narayan-Dhingra
Created July 30, 2026 14:37
Show Gist options
  • Select an option

  • Save Narayan-Dhingra/8df05354093bec540c6751917f01ba2d to your computer and use it in GitHub Desktop.

Select an option

Save Narayan-Dhingra/8df05354093bec540c6751917f01ba2d to your computer and use it in GitHub Desktop.
iOS-like Predictive Back Animation in Kotlin Multiplatform (Compose)
import androidx.compose.animation.core.Animatable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback.PredictiveBackAnimatable
import com.arkivanov.essenty.backhandler.BackEvent
@OptIn(ExperimentalDecomposeApi::class)
internal class IosLikePredictiveBackAnimatableImpl(
private val scrimColor: Color = Color(0x66000000), // 40% Black overlay
) : PredictiveBackAnimatable {
// progress: 0f = gesture not started, 1f = gesture fully swiped
private val progressAnim = Animatable(0f)
private var rawProgress: Float by mutableFloatStateOf(0f)
override val exitModifier: Modifier
get() = Modifier.graphicsLayer {
// Active screen follows finger: 0% → 100% of its own width
translationX = rawProgress * size.width
}
override val enterModifier: Modifier
get() = Modifier
.graphicsLayer {
// Previous screen parallaxes from -30% at rest toward 0 on complete
translationX = (1f - rawProgress) * (-0.30f * size.width)
}
.drawWithContent {
drawContent()
// Scrim fades from 40% at rest to 0 as gesture progresses
val scrimAlpha = (1f - rawProgress) * scrimColor.alpha
if (scrimAlpha > 0f) {
drawRect(color = scrimColor.copy(alpha = scrimAlpha))
}
}
override suspend fun animate(event: BackEvent) {
rawProgress = event.progress
}
override suspend fun finish() {
rawProgress = 1f
}
override suspend fun cancel() {
// Snap progressAnim to actual visible position so the spring
// starts from where the finger left off.
progressAnim.snapTo(rawProgress)
progressAnim.animateTo(targetValue = 0f) {
rawProgress = value
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment