diff --git a/CHANGELOG.md b/CHANGELOG.md index ae59535..fbff252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ are no binary releases; "version" tracks the shared `version` in the root build ### Fixed +- **`identity`** — A committed predictive back no longer snaps the surface back + to full size before it leaves. `Modifier.predictiveBack()` now holds the + preview transform after `onBack`, and the new `predictiveBackExit()` exit + fades and keeps shrinking from there; pair them on the surface's + `AnimatedVisibility` instead of a full-size slide-out. - **`core-crash`** — The prefilled crash issue is filed in English again. Its title and body came from string resources, so a crash on a French phone opened the tracker with a French issue — unreadable to whoever has to triage diff --git a/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Motion.kt b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Motion.kt index 8431988..1742dfa 100644 --- a/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Motion.kt +++ b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Motion.kt @@ -3,9 +3,12 @@ package de.jeanlucmakiola.floret.identity import android.provider.Settings import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.animation.ExitTransition import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleOut import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme @@ -19,6 +22,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay import kotlin.coroutines.cancellation.CancellationException /** @@ -59,7 +63,11 @@ fun Modifier.animateContentSizeMotion(reduceMotion: Boolean = rememberReduceMoti * The standard Android predictive-back transform for a full-screen surface: as * the back gesture is dragged, the surface scales toward ~90%, shifts toward the * swiped edge and rounds its corners, previewing what's behind. Completing the - * gesture invokes [onBack]; cancelling springs it back. + * gesture invokes [onBack] and keeps the preview transform, so the surface + * leaves from where the finger let go instead of snapping back to full size — + * pair it with [predictiveBackExit] on the surface's own exit. Should the surface + * still be there shortly after (the back was taken without dismissing it), it + * settles back. Cancelling springs it back. * * A drop-in replacement for a screen's own `BackHandler(onBack)` — register it * once and apply the returned [Modifier] to that screen's root so the preview @@ -87,7 +95,8 @@ fun Modifier.predictiveBack( progress.snapTo(FastOutSlowInEasing.transform(event.progress)) } onBack() - progress.snapTo(0f) + delay(COMMIT_SETTLE_DELAY_MILLIS) + progress.animateTo(0f) } catch (_: CancellationException) { progress.animateTo(0f) } @@ -104,3 +113,26 @@ fun Modifier.predictiveBack( clip = p > 0f } } + +/** + * How long a committed preview is held before it settles back — past any exit + * the surface would play, so it only fires for a back that left it in place. + */ +private const val COMMIT_SETTLE_DELAY_MILLIS = 1_000L + +/** + * The exit for a surface closed through [predictiveBack]: it fades while it keeps + * shrinking from the preview's scale, rather than growing back and sliding off + * at full size. Also reads right for a close without a gesture. Fade only under + * reduced motion. + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun predictiveBackExit(reduceMotion: Boolean = rememberReduceMotion()): ExitTransition { + val fade = fadeOut(MaterialTheme.motionScheme.defaultEffectsSpec()) + return if (reduceMotion) { + fade + } else { + fade + scaleOut(MaterialTheme.motionScheme.defaultSpatialSpec(), targetScale = 0.9f) + } +}