From a20f4b3524afccd92ed21ab933d08a1c728c4934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E8=A6=81=E7=B3=96=E9=86=8B=E6=94=BE=E6=A4=92?= =?UTF-8?q?=E7=9B=90?= <62319450+Moriafly@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:28:39 +0800 Subject: [PATCH] Fix transient stale layouts during window rotation Window metrics can change orientation after Compose has already laid out the current frame. Track that transition until pre-draw, including changes first observed during global layout, and cancel the stale drawing pass after sending pending snapshot apply notifications so the next frame can recompose. Restrict draw cancellation to orientation changes to preserve drawing during same-orientation resizing. Route refresh and reattachment through the same size update method, and clear the pending draw flag when observation stops. Validation: - Adaptive-window Android compilation and SPA Official Debug/Release builds passed, including the release R8 reflection check. - Without a debugger, actual draw diagnostics passed 20 rotations on API 30, 10 on API 31, and 20 on API 32 after the original API 31 build reproduced stale-layout draws on all six observed rotations. - View/ComposeView reattachment, zero-sized initialization, context rebasing, and the existing controlled delayed-metrics recovery paths passed. - Fold/unfold and resize checks passed 43 final native-window size assertions. - SPA playback recordings no longer showed the transient tiny-cover layout seen before this change. Huawei Mate 40 Pro hardware verification is pending. --- .../window/AndroidWindowMetricsState.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/salt-adaptive-window/src/androidMain/kotlin/com/moriafly/salt/adaptive/window/AndroidWindowMetricsState.kt b/salt-adaptive-window/src/androidMain/kotlin/com/moriafly/salt/adaptive/window/AndroidWindowMetricsState.kt index 9d408bf4..6f096761 100644 --- a/salt-adaptive-window/src/androidMain/kotlin/com/moriafly/salt/adaptive/window/AndroidWindowMetricsState.kt +++ b/salt-adaptive-window/src/androidMain/kotlin/com/moriafly/salt/adaptive/window/AndroidWindowMetricsState.kt @@ -30,6 +30,7 @@ import android.view.ViewTreeObserver import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntSize @@ -68,6 +69,7 @@ private class AndroidWindowMetricsState(private val view: View) : private var observer: ViewTreeObserver? = null private var pendingWindowInfoUpdate = false private var lastRootViewSize = IntSize.Zero + private var deferOrientationDraw = false var size by mutableStateOf(readSize()) private set @@ -93,6 +95,13 @@ private class AndroidWindowMetricsState(private val view: View) : // Bounds can reach Resources after the last layout, without another public configuration // callback. This only compares cached inputs; unchanged frames do not query metrics. refreshSizeIfNeeded() + if (deferOrientationDraw) { + deferOrientationDraw = false + // Layout has already finished. Publish the pending snapshot changes and cancel this + // draw so the next frame can recompose and lay out with the new orientation. + Snapshot.sendApplyNotifications() + return false + } return true } @@ -111,8 +120,19 @@ private class AndroidWindowMetricsState(private val view: View) : if (pendingWindowInfoUpdate || rootViewSize != lastRootViewSize || metricsSourceChanged) { pendingWindowInfoUpdate = false lastRootViewSize = rootViewSize - size = readSize(windowMetricsContext) + updateSize(readSize(windowMetricsContext)) + } + } + + private fun updateSize(newSize: DpSize) { + val wasLandscape = size.width > size.height + val isLandscape = newSize.width > newSize.height + if (wasLandscape != isLandscape) { + // Keep the flag until pre-draw: global layout may publish the change first. + // Restrict draw cancellation to orientation changes so continuous resizing can draw. + deferOrientationDraw = true } + size = newSize } override fun onConfigurationChanged(configuration: Configuration) { @@ -135,7 +155,7 @@ private class AndroidWindowMetricsState(private val view: View) : it.addOnGlobalLayoutListener(this) it.addOnPreDrawListener(this) } - size = readSize() + updateSize(readSize()) } private fun stopObservingLayout() { @@ -149,6 +169,7 @@ private class AndroidWindowMetricsState(private val view: View) : observer = null pendingWindowInfoUpdate = false lastRootViewSize = IntSize.Zero + deferOrientationDraw = false } // Match AndroidWindowInfo.calculateWindowSize, including pre-30 window metrics and the