Preview the previous view on a back gesture (#185)

This commit is contained in:
2026-09-27 12:11:15 +02:00
parent 57dcda5b54
commit e84378cc5b
@@ -1,10 +1,13 @@
package de.jeanlucmakiola.calendula.ui
import androidx.activity.compose.BackHandler
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.SeekableTransitionState
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInHorizontally
import androidx.compose.foundation.layout.Box
@@ -56,6 +59,7 @@ import de.jeanlucmakiola.calendula.ui.week.WeekScreen
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Clock
/**
@@ -358,8 +362,26 @@ fun CalendarHost(
val anyOverlayVisible = showSearch || detailKey != null || createDateIso != null ||
editKey != null || showSettings || showCalendars || showBackup ||
importUri != null || importForm != null
BackHandler(enabled = !anyOverlayVisible && viewStack.size > 1) {
viewStack = viewStack.dropLast(1)
// The view switch runs on a seekable transition, so a back gesture can scrub
// it (#185): the previous view morphs in under the finger, releasing finishes
// the pop and cancelling springs back. Reduced motion keeps a plain back.
val viewTransition = remember { SeekableTransitionState(view) }
LaunchedEffect(view) { viewTransition.animateTo(view) }
val reduceMotion = rememberReduceMotion()
val viewBackEnabled = !anyOverlayVisible && viewStack.size > 1
if (reduceMotion) {
BackHandler(enabled = viewBackEnabled) { viewStack = viewStack.dropLast(1) }
} else {
PredictiveBackHandler(enabled = viewBackEnabled) { events ->
val previous = viewStack[viewStack.size - 2]
try {
events.collect { viewTransition.seekTo(it.progress, targetState = previous) }
viewStack = viewStack.dropLast(1)
} catch (e: CancellationException) {
viewTransition.animateTo(viewTransition.currentState)
throw e
}
}
}
Box(modifier = modifier.fillMaxSize()) {
@@ -368,7 +390,6 @@ fun CalendarHost(
// view keeps the directional slide. What both views show morphs across
// (#184): see [ViewMorphKey]. Reduced motion keeps the plain fade.
val viewSwitch = fadeThrough()
val reduceMotion = rememberReduceMotion()
val morphBounds = rememberViewMorphBoundsTransform()
val morphFade = rememberCalendarFadeSpec()
CompositionLocalProvider(
@@ -378,10 +399,8 @@ fun CalendarHost(
LocalManageCalendars provides { showCalendars = true },
) {
SharedTransitionLayout(Modifier.fillMaxSize()) {
AnimatedContent(
targetState = view,
rememberTransition(viewTransition, label = "view-switch").AnimatedContent(
transitionSpec = { viewSwitch },
label = "view-switch",
) { currentView ->
val sharedScope = this@SharedTransitionLayout
val visibilityScope = this@AnimatedContent