Skip to content

Commit

Permalink
Release GraphicsLayer when stopped (#499)
Browse files Browse the repository at this point in the history
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.

Fixes #497
  • Loading branch information
chrisbanes authored Feb 3, 2025
1 parent e99e7d6 commit f807dd8
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 15 deletions.
16 changes: 9 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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" }

Expand All @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions haze/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ kotlin {
dependencies {
api(compose.ui)
implementation(compose.foundation)
implementation(libs.androidx.lifecycle.compose)
}
}

Expand Down
13 changes: 8 additions & 5 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffectNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}
}
}
}
Expand Down
44 changes: 41 additions & 3 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeSourceNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -92,6 +94,7 @@ class HazeSourceNode(
log(TAG) { "onAttach. Adding HazeArea: $area" }
state.addArea(area)
onObservedReadsChanged()
observeLifecycle()
}

override fun onObservedReadsChanged() {
Expand Down Expand Up @@ -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()
}
Expand All @@ -172,6 +181,7 @@ class HazeSourceNode(
override fun onDetach() {
log(TAG) { "onDetach. Removing HazeArea: $area" }
area.reset()
area.releaseLayer()
state.removeArea(area)
}

Expand All @@ -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"
}
Expand Down

0 comments on commit f807dd8

Please sign in to comment.