mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor: simplify HDR request tracking per code review
- HdrRequests tracks plain headroom values instead of identity tokens (equal requests are interchangeable) and carries the window's original colorMode, replacing the separate HdrWindowState wrapper. - RequestHdrFor takes `fullscreen` and picks the headroom itself, so the two image views no longer repeat the UNCAPPED/FEED_HEADROOM choice. - ZoomableImageDialog passes imageBounds and the zoom state to DialogContent as lambdas, read only in graphicsLayer. The dialog shell no longer recomposes on every pager swipe frame (was ~60 per swipe, now 0), which was the root cause behind the attribute copy clobbering the dialog's HDR mode.
This commit is contained in:
+5
-5
@@ -65,8 +65,8 @@ class HdrWindowModeTest {
|
||||
|
||||
rule.setContent {
|
||||
window = getActivityWindow()
|
||||
if (showFirst) RequestHdrFor(image, HdrRequests.FEED_HEADROOM)
|
||||
if (showSecond) RequestHdrFor(image, HdrRequests.FEED_HEADROOM)
|
||||
if (showFirst) RequestHdrFor(image, fullscreen = false)
|
||||
if (showSecond) RequestHdrFor(image, fullscreen = false)
|
||||
}
|
||||
rule.waitForIdle()
|
||||
assertEquals(ActivityInfo.COLOR_MODE_HDR, window!!.colorMode)
|
||||
@@ -85,7 +85,7 @@ class HdrWindowModeTest {
|
||||
var window: Window? = null
|
||||
rule.setContent {
|
||||
window = getActivityWindow()
|
||||
RequestHdrFor(sdrImage(), HdrRequests.FEED_HEADROOM)
|
||||
RequestHdrFor(sdrImage(), fullscreen = false)
|
||||
}
|
||||
rule.waitForIdle()
|
||||
assertEquals(ActivityInfo.COLOR_MODE_DEFAULT, window!!.colorMode)
|
||||
@@ -101,7 +101,7 @@ class HdrWindowModeTest {
|
||||
activityWindow = getActivityWindow()
|
||||
Dialog(onDismissRequest = {}) {
|
||||
dialogWindow = getDialogWindow()
|
||||
RequestHdrFor(image, HdrRequests.UNCAPPED)
|
||||
RequestHdrFor(image, fullscreen = true)
|
||||
}
|
||||
}
|
||||
rule.waitForIdle()
|
||||
@@ -118,7 +118,7 @@ class HdrWindowModeTest {
|
||||
|
||||
rule.setContent {
|
||||
window = getActivityWindow()
|
||||
repeat(count) { RequestHdrFor(image, HdrRequests.FEED_HEADROOM) }
|
||||
repeat(count) { RequestHdrFor(image, fullscreen = false) }
|
||||
}
|
||||
rule.waitForIdle()
|
||||
rule.runOnUiThread {
|
||||
|
||||
@@ -38,25 +38,25 @@ import java.util.WeakHashMap
|
||||
* An Ultra HDR photo (a JPEG with a gain map) only renders brighter than SDR white while its
|
||||
* window is in [ActivityInfo.COLOR_MODE_HDR]; otherwise the platform silently draws the SDR base
|
||||
* image. The window is shared by every card in a feed, so HDR stays on until the last image
|
||||
* asking for it leaves composition.
|
||||
* asking for it leaves composition, and then the window gets its [originalColorMode] back.
|
||||
*/
|
||||
class HdrRequests {
|
||||
private val tokens = mutableListOf<Request>()
|
||||
class HdrRequests(
|
||||
val originalColorMode: Int,
|
||||
) {
|
||||
private val headrooms = mutableListOf<Float>()
|
||||
|
||||
private class Request(
|
||||
val headroom: Float,
|
||||
)
|
||||
val wantsHdr: Boolean get() = headrooms.isNotEmpty()
|
||||
|
||||
val wantsHdr: Boolean get() = tokens.isNotEmpty()
|
||||
/** [UNCAPPED] if any request is uncapped or there are none, else the largest cap asked for. */
|
||||
val headroom: Float get() = if (UNCAPPED in headrooms) UNCAPPED else headrooms.maxOrNull() ?: UNCAPPED
|
||||
|
||||
/** [UNCAPPED] if any request is uncapped, else the largest cap asked for. */
|
||||
val headroom: Float
|
||||
get() = if (tokens.any { it.headroom == UNCAPPED }) UNCAPPED else tokens.maxOfOrNull { it.headroom } ?: UNCAPPED
|
||||
fun add(headroom: Float) {
|
||||
headrooms.add(headroom)
|
||||
}
|
||||
|
||||
fun add(headroom: Float): Any = Request(headroom).also { tokens.add(it) }
|
||||
|
||||
fun remove(token: Any) {
|
||||
tokens.removeAll { it === token }
|
||||
/** Drops one request for [headroom]; equal requests are interchangeable. */
|
||||
fun remove(headroom: Float) {
|
||||
headrooms.remove(headroom)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -71,23 +71,16 @@ class HdrRequests {
|
||||
}
|
||||
}
|
||||
|
||||
private class HdrWindowState(
|
||||
val originalColorMode: Int,
|
||||
) {
|
||||
val requests = HdrRequests()
|
||||
}
|
||||
|
||||
// Main-thread only: composition and disposal both run there.
|
||||
private val windowStates = WeakHashMap<Window, HdrWindowState>()
|
||||
private val windowRequests = WeakHashMap<Window, HdrRequests>()
|
||||
|
||||
// Each setter dispatches the window attributes to the window manager even when the value is
|
||||
// unchanged, and cards scroll in and out of a feed constantly: only write what actually changed.
|
||||
private fun Window.applyHdr(state: HdrWindowState) {
|
||||
val mode = if (state.requests.wantsHdr) ActivityInfo.COLOR_MODE_HDR else state.originalColorMode
|
||||
private fun Window.applyHdr(requests: HdrRequests) {
|
||||
val mode = if (requests.wantsHdr) ActivityInfo.COLOR_MODE_HDR else requests.originalColorMode
|
||||
if (colorMode != mode) colorMode = mode
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val headroom = if (state.requests.wantsHdr) state.requests.headroom else HdrRequests.UNCAPPED
|
||||
if (desiredHdrHeadroom != headroom) desiredHdrHeadroom = headroom
|
||||
if (desiredHdrHeadroom != requests.headroom) desiredHdrHeadroom = requests.headroom
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,24 +89,26 @@ fun Image.hasGainmap(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.U
|
||||
/**
|
||||
* Puts the hosting window (the dialog's own window inside a `Dialog`) into HDR mode while this
|
||||
* is in composition and [image] carries a gain map, so an Ultra HDR photo shows its highlights.
|
||||
* A [fullscreen] image gets the display's full HDR range; a feed card is capped.
|
||||
*/
|
||||
@Composable
|
||||
fun RequestHdrFor(
|
||||
image: Image,
|
||||
headroom: Float,
|
||||
fullscreen: Boolean,
|
||||
) {
|
||||
if (!remember(image) { image.hasGainmap() }) return
|
||||
val window = getDialogWindow() ?: getActivityWindow() ?: return
|
||||
val headroom = if (fullscreen) HdrRequests.UNCAPPED else HdrRequests.FEED_HEADROOM
|
||||
|
||||
DisposableEffect(window, headroom) {
|
||||
val state = windowStates.getOrPut(window) { HdrWindowState(window.colorMode) }
|
||||
val token = state.requests.add(headroom)
|
||||
window.applyHdr(state)
|
||||
val requests = windowRequests.getOrPut(window) { HdrRequests(window.colorMode) }
|
||||
requests.add(headroom)
|
||||
window.applyHdr(requests)
|
||||
|
||||
onDispose {
|
||||
state.requests.remove(token)
|
||||
window.applyHdr(state)
|
||||
if (!state.requests.wantsHdr) windowStates.remove(window)
|
||||
requests.remove(headroom)
|
||||
window.applyHdr(requests)
|
||||
if (!requests.wantsHdr) windowRequests.remove(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -184,8 +184,8 @@ fun ZoomableImageDialog(
|
||||
val activityWindow = getActivityWindow()
|
||||
val dialogWindow = getDialogWindow()
|
||||
|
||||
// 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.
|
||||
// Keyed, not inline: each assignment below is a window-manager round trip, so it must run
|
||||
// on orientation changes only, not on every recomposition of this content.
|
||||
DisposableEffect(orientation, activityWindow, dialogWindow) {
|
||||
if (activityWindow != null && dialogWindow != null) {
|
||||
// Preserve what the dialog window owns: the brightness override applied by the
|
||||
@@ -226,9 +226,9 @@ fun ZoomableImageDialog(
|
||||
allImages = allImages,
|
||||
imageUrl = imageUrl,
|
||||
sourceBounds = sourceBounds,
|
||||
imageBounds = imageBounds,
|
||||
imageBounds = { imageBounds },
|
||||
onImageBoundsChanged = updateImageBounds,
|
||||
currentZoomState = currentZoomState,
|
||||
currentZoomState = { currentZoomState },
|
||||
onZoomStateChanged = { currentZoomState = it },
|
||||
progress = progressProvider,
|
||||
onDismiss = dismissWithAnimation,
|
||||
@@ -243,9 +243,9 @@ private fun DialogContent(
|
||||
allImages: ImmutableList<BaseMediaContent>,
|
||||
imageUrl: BaseMediaContent,
|
||||
sourceBounds: Rect?,
|
||||
imageBounds: Rect?,
|
||||
imageBounds: () -> Rect?,
|
||||
onImageBoundsChanged: (Rect) -> Unit,
|
||||
currentZoomState: ZoomState?,
|
||||
currentZoomState: () -> ZoomState?,
|
||||
onZoomStateChanged: (ZoomState) -> Unit,
|
||||
progress: () -> Float,
|
||||
onDismiss: () -> Unit,
|
||||
@@ -282,12 +282,12 @@ private fun DialogContent(
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
val src = sourceBounds
|
||||
val img = imageBounds
|
||||
val img = imageBounds()
|
||||
if (src != null && img != null && src.hasArea() && img.hasArea()) {
|
||||
// Account for user-applied zoom: the exit animation must start from
|
||||
// the visible bounds, not the unzoomed layout bounds — otherwise
|
||||
// dismissing a zoomed-in image jumps.
|
||||
val zoomed = img.zoomedBy(currentZoomState)
|
||||
val zoomed = img.zoomedBy(currentZoomState())
|
||||
// Uniform scale so non-square images keep their aspect ratio during
|
||||
// the grow animation. The image covers the source rect; the overflow
|
||||
// is clipped below.
|
||||
|
||||
+2
-2
@@ -458,7 +458,7 @@ fun LocalImageView(
|
||||
SubcomposeAsyncImageContent(loadedImageModifier)
|
||||
|
||||
val image = (state as AsyncImagePainter.State.Success).result.image
|
||||
RequestHdrFor(image, if (fullResolution) HdrRequests.UNCAPPED else HdrRequests.FEED_HEADROOM)
|
||||
RequestHdrFor(image, fullscreen = fullResolution)
|
||||
|
||||
SideEffect {
|
||||
MediaAspectRatioCache.add(content.localJavaFile.toString(), image.width, image.height)
|
||||
@@ -602,7 +602,7 @@ fun UrlImageView(
|
||||
ShowHashAnimated(content, controllerVisible, Modifier.align(Alignment.TopEnd))
|
||||
|
||||
val image = (state as AsyncImagePainter.State.Success).result.image
|
||||
RequestHdrFor(image, if (fullResolution) HdrRequests.UNCAPPED else HdrRequests.FEED_HEADROOM)
|
||||
RequestHdrFor(image, fullscreen = fullResolution)
|
||||
|
||||
SideEffect {
|
||||
MediaAspectRatioCache.add(content.url, image.width, image.height)
|
||||
|
||||
+13
-21
@@ -26,28 +26,31 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class HdrRequestsTest {
|
||||
private fun requests() = HdrRequests(originalColorMode = 0)
|
||||
|
||||
@Test
|
||||
fun noRequestsMeansSdr() {
|
||||
val requests = HdrRequests()
|
||||
val requests = requests()
|
||||
assertFalse(requests.wantsHdr)
|
||||
assertEquals(HdrRequests.UNCAPPED, requests.headroom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hdrStaysOnUntilTheLastRequestLeaves() {
|
||||
val requests = HdrRequests()
|
||||
val first = requests.add(2f)
|
||||
val second = requests.add(2f)
|
||||
val requests = requests()
|
||||
requests.add(2f)
|
||||
requests.add(2f)
|
||||
|
||||
requests.remove(first)
|
||||
requests.remove(2f)
|
||||
assertTrue(requests.wantsHdr)
|
||||
|
||||
requests.remove(second)
|
||||
requests.remove(2f)
|
||||
assertFalse(requests.wantsHdr)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theLargestCappedHeadroomWins() {
|
||||
val requests = HdrRequests()
|
||||
val requests = requests()
|
||||
requests.add(2f)
|
||||
requests.add(3f)
|
||||
assertEquals(3f, requests.headroom)
|
||||
@@ -55,23 +58,12 @@ class HdrRequestsTest {
|
||||
|
||||
@Test
|
||||
fun anUncappedRequestLiftsTheCap() {
|
||||
val requests = HdrRequests()
|
||||
val requests = requests()
|
||||
requests.add(2f)
|
||||
val fullscreen = requests.add(HdrRequests.UNCAPPED)
|
||||
requests.add(HdrRequests.UNCAPPED)
|
||||
assertEquals(HdrRequests.UNCAPPED, requests.headroom)
|
||||
|
||||
requests.remove(fullscreen)
|
||||
requests.remove(HdrRequests.UNCAPPED)
|
||||
assertEquals(2f, requests.headroom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun equalHeadroomsAreStillSeparateRequests() {
|
||||
val requests = HdrRequests()
|
||||
val first = requests.add(2f)
|
||||
requests.add(2f)
|
||||
|
||||
requests.remove(first)
|
||||
requests.remove(first)
|
||||
assertTrue("removing one token twice must not release the other", requests.wantsHdr)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user