From 2779db5e6c4c6e6752520b86d968d040912d3aee Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Sun, 2 Feb 2025 13:58:33 +0000 Subject: [PATCH] Release GraphicsLayer when stopped When the UI host is stopped, release the GraphicsLayer. Android seems to have a issue tracking layers re-paints after an Activity stop + start. Clearing the layer once we're no longer visible fixes it. --- gradle/libs.versions.toml | 16 ++++--- haze/build.gradle.kts | 1 + .../dev/chrisbanes/haze/HazeEffectNode.kt | 13 +++--- .../dev/chrisbanes/haze/HazeSourceNode.kt | 44 +++++++++++++++++-- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83965e9c..b45ab050 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.8.0" androidx-benchmark = "1.3.3" +androidx-lifecycle = "2.8.4" androidx-media3 = "1.4.1" androidx-test-ext-junit = "1.2.1" assertk = "0.28.1" @@ -38,18 +39,19 @@ mavenpublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" } androidx-core = "androidx.core:core-ktx:1.13.1" androidx-activity-compose = "androidx.activity:activity-compose:1.9.3" -androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "jetpack-compose" } -androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "jetpack-compose" } +androidx-lifecycle-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } androidx-profileinstaller = "androidx.profileinstaller:profileinstaller:1.4.1" androidx-test-ext-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-test-ext-junit" } androidx-test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0" +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "jetpack-compose" } androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "jetpack-compose" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "jetpack-compose" } androidx-compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "jetpack-compose" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "jetpack-compose" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "jetpack-compose" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "jetpack-compose" } assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } @@ -63,10 +65,10 @@ ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } -roborazzi-core = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi"} -roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi"} -roborazzi-composedesktop = { module = "io.github.takahirom.roborazzi:roborazzi-compose-desktop", version.ref = "roborazzi"} -roborazzi-junit = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi"} +roborazzi-core = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } +roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } +roborazzi-composedesktop = { module = "io.github.takahirom.roborazzi:roborazzi-compose-desktop", version.ref = "roborazzi" } +roborazzi-junit = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } # Build logic dependencies android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } diff --git a/haze/build.gradle.kts b/haze/build.gradle.kts index 17912d57..b259b2d0 100644 --- a/haze/build.gradle.kts +++ b/haze/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { dependencies { api(compose.ui) implementation(compose.foundation) + implementation(libs.androidx.lifecycle.compose) } } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt index 06df0a23..50757d3b 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt @@ -364,13 +364,16 @@ class HazeEffectNode( translate(position) { // Draw the content into our effect layer. We do want to observe this via snapshot // state - area.contentLayer + val areaLayer = area.contentLayer ?.takeUnless { it.isReleased } ?.takeUnless { it.size.width <= 0 || it.size.height <= 0 } - ?.let { - log(TAG) { "Drawing HazeArea GraphicsLayer: $it" } - drawLayer(it) - } + + if (areaLayer != null) { + log(TAG) { "Drawing HazeArea GraphicsLayer: $areaLayer" } + drawLayer(areaLayer) + } else { + log(TAG) { "HazeArea GraphicsLayer is not valid" } + } } } } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt index 2e899f09..cd9f123b 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.modifier.ModifierLocalModifierNode import androidx.compose.ui.modifier.modifierLocalMapOf import androidx.compose.ui.node.CompositionLocalConsumerModifierNode @@ -30,6 +29,9 @@ import androidx.compose.ui.platform.LocalGraphicsContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.takeOrElse import androidx.compose.ui.unit.toSize +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import kotlinx.coroutines.launch @RequiresOptIn(message = "Experimental Haze API", level = RequiresOptIn.Level.WARNING) annotation class ExperimentalHazeApi @@ -92,6 +94,7 @@ class HazeSourceNode( log(TAG) { "onAttach. Adding HazeArea: $area" } state.addArea(area) onObservedReadsChanged() + observeLifecycle() } override fun onObservedReadsChanged() { @@ -150,16 +153,22 @@ class HazeSourceNode( val contentLayer = area.contentLayer ?.takeUnless { it.isReleased } - ?: graphicsContext.createGraphicsLayer().also { area.contentLayer = it } + ?: graphicsContext.createGraphicsLayer().also { + area.contentLayer = it + log(TAG) { "Updated contentLayer in HazeArea: $area" } + } // First we draw the composable content into a graphics layer contentLayer.record { this@draw.drawContent() + log(TAG) { "Drawn content into layer: $contentLayer" } } // Now we draw `content` into the window canvas drawLayer(contentLayer) + log(TAG) { "Drawn layer to canvas: $contentLayer" } } else { + log(TAG) { "Not using graphics layer, so drawing content direct to canvas" } // If we're not using graphics layers, just call drawContent and return early drawContent() } @@ -172,6 +181,7 @@ class HazeSourceNode( override fun onDetach() { log(TAG) { "onDetach. Removing HazeArea: $area" } area.reset() + area.releaseLayer() state.removeArea(area) } @@ -184,10 +194,38 @@ class HazeSourceNode( positionOnScreen = Offset.Unspecified size = Size.Unspecified contentDrawing = false - contentLayer?.let { currentValueOf(LocalGraphicsContext).releaseGraphicsLayer(it) } + } + + private fun HazeArea.releaseLayer() { + contentLayer?.let { layer -> + log(TAG) { "Releasing content layer: $layer" } + currentValueOf(LocalGraphicsContext).releaseGraphicsLayer(layer) + } contentLayer = null } + private fun observeLifecycle() { + runCatching { currentValueOf(LocalLifecycleOwner) } + .onSuccess { lifecycleOwner -> + coroutineScope.launch { + lifecycleOwner.lifecycle.currentStateFlow.collect { state -> + if (state <= Lifecycle.State.CREATED) { + // When the UI host is stopped, release the GraphicsLayer. Android seems to have a issue + // tracking layers re-paints after an Activity stop + start. Clearing the layer once + // we're no longer visible fixes it 🤷: https://github.com/chrisbanes/haze/issues/497 + area.releaseLayer() + } + } + } + } + .onFailure { error -> + // This is probably because we're built against an old version of androidx.lifecycle + // which doesn't work at runtime on non-Android platforms. Not much we can do here, and + // this workaround is for Android anyway. + log(TAG) { "Error whilst retrieving LocalLifecycleOwner: $error" } + } + } + private companion object { const val TAG = "HazeSource" }