diff --git a/docs/usage.md b/docs/usage.md index 62eee943..86c55e9d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -25,7 +25,7 @@ Box { modifier = Modifier // We use hazeChild on anything where we want the background // blurred. - .hazeChild(key = "app_bar", state = hazeState) + .hazeChild(state = hazeState) .fillMaxWidth(), ) } @@ -40,7 +40,7 @@ Haze has some support for blurring of `Shape`s. Each platform has varying suppor To use a shape, you can just pass it in to `hazeChild`: -``` kotlin hl_lines="11" +``` kotlin hl_lines="10" val hazeState = remember { HazeState() } Box { @@ -49,7 +49,6 @@ Box { LargeTopAppBar( modifier = Modifier .hazeChild( - key = "app_bar", state = hazeState, shape = RoundedCornerShape(16.dp), ), @@ -74,7 +73,7 @@ Scaffold( // Need to make app bar transparent to see the content behind colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent), modifier = Modifier - .hazeChild(key = "app_bar", state = hazeState) + .hazeChild(state = hazeState) .fillMaxWidth(), ) { /* todo */ @@ -84,7 +83,7 @@ Scaffold( NavigationBar( containerColor = Color.Transparent, modifier = Modifier - .hazeChild(key = "app_bar", state = hazeState) + .hazeChild(state = hazeState) .fillMaxWidth(), ) { /* todo */ diff --git a/haze-jetpack-compose/api/api.txt b/haze-jetpack-compose/api/api.txt index e600ad65..32e23dd8 100644 --- a/haze-jetpack-compose/api/api.txt +++ b/haze-jetpack-compose/api/api.txt @@ -6,15 +6,15 @@ package dev.chrisbanes.haze { method public long getPositionInRoot(); method public androidx.compose.ui.graphics.Shape getShape(); method public long getSize(); - method public boolean isEmpty(); - property public final boolean isEmpty; + method public boolean isValid(); + property public final boolean isValid; property public final long positionInRoot; property public final androidx.compose.ui.graphics.Shape shape; property public final long size; } public final class HazeChildKt { - method public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, Object key, dev.chrisbanes.haze.HazeState state, optional androidx.compose.ui.graphics.Shape shape); + method public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional androidx.compose.ui.graphics.Shape shape); } public final class HazeDefaults { @@ -33,12 +33,10 @@ package dev.chrisbanes.haze { @androidx.compose.runtime.Stable public final class HazeState { ctor public HazeState(); - method public void clearArea(Object key); - method public java.util.Set getAreas(); - method public void updateAreaPosition(Object key, long positionInRoot); - method public void updateAreaShape(Object key, androidx.compose.ui.graphics.Shape shape); - method public void updateAreaSize(Object key, long size); - property public final java.util.Set areas; + method public java.util.List getAreas(); + method public void registerArea(dev.chrisbanes.haze.HazeArea area); + method public void unregisterArea(dev.chrisbanes.haze.HazeArea area); + property public final java.util.List areas; } } diff --git a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/Haze.kt b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/Haze.kt index fbbaadf7..d4e562d2 100644 --- a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/Haze.kt +++ b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/Haze.kt @@ -5,13 +5,14 @@ package dev.chrisbanes.haze import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.geometry.isUnspecified import androidx.compose.ui.geometry.toRect import androidx.compose.ui.geometry.translate @@ -32,44 +33,33 @@ class HazeState { /** * The areas which are blurred by any [Modifier.haze] instances which use this state. */ - private val _areas = mutableStateMapOf() + private val _areas = mutableStateListOf() - val areas: Set get() = _areas.values.toSet() + val areas: List get() = _areas.toList() - fun updateAreaPosition(key: Any, positionInRoot: Offset) { - _areas.getOrPut(key, ::HazeArea).apply { - this.positionInRoot = positionInRoot - } - } - - fun updateAreaSize(key: Any, size: Size) { - _areas.getOrPut(key, ::HazeArea).apply { - this.size = size - } + fun registerArea(area: HazeArea) { + _areas.add(area) } - fun updateAreaShape(key: Any, shape: Shape) { - _areas.getOrPut(key, ::HazeArea).apply { - this.shape = shape - } - } - - fun clearArea(key: Any) { - _areas.remove(key) + fun unregisterArea(area: HazeArea) { + _areas.remove(area) } } internal fun HazeState.addAreasToPath( path: Path, + positionInRoot: Offset, layoutDirection: LayoutDirection, density: Density, ) { + if (positionInRoot.isUnspecified) return + areas.asSequence() - .filterNot { it.isEmpty } + .filter { it.isValid } .forEach { area -> path.addOutline( outline = area.shape.createOutline(area.size, layoutDirection, density), - offset = area.positionInRoot, + offset = area.positionInRoot - positionInRoot, ) } } @@ -91,13 +81,13 @@ class HazeArea { var shape: Shape by mutableStateOf(RectangleShape) internal set - val isEmpty: Boolean get() = size.isEmpty() + val isValid: Boolean + get() = size.isSpecified && positionInRoot.isSpecified && !size.isEmpty() } internal fun HazeArea.boundsInLocal(hazePositionInRoot: Offset): Rect? { - if (size.isUnspecified) return null + if (!isValid) return null if (hazePositionInRoot.isUnspecified) return null - if (positionInRoot.isUnspecified) return null return size.toRect().translate(positionInRoot - hazePositionInRoot) } diff --git a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeChild.kt b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeChild.kt index 87773c26..978446d6 100644 --- a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeChild.kt +++ b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeChild.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.node.LayoutAwareModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.InspectorInfo -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize /** @@ -21,20 +20,17 @@ import androidx.compose.ui.unit.toSize * [Modifier.haze] to blur any content behind the host composable. */ fun Modifier.hazeChild( - key: Any, state: HazeState, shape: Shape = RectangleShape, -): Modifier = this then HazeChildNodeElement(key, state, shape) +): Modifier = this then HazeChildNodeElement(state, shape) private data class HazeChildNodeElement( - val key: Any, val state: HazeState, val shape: Shape, ) : ModifierNodeElement() { - override fun create(): HazeChildNode = HazeChildNode(key, state, shape) + override fun create(): HazeChildNode = HazeChildNode(state, shape) override fun update(node: HazeChildNode) { - node.key = key node.state = state node.shape = shape node.onUpdate() @@ -42,39 +38,52 @@ private data class HazeChildNodeElement( override fun InspectorInfo.inspectableProperties() { name = "HazeChild" - properties["key"] = key properties["shape"] = shape } } private data class HazeChildNode( - var key: Any, var state: HazeState, var shape: Shape, ) : Modifier.Node(), LayoutAwareModifierNode { - override fun onPlaced(coordinates: LayoutCoordinates) { - // After we've been placed, update the state with our new bounds (in root coordinates) - state.updateAreaPosition(key, coordinates.positionInRoot()) - } + + private val area: HazeArea by lazy { HazeArea() } + + private var attachedState: HazeState? = null override fun onAttach() { - state.updateAreaShape(key, shape) + attachToHazeState() } fun onUpdate() { - state.updateAreaShape(key, shape) - } + // Propagate any shape changes to the HazeArea + area.shape = shape - override fun onRemeasured(size: IntSize) { - // After we've been remeasured, update the state with our new size - state.updateAreaSize(key, size.toSize()) + if (state != attachedState) { + // The provided HazeState has changed, so we need to detach from the old one, + // and attach to the new one + detachFromHazeState() + attachToHazeState() + } } - override fun onReset() { - state.clearArea(key) + override fun onPlaced(coordinates: LayoutCoordinates) { + // After we've been placed, update the state with our new bounds (in root coordinates) + area.positionInRoot = coordinates.positionInRoot() + area.size = coordinates.size.toSize() } override fun onDetach() { - state.clearArea(key) + detachFromHazeState() + } + + private fun attachToHazeState() { + state.registerArea(area) + attachedState = state + } + + private fun detachFromHazeState() { + attachedState?.unregisterArea(area) + attachedState = null } } diff --git a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt index ed7f0274..44f9b44b 100644 --- a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt +++ b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNode31.kt @@ -16,6 +16,7 @@ import android.graphics.Shader.TileMode.REPEAT import androidx.annotation.RequiresApi import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path @@ -39,9 +40,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toSize import dev.chrisbanes.haze.jetpackcompose.R import kotlin.math.roundToInt +import kotlin.properties.Delegates.observable @RequiresApi(31) internal class HazeNode31( @@ -64,29 +68,41 @@ internal class HazeNode31( private var effectsDirty = true private var effects: List = emptyList() - private var positionInRoot = Offset.Zero + + private var positionInRoot by observable(Offset.Unspecified) { _, oldValue, newValue -> + if (oldValue != newValue) { + invalidateEffects() + } + } + private var size by observable(Size.Unspecified) { _, oldValue, newValue -> + if (oldValue != newValue) { + invalidateEffects() + } + } private var noiseTexture: Bitmap? = null private var noiseTextureFactor: Float = Float.MIN_VALUE override fun onUpdate() { - effectsDirty = true - invalidateDraw() + invalidateEffects() } override fun onObservedReadsChanged() { + invalidateEffects() + } + + private fun invalidateEffects() { effectsDirty = true invalidateDraw() } override fun onPlaced(coordinates: LayoutCoordinates) { - val newPositionInRoot = coordinates.positionInRoot() - if (positionInRoot != newPositionInRoot) { - positionInRoot = newPositionInRoot + positionInRoot = coordinates.positionInRoot() + size = coordinates.size.toSize() + } - effectsDirty = true - invalidateDraw() - } + override fun onRemeasured(size: IntSize) { + this.size = size.toSize() } override fun ContentDrawScope.draw() { diff --git a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt index 40a4049c..d67c9a93 100644 --- a/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt +++ b/haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt @@ -4,6 +4,7 @@ package dev.chrisbanes.haze import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -19,7 +20,10 @@ import androidx.compose.ui.node.observeReads import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toSize +import kotlin.properties.Delegates.observable /** * On older platforms, we draw a translucent scrim over the content @@ -38,27 +42,38 @@ internal class HazeNodeBase( private val path = Path() private var pathDirty = false - private var positionInRoot = Offset.Zero + + private var positionInRoot by observable(Offset.Unspecified) { _, oldValue, newValue -> + if (oldValue != newValue) { + invalidatePath() + } + } + private var size by observable(Size.Unspecified) { _, oldValue, newValue -> + if (oldValue != newValue) { + invalidatePath() + } + } override fun onUpdate() { invalidateDraw() } override fun onObservedReadsChanged() { - markPathAsDirty() + invalidatePath() } - private fun markPathAsDirty() { + private fun invalidatePath() { pathDirty = true invalidateDraw() } override fun onPlaced(coordinates: LayoutCoordinates) { - val newPositionInRoot = coordinates.positionInRoot() - if (positionInRoot != newPositionInRoot) { - positionInRoot = newPositionInRoot - markPathAsDirty() - } + positionInRoot = coordinates.positionInRoot() + size = coordinates.size.toSize() + } + + override fun onRemeasured(size: IntSize) { + this.size = size.toSize() } override fun ContentDrawScope.draw() { @@ -77,7 +92,7 @@ internal class HazeNodeBase( private fun updatePath(layoutDirection: LayoutDirection, density: Density) { path.reset() - state.addAreasToPath(path, layoutDirection, density) + state.addAreasToPath(path, positionInRoot, layoutDirection, density) pathDirty = false } } diff --git a/haze/api/api.txt b/haze/api/api.txt index e600ad65..32e23dd8 100644 --- a/haze/api/api.txt +++ b/haze/api/api.txt @@ -6,15 +6,15 @@ package dev.chrisbanes.haze { method public long getPositionInRoot(); method public androidx.compose.ui.graphics.Shape getShape(); method public long getSize(); - method public boolean isEmpty(); - property public final boolean isEmpty; + method public boolean isValid(); + property public final boolean isValid; property public final long positionInRoot; property public final androidx.compose.ui.graphics.Shape shape; property public final long size; } public final class HazeChildKt { - method public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, Object key, dev.chrisbanes.haze.HazeState state, optional androidx.compose.ui.graphics.Shape shape); + method public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional androidx.compose.ui.graphics.Shape shape); } public final class HazeDefaults { @@ -33,12 +33,10 @@ package dev.chrisbanes.haze { @androidx.compose.runtime.Stable public final class HazeState { ctor public HazeState(); - method public void clearArea(Object key); - method public java.util.Set getAreas(); - method public void updateAreaPosition(Object key, long positionInRoot); - method public void updateAreaShape(Object key, androidx.compose.ui.graphics.Shape shape); - method public void updateAreaSize(Object key, long size); - property public final java.util.Set areas; + method public java.util.List getAreas(); + method public void registerArea(dev.chrisbanes.haze.HazeArea area); + method public void unregisterArea(dev.chrisbanes.haze.HazeArea area); + property public final java.util.List areas; } } diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt index 40a4049c..d67c9a93 100644 --- a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNodeBase.kt @@ -4,6 +4,7 @@ package dev.chrisbanes.haze import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.ContentDrawScope @@ -19,7 +20,10 @@ import androidx.compose.ui.node.observeReads import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.toSize +import kotlin.properties.Delegates.observable /** * On older platforms, we draw a translucent scrim over the content @@ -38,27 +42,38 @@ internal class HazeNodeBase( private val path = Path() private var pathDirty = false - private var positionInRoot = Offset.Zero + + private var positionInRoot by observable(Offset.Unspecified) { _, oldValue, newValue -> + if (oldValue != newValue) { + invalidatePath() + } + } + private var size by observable(Size.Unspecified) { _, oldValue, newValue -> + if (oldValue != newValue) { + invalidatePath() + } + } override fun onUpdate() { invalidateDraw() } override fun onObservedReadsChanged() { - markPathAsDirty() + invalidatePath() } - private fun markPathAsDirty() { + private fun invalidatePath() { pathDirty = true invalidateDraw() } override fun onPlaced(coordinates: LayoutCoordinates) { - val newPositionInRoot = coordinates.positionInRoot() - if (positionInRoot != newPositionInRoot) { - positionInRoot = newPositionInRoot - markPathAsDirty() - } + positionInRoot = coordinates.positionInRoot() + size = coordinates.size.toSize() + } + + override fun onRemeasured(size: IntSize) { + this.size = size.toSize() } override fun ContentDrawScope.draw() { @@ -77,7 +92,7 @@ internal class HazeNodeBase( private fun updatePath(layoutDirection: LayoutDirection, density: Density) { path.reset() - state.addAreasToPath(path, layoutDirection, density) + state.addAreasToPath(path, positionInRoot, layoutDirection, density) pathDirty = false } } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt index fbbaadf7..d4e562d2 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt @@ -5,13 +5,14 @@ package dev.chrisbanes.haze import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.geometry.isUnspecified import androidx.compose.ui.geometry.toRect import androidx.compose.ui.geometry.translate @@ -32,44 +33,33 @@ class HazeState { /** * The areas which are blurred by any [Modifier.haze] instances which use this state. */ - private val _areas = mutableStateMapOf() + private val _areas = mutableStateListOf() - val areas: Set get() = _areas.values.toSet() + val areas: List get() = _areas.toList() - fun updateAreaPosition(key: Any, positionInRoot: Offset) { - _areas.getOrPut(key, ::HazeArea).apply { - this.positionInRoot = positionInRoot - } - } - - fun updateAreaSize(key: Any, size: Size) { - _areas.getOrPut(key, ::HazeArea).apply { - this.size = size - } + fun registerArea(area: HazeArea) { + _areas.add(area) } - fun updateAreaShape(key: Any, shape: Shape) { - _areas.getOrPut(key, ::HazeArea).apply { - this.shape = shape - } - } - - fun clearArea(key: Any) { - _areas.remove(key) + fun unregisterArea(area: HazeArea) { + _areas.remove(area) } } internal fun HazeState.addAreasToPath( path: Path, + positionInRoot: Offset, layoutDirection: LayoutDirection, density: Density, ) { + if (positionInRoot.isUnspecified) return + areas.asSequence() - .filterNot { it.isEmpty } + .filter { it.isValid } .forEach { area -> path.addOutline( outline = area.shape.createOutline(area.size, layoutDirection, density), - offset = area.positionInRoot, + offset = area.positionInRoot - positionInRoot, ) } } @@ -91,13 +81,13 @@ class HazeArea { var shape: Shape by mutableStateOf(RectangleShape) internal set - val isEmpty: Boolean get() = size.isEmpty() + val isValid: Boolean + get() = size.isSpecified && positionInRoot.isSpecified && !size.isEmpty() } internal fun HazeArea.boundsInLocal(hazePositionInRoot: Offset): Rect? { - if (size.isUnspecified) return null + if (!isValid) return null if (hazePositionInRoot.isUnspecified) return null - if (positionInRoot.isUnspecified) return null return size.toRect().translate(positionInRoot - hazePositionInRoot) } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt index 87773c26..978446d6 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.node.LayoutAwareModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.InspectorInfo -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize /** @@ -21,20 +20,17 @@ import androidx.compose.ui.unit.toSize * [Modifier.haze] to blur any content behind the host composable. */ fun Modifier.hazeChild( - key: Any, state: HazeState, shape: Shape = RectangleShape, -): Modifier = this then HazeChildNodeElement(key, state, shape) +): Modifier = this then HazeChildNodeElement(state, shape) private data class HazeChildNodeElement( - val key: Any, val state: HazeState, val shape: Shape, ) : ModifierNodeElement() { - override fun create(): HazeChildNode = HazeChildNode(key, state, shape) + override fun create(): HazeChildNode = HazeChildNode(state, shape) override fun update(node: HazeChildNode) { - node.key = key node.state = state node.shape = shape node.onUpdate() @@ -42,39 +38,52 @@ private data class HazeChildNodeElement( override fun InspectorInfo.inspectableProperties() { name = "HazeChild" - properties["key"] = key properties["shape"] = shape } } private data class HazeChildNode( - var key: Any, var state: HazeState, var shape: Shape, ) : Modifier.Node(), LayoutAwareModifierNode { - override fun onPlaced(coordinates: LayoutCoordinates) { - // After we've been placed, update the state with our new bounds (in root coordinates) - state.updateAreaPosition(key, coordinates.positionInRoot()) - } + + private val area: HazeArea by lazy { HazeArea() } + + private var attachedState: HazeState? = null override fun onAttach() { - state.updateAreaShape(key, shape) + attachToHazeState() } fun onUpdate() { - state.updateAreaShape(key, shape) - } + // Propagate any shape changes to the HazeArea + area.shape = shape - override fun onRemeasured(size: IntSize) { - // After we've been remeasured, update the state with our new size - state.updateAreaSize(key, size.toSize()) + if (state != attachedState) { + // The provided HazeState has changed, so we need to detach from the old one, + // and attach to the new one + detachFromHazeState() + attachToHazeState() + } } - override fun onReset() { - state.clearArea(key) + override fun onPlaced(coordinates: LayoutCoordinates) { + // After we've been placed, update the state with our new bounds (in root coordinates) + area.positionInRoot = coordinates.positionInRoot() + area.size = coordinates.size.toSize() } override fun onDetach() { - state.clearArea(key) + detachFromHazeState() + } + + private fun attachToHazeState() { + state.registerArea(area) + attachedState = state + } + + private fun detachFromHazeState() { + attachedState?.unregisterArea(area) + attachedState = null } } diff --git a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/CardSample.kt b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/CardSample.kt index 09a4ca7b..14b44f01 100644 --- a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/CardSample.kt +++ b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/CardSample.kt @@ -92,7 +92,7 @@ fun CreditCardSample(navigator: Navigator) { } }, ) - .hazeChild("card", state = hazeState, shape = RoundedCornerShape(16.dp)), + .hazeChild(state = hazeState, shape = RoundedCornerShape(16.dp)), ) { Column(Modifier.padding(32.dp)) { Text("Bank of Haze") diff --git a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ImagesList.kt b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ImagesList.kt new file mode 100644 index 00000000..7d416d10 --- /dev/null +++ b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ImagesList.kt @@ -0,0 +1,96 @@ +// Copyright 2023, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.sample.android + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.seiko.imageloader.rememberImagePainter +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImagesList(navigator: Navigator) { + MaterialTheme { + Scaffold( + topBar = { + LargeTopAppBar( + title = { Text(text = "Images") }, + navigationIcon = { + IconButton(onClick = navigator::navigateUp) { + Icon(Icons.Default.ArrowBack, null) + } + }, + modifier = Modifier.fillMaxWidth(), + ) + }, + modifier = Modifier.fillMaxSize(), + ) { contentPadding -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = contentPadding, + modifier = Modifier.fillMaxSize(), + ) { + items(50) { index -> + val hazeState = remember { HazeState() } + + Box( + modifier = Modifier + .fillParentMaxWidth() + .height(160.dp), + ) { + Image( + painter = rememberImagePainter(rememberRandomSampleImageUrl(width = 800)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .haze( + state = hazeState, + backgroundColor = Color(0xFF646464), + tint = Color(0x4D646464), + ) + .fillMaxSize(), + ) + + Box( + modifier = Modifier + .fillMaxSize(0.8f) + .align(Alignment.Center) + .hazeChild(state = hazeState, shape = RoundedCornerShape(4.dp)), + ) { + Text( + "Image $index", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + } + } + } +} diff --git a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/Samples.kt b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/Samples.kt index 21b51261..97878d79 100644 --- a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/Samples.kt +++ b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/Samples.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier val Samples = listOf( Sample("Scaffold") { ScaffoldSample(it) }, Sample("Credit Card") { CreditCardSample(it) }, + Sample("Images List") { ImagesList(it) }, ) data class Sample( diff --git a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ScaffoldSample.kt b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ScaffoldSample.kt index d90cef80..7c8ac719 100644 --- a/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ScaffoldSample.kt +++ b/sample/android-jetpack/src/main/kotlin/dev/chrisbanes/haze/sample/android/ScaffoldSample.kt @@ -63,7 +63,7 @@ fun ScaffoldSample(navigator: Navigator) { }, colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent), modifier = Modifier - .hazeChild("app_bar", hazeState) + .hazeChild(hazeState) .fillMaxWidth(), ) }, @@ -79,7 +79,7 @@ fun ScaffoldSample(navigator: Navigator) { selectedIndex, onItemClicked = { selectedIndex = it }, modifier = Modifier - .hazeChild("nav_bar", hazeState) + .hazeChild(hazeState) .fillMaxWidth(), ) } diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt index f8d1e0ad..cc4f00e4 100644 --- a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt @@ -94,7 +94,7 @@ fun CreditCardSample(navigator: Navigator) { } }, ) - .hazeChild("card", state = hazeState, shape = RoundedCornerShape(16.dp)), + .hazeChild(state = hazeState, shape = RoundedCornerShape(16.dp)), ) { Column(Modifier.padding(32.dp)) { Text("Bank of Haze") diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ImagesList.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ImagesList.kt new file mode 100644 index 00000000..17d35a59 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ImagesList.kt @@ -0,0 +1,96 @@ +// Copyright 2023, Christopher Banes and the Haze project contributors +// SPDX-License-Identifier: Apache-2.0 + +package dev.chrisbanes.haze.sample + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.seiko.imageloader.rememberImagePainter +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.haze +import dev.chrisbanes.haze.hazeChild + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImagesList(navigator: Navigator) { + MaterialTheme { + Scaffold( + topBar = { + LargeTopAppBar( + title = { Text(text = "Images") }, + navigationIcon = { + IconButton(onClick = navigator::navigateUp) { + Icon(Icons.Default.ArrowBack, null) + } + }, + modifier = Modifier.fillMaxWidth(), + ) + }, + modifier = Modifier.fillMaxSize(), + ) { contentPadding -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = contentPadding, + modifier = Modifier.fillMaxSize(), + ) { + items(50) { index -> + val hazeState = remember { HazeState() } + + Box( + modifier = Modifier + .fillParentMaxWidth() + .height(160.dp), + ) { + Image( + painter = rememberImagePainter(rememberRandomSampleImageUrl(width = 800)), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .haze( + state = hazeState, + backgroundColor = Color(0xFF646464), + tint = Color(0x4D646464), + ) + .fillMaxSize(), + ) + + Box( + modifier = Modifier + .fillMaxSize(0.8f) + .align(Alignment.Center) + .hazeChild(state = hazeState, shape = RoundedCornerShape(4.dp)), + ) { + Text( + "Image $index", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + } + } + } +} diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt index 4f81408c..396d6237 100644 --- a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/Samples.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier val Samples = listOf( Sample("Scaffold") { ScaffoldSample(it) }, Sample("Credit Card") { CreditCardSample(it) }, + Sample("Images List") { ImagesList(it) }, ) data class Sample( diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt index ef0ad348..6d1b2f9d 100644 --- a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt @@ -63,7 +63,7 @@ fun ScaffoldSample(navigator: Navigator) { }, colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent), modifier = Modifier - .hazeChild("app_bar", hazeState) + .hazeChild(hazeState) .fillMaxWidth(), ) }, @@ -79,7 +79,7 @@ fun ScaffoldSample(navigator: Navigator) { selectedIndex, onItemClicked = { selectedIndex = it }, modifier = Modifier - .hazeChild("nav_bar", hazeState) + .hazeChild(hazeState) .fillMaxWidth(), ) }