nav: fix blank back-gesture preview — let navigation drive predictive back

The per-screen Modifier.predictiveBack scaled the leaving screen with a
graphicsLayer, but NavHost only composes one back-stack entry at a time, so
nothing was drawn behind the shrinking screen — the gesture revealed the white
window background instead of the previous screen.

Drive predictive back through navigation-compose instead: it composes the
destination being revealed and seeks popExit with the gesture. popExit is now a
scaleOut + fade peek, so the leaving screen shrinks to reveal the real previous
screen held static. Drops the predictiveBack modifier from the destinations and
Settings (its inner section BackHandler is unchanged). The kit's predictiveBack
stays for overlay-style screens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 19:15:24 +02:00
parent 37e3c4e659
commit 9d134be621
2 changed files with 6 additions and 13 deletions

View File

@@ -1,9 +1,10 @@
package de.jeanlucmakiola.agendula.ui.navigation package de.jeanlucmakiola.agendula.ui.navigation
import de.jeanlucmakiola.floret.identity.predictiveBack
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -40,14 +41,13 @@ fun AgendulaNavHost(modifier: Modifier = Modifier) {
modifier = modifier, modifier = modifier,
// Only the screen on top moves; the background is always held static. // Only the screen on top moves; the background is always held static.
// Forward is a plain fade-in over the held screen — calm, not a slide. // Forward is a plain fade-in over the held screen — calm, not a slide.
// Back is the system predictive-back peek: each destination applies // Back is the predictive-back peek: navigation-compose composes the
// Modifier.predictiveBack, so the leaving screen scales/rounds with the // screen being revealed underneath and seeks popExit with the gesture, so
// gesture and the held screen is revealed unmoved. NavHost itself does no // the leaving screen scales down to reveal the (unmoved) previous screen.
// pop animation (the screen owns the motion).
enterTransition = { fadeIn() }, enterTransition = { fadeIn() },
exitTransition = { ExitTransition.None }, exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None }, popEnterTransition = { EnterTransition.None },
popExitTransition = { ExitTransition.None }, popExitTransition = { scaleOut(targetScale = 0.9f) + fadeOut() },
) { ) {
composable(Dest.LISTS) { composable(Dest.LISTS) {
ListsScreen( ListsScreen(
@@ -71,7 +71,6 @@ fun AgendulaNavHost(modifier: Modifier = Modifier) {
LaunchedEffect(filter) { vm.bind(filter) } LaunchedEffect(filter) { vm.bind(filter) }
TaskListScreen( TaskListScreen(
modifier = Modifier.predictiveBack(onBack = { nav.popBackStack() }),
filter = filter, filter = filter,
viewModel = vm, viewModel = vm,
onOpenTask = { taskId -> nav.navigate(Dest.TaskDetail.build(taskId)) }, onOpenTask = { taskId -> nav.navigate(Dest.TaskDetail.build(taskId)) },
@@ -89,7 +88,6 @@ fun AgendulaNavHost(modifier: Modifier = Modifier) {
LaunchedEffect(taskId) { vm.bind(taskId) } LaunchedEffect(taskId) { vm.bind(taskId) }
TaskDetailScreen( TaskDetailScreen(
modifier = Modifier.predictiveBack(onBack = { nav.popBackStack() }),
viewModel = vm, viewModel = vm,
onEdit = { nav.navigate(Dest.TaskEdit.buildEdit(taskId)) }, onEdit = { nav.navigate(Dest.TaskEdit.buildEdit(taskId)) },
onDeleted = { nav.popBackStack() }, onDeleted = { nav.popBackStack() },
@@ -118,7 +116,6 @@ fun AgendulaNavHost(modifier: Modifier = Modifier) {
} }
TaskEditScreen( TaskEditScreen(
modifier = Modifier.predictiveBack(onBack = { nav.popBackStack() }),
viewModel = vm, viewModel = vm,
onSaved = { nav.popBackStack() }, onSaved = { nav.popBackStack() },
onBack = { nav.popBackStack() }, onBack = { nav.popBackStack() },

View File

@@ -93,7 +93,6 @@ import de.jeanlucmakiola.floret.components.pastelize
import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.floret.components.positionOf
import de.jeanlucmakiola.floret.identity.collapseExit import de.jeanlucmakiola.floret.identity.collapseExit
import de.jeanlucmakiola.floret.identity.expandEnter import de.jeanlucmakiola.floret.identity.expandEnter
import de.jeanlucmakiola.floret.identity.predictiveBack
import de.jeanlucmakiola.floret.reminders.ReminderOverride import de.jeanlucmakiola.floret.reminders.ReminderOverride
import de.jeanlucmakiola.agendula.ui.common.reminderLeadTimeLabel import de.jeanlucmakiola.agendula.ui.common.reminderLeadTimeLabel
@@ -128,9 +127,6 @@ fun SettingsScreen(
Box( Box(
modifier = modifier modifier = modifier
// At the hub, the back gesture previews the pop to the lists overview;
// in a section the BackHandler above takes it (section -> hub) instead.
.predictiveBack(onBack = onBack, enabled = section == null)
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surface), .background(MaterialTheme.colorScheme.surface),
) { ) {