Created
July 30, 2026 14:37
-
-
Save Narayan-Dhingra/8df05354093bec540c6751917f01ba2d to your computer and use it in GitHub Desktop.
iOS-like Predictive Back Animation in Kotlin Multiplatform (Compose)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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