fix: stop the media dialog clobbering its window attributes every frame

ZoomableImageDialog copied the activity window's attributes onto the
dialog window inline in composition. That content recomposes on every
frame of a pager swipe (it reads the image bounds the pager moves), so a
swipe sent ~60 attribute updates to the window manager, and each one
overwrote the dialog's own colorMode and desiredHdrHeadroom with the
feed's. On a Pixel 9a that capped fullscreen HDR photos at the feed's 2x,
kept an SDR page in HDR mode (ratio 5), and made swiping back to an HDR
photo land in SDR depending on timing.

Run the copy in an effect keyed on orientation and the two windows, and
preserve the dialog's colorMode and HDR headroom alongside the brightness
override it already kept. Measured after: HDR page 5, SDR page 1, HDR
again 5, stable across repeated swipes.
This commit is contained in:
davotoula
2026-09-30 11:44:44 +02:00
parent e98c7ba17e
commit bf8e11ae1a
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.WindowManager
@@ -44,6 +45,7 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -182,20 +184,29 @@ fun ZoomableImageDialog(
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
if (activityWindow != null && dialogWindow != null) {
// Preserve any brightness override already applied to the dialog window (e.g. by the
// fullscreen swipe controls). This block re-runs on recomposition (orientation change
// re-reads `orientation` above), and copying the activity attributes would otherwise
// reset screenBrightness and snap the user's brightness back mid-session.
val currentBrightness = dialogWindow.attributes.screenBrightness
val attributes = WindowManager.LayoutParams()
attributes.copyFrom(activityWindow.attributes)
attributes.type = dialogWindow.attributes.type
// Disable the system dim so the thumbnail stays visible behind the growing dialog.
attributes.dimAmount = 0f
attributes.flags = attributes.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()
attributes.screenBrightness = currentBrightness
dialogWindow.attributes = attributes
// Keyed, not inline: this content recomposes on every frame of a pager swipe (the image
// bounds it reads move), and each assignment below is a window-manager round trip.
DisposableEffect(orientation, activityWindow, dialogWindow) {
if (activityWindow != null && dialogWindow != null) {
// Preserve what the dialog window owns: the brightness override applied by the
// fullscreen swipe controls, and the HDR mode [RequestHdrFor] manages for the
// image on screen. Copying the activity's instead would snap brightness back
// mid-session and cap or force HDR by whatever the feed behind happens to show.
val current = dialogWindow.attributes
val attributes = WindowManager.LayoutParams()
attributes.copyFrom(activityWindow.attributes)
attributes.type = current.type
// Disable the system dim so the thumbnail stays visible behind the growing dialog.
attributes.dimAmount = 0f
attributes.flags = attributes.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()
attributes.screenBrightness = current.screenBrightness
attributes.colorMode = current.colorMode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
attributes.desiredHdrHeadroom = current.desiredHdrHeadroom
}
dialogWindow.attributes = attributes
}
onDispose {}
}
// Go fully immersive while the full-screen media viewer is open. Applies to full-screen