@@ -12,8 +12,12 @@ import android.view.Window
1212import android.view.WindowManager
1313import android.view.accessibility.AccessibilityManager
1414import android.widget.FrameLayout
15+ import androidx.activity.compose.BackHandler
1516import androidx.compose.foundation.layout.Box
17+ import androidx.compose.foundation.layout.WindowInsets
1618import androidx.compose.foundation.layout.fillMaxSize
19+ import androidx.compose.foundation.layout.systemBars
20+ import androidx.compose.foundation.layout.windowInsetsPadding
1721import androidx.compose.material3.CircularProgressIndicator
1822import androidx.compose.material3.Text
1923import androidx.compose.runtime.Composable
@@ -88,6 +92,22 @@ fun ReaderScreen(viewModel: ReaderViewModel, onClose: () -> Unit) {
8892 onDispose { mainActivity.onBeforeReaderConfigChange = null }
8993 }
9094
95+ // Reveal the system bars the instant the user leaves the reader (top-bar back button
96+ // or system/predictive back), before the pop animation starts. Immersive reading hides
97+ // them, which drops WindowInsets.systemBars to zero; if they're only re-shown when the
98+ // reader finishes disposing at the END of the pop, the incoming screen's Scaffold
99+ // re-pads the moment they reappear — a visible resize/jump. Showing them up-front lets
100+ // the insets settle while the destination is still fading in.
101+ val readerView = LocalView .current
102+ val insetsController = remember(activity.window, readerView) {
103+ WindowCompat .getInsetsController(activity.window, readerView)
104+ }
105+ val leaveReader: () -> Unit = {
106+ insetsController.show(WindowInsetsCompat .Type .systemBars())
107+ onClose()
108+ }
109+ BackHandler { leaveReader() }
110+
91111 LaunchedEffect (Unit ) { viewModel.load() }
92112
93113 LaunchedEffect (chromeVisible, isDragging, showFontSheet) {
@@ -107,8 +127,6 @@ fun ReaderScreen(viewModel: ReaderViewModel, onClose: () -> Unit) {
107127 ReaderUiState .Loading -> CircularProgressIndicator (Modifier .align(Alignment .Center ))
108128 is ReaderUiState .Error -> Text (s.message, Modifier .align(Alignment .Center ))
109129 is ReaderUiState .Open -> {
110- // Declared before ReaderContent so decor-fits is applied before the navigator
111- // fragment is created (cold start paginates at final edge-to-edge geometry).
112130 // Scoped to the Open state so Loading/Error keep normal, themed system bars.
113131 ImmersiveWindowEffects (
114132 immersive = preferences.immersiveReading,
@@ -119,64 +137,80 @@ fun ReaderScreen(viewModel: ReaderViewModel, onClose: () -> Unit) {
119137 onExit = viewModel::clearPendingResize,
120138 )
121139
122- ReaderContent (
123- publication = s.publication,
124- initialLocator = s.initialLocator,
125- preferences = preferences,
126- onLocator = viewModel::publishLocator,
127- onNavigatorReady = viewModel::bindNavigator,
128- onPrev = viewModel::pageBackward,
129- onNext = viewModel::pageForward,
130- onToggleChrome = viewModel::toggleChrome,
131- onPageLoaded = viewModel::completeViewportResize,
132- )
140+ // The app is edge-to-edge (MainActivity). Immersive reading uses that full
141+ // bleed: content draws behind the (hidden) bars and the chrome self-insets.
142+ // With immersive off, the reader behaves like any normal screen — inset the
143+ // whole subtree (content + chrome) clear of the opaque system bars. Toggling
144+ // this padding is what resizes the WebView on an immersive flip; the
145+ // re-anchor machinery in ImmersiveWindowEffects handles that.
146+ val immersive = preferences.immersiveReading
147+ Box (
148+ modifier = Modifier
149+ .fillMaxSize()
150+ .then(
151+ if (immersive) Modifier
152+ else Modifier .windowInsetsPadding(WindowInsets .systemBars),
153+ ),
154+ ) {
155+ ReaderContent (
156+ publication = s.publication,
157+ initialLocator = s.initialLocator,
158+ preferences = preferences,
159+ onLocator = viewModel::publishLocator,
160+ onNavigatorReady = viewModel::bindNavigator,
161+ onPrev = viewModel::pageBackward,
162+ onNext = viewModel::pageForward,
163+ onToggleChrome = viewModel::toggleChrome,
164+ onPageLoaded = viewModel::completeViewportResize,
165+ )
133166
134- ReaderTopBar (
135- visible = chromeVisible,
136- title = s.document.title,
137- onBack = onClose,
138- onOverflow = { showFontSheet = true },
139- modifier = Modifier .align(Alignment .TopCenter ),
140- edgeToEdge = preferences.immersiveReading,
141- )
142- val positionsList = positions
143- val locationTotal = positionsList?.size?.takeIf { it > 0 }
144- val locationIndex = dragPercent?.let { p ->
145- locationTotal?.let { total ->
146- (p.coerceIn(0.0 , 1.0 ) * (total - 1 )).toInt().coerceIn(0 , total - 1 ) + 1
167+ ReaderTopBar (
168+ visible = chromeVisible,
169+ title = s.document.title,
170+ onBack = leaveReader,
171+ onOverflow = { showFontSheet = true },
172+ modifier = Modifier .align(Alignment .TopCenter ),
173+ edgeToEdge = immersive,
174+ )
175+ val positionsList = positions
176+ val locationTotal = positionsList?.size?.takeIf { it > 0 }
177+ val locationIndex = dragPercent?.let { p ->
178+ locationTotal?.let { total ->
179+ (p.coerceIn(0.0 , 1.0 ) * (total - 1 )).toInt().coerceIn(0 , total - 1 ) + 1
180+ }
147181 }
148- }
149- ReaderBottomBar (
150- visible = chromeVisible,
151- chapterTitle = dragPreview?.title
152- ? : liveLocator?.title
153- ? : s.initialLocator?.title,
154- percent = dragPreview?.locations?.let { it.totalProgression ? : it.progression }
155- ? : liveLocator?.locations?.let { it.totalProgression ? : it.progression }
156- ? : s.savedProgress?.percent ? : 0.0 ,
157- enabled = positionsList?.isNotEmpty() == true ,
158- isDragging = isDragging,
159- locationIndex = locationIndex,
160- locationTotal = locationTotal,
161- onSeekChange = { p ->
162- dragPercent = p
163- dragPreview = viewModel.previewLocator(p)
164- },
165- onSeekFinished = {
166- dragPercent?.let { viewModel.seek(it) }
167- dragPercent = null
168- dragPreview = null
169- },
170- modifier = Modifier .align(Alignment .BottomCenter ),
171- edgeToEdge = preferences.immersiveReading,
172- )
173-
174- if (showFontSheet) {
175- FontSettingsSheet (
176- prefs = preferences,
177- onChange = { next -> viewModel.updatePreferences(next) },
178- onDismiss = { showFontSheet = false },
182+ ReaderBottomBar (
183+ visible = chromeVisible,
184+ chapterTitle = dragPreview?.title
185+ ? : liveLocator?.title
186+ ? : s.initialLocator?.title,
187+ percent = dragPreview?.locations?.let { it.totalProgression ? : it.progression }
188+ ? : liveLocator?.locations?.let { it.totalProgression ? : it.progression }
189+ ? : s.savedProgress?.percent ? : 0.0 ,
190+ enabled = positionsList?.isNotEmpty() == true ,
191+ isDragging = isDragging,
192+ locationIndex = locationIndex,
193+ locationTotal = locationTotal,
194+ onSeekChange = { p ->
195+ dragPercent = p
196+ dragPreview = viewModel.previewLocator(p)
197+ },
198+ onSeekFinished = {
199+ dragPercent?.let { viewModel.seek(it) }
200+ dragPercent = null
201+ dragPreview = null
202+ },
203+ modifier = Modifier .align(Alignment .BottomCenter ),
204+ edgeToEdge = immersive,
179205 )
206+
207+ if (showFontSheet) {
208+ FontSettingsSheet (
209+ prefs = preferences,
210+ onChange = { next -> viewModel.updatePreferences(next) },
211+ onDismiss = { showFontSheet = false },
212+ )
213+ }
180214 }
181215 }
182216 }
@@ -319,10 +353,11 @@ private fun ImmersiveWindowEffects(
319353 // edge-to-edge from the start, so there's nothing to re-anchor.
320354 val firstRun = remember { booleanArrayOf(true ) }
321355
322- // Apply the window mode whenever immersive flips. Arm the re-anchor BEFORE the
323- // decor-fits change (which resizes the WebView) so Readium's drifted emissions are
324- // suppressed until onPageChanged (or the fallback below) restores the anchor. Skipped
325- // on first run — the fragment is created at the final geometry, so no resize occurs.
356+ // Apply the bar cosmetics whenever immersive flips, and arm the re-anchor BEFORE the
357+ // recomposition toggles the reader content's systemBars inset (which resizes the
358+ // WebView) so Readium's drifted emissions are suppressed until onPageChanged (or the
359+ // fallback below) restores the anchor. Skipped on first run — the fragment is created
360+ // at the final geometry, so no resize occurs.
326361 DisposableEffect (immersive) {
327362 if (! firstRun[0 ]) onBeforeResize()
328363 if (immersive) applyImmersive(window, controller) else restoreBars(window, controller, original)
@@ -395,7 +430,9 @@ private data class OriginalBarState(
395430)
396431
397432private fun applyImmersive (window : Window , controller : WindowInsetsControllerCompat ) {
398- WindowCompat .setDecorFitsSystemWindows(window, false )
433+ // NB: decorFitsSystemWindows is set false once, app-wide, in MainActivity — never
434+ // toggled here. Toggling it on the shared single-Activity window resized the window
435+ // on reader exit and showed a visible jump. This only tweaks bar cosmetics/behavior.
399436 window.statusBarColor = AndroidColor .TRANSPARENT
400437 window.navigationBarColor = AndroidColor .TRANSPARENT
401438 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
@@ -416,7 +453,8 @@ private fun restoreBars(
416453 controller : WindowInsetsControllerCompat ,
417454 original : OriginalBarState ,
418455) {
419- WindowCompat .setDecorFitsSystemWindows(window, true )
456+ // Colors/appearance/contrast/cutout only — decorFitsSystemWindows stays false
457+ // (owned by MainActivity), so restoring bars never resizes the window.
420458 window.statusBarColor = original.statusColor
421459 window.navigationBarColor = original.navColor
422460 controller.isAppearanceLightStatusBars = original.lightStatus
0 commit comments