Skip to content

Commit

Permalink
Add support for multiple Haze nodes attached to a HazeState (#441)
Browse files Browse the repository at this point in the history
This PR adds in support for multiple `Modifier.haze` nodes attached to a
`HazeState`. This means that there can be N haze nodes, and M hazeChild
nodes attached to a state.

This has a number of use cases which I'll document, but the main one is
the overlapping area use case:


https://github.com/user-attachments/assets/e7c71898-4988-47f1-b094-c43e6bce7422

- [x] Benchmark performance
- [x] Write docs

Fixes #425, #443
  • Loading branch information
chrisbanes authored Dec 18, 2024
1 parent 64a3edc commit 66fe85d
Show file tree
Hide file tree
Showing 31 changed files with 901 additions and 297 deletions.
Binary file added docs/media/overlap.webp
Binary file not shown.
Binary file added docs/media/sticky.mp4
Binary file not shown.
32 changes: 32 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,35 @@ Scaffold(
}
}
```

## Sticky Headers

The `stickyHeader` functionality on `LazyColumn` and friends is very useful, but unfortunately the limitations of Haze means that blurring the list contents for the header background is tricky.

Since we can not use `Modifier.haze` on the `LazyColumn` and `Modifier.hazeChild` on items, as we would get into recursive drawing, we need to get a bit more creative.

Since we can have multiple nodes using `Modifier.haze`, we can use the modifier on all non-header items, and then use `hazeChild` as normal on the `stickyHeader`:

```kotlin
val hazeState = remember { HazeState() }

LazyColumn(...) {
stickyHeader {
Header(
modifier = Modifier
.hazeChild(state = hazeState),
)
}

items(list) { item ->
Foo(
modifier = Modifier
.haze(hazeState),
)
}
}
```

A more complete example can be found here: [ListWithStickyHeaders](https://github.com/chrisbanes/haze/blob/main/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ListWithStickyHeaders.kt).

![type:video](./media/sticky.mp4)
67 changes: 67 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,70 @@ When using a `Fixed` value, less than 1.0 **may** improve performance, at the sa
If you're looking for a good value to experiment with, `0.66` results in a reduction in total resolution of ~55%, while being visually imperceptible to most people (probably).

The minimum value I would realistically use is somewhere in the region of `0.33`, which results in the total pixel count of only 11% of the original content. This is likely to be visually different to no scaling, but depending on the styling parameters, it will be visually pleasing to the user.

## Using both Modifier.haze and Modifier.hazeChild

A layout node can use both a `Modifier.hazeChild`, drawing a blurred effect from other areas, _and_ use `Modifier.haze` to draw itself for other `hazeChild` users.

This nested functionality sounds complicated, but in reality it enables a simple use case: overlapping blurred layout nodes.

![](./media/overlap.webp)

This code to implement this is like below. You'll notice that the `CreditCard()` nodes use both the `haze` and `hazeChild` modifiers. **Pay attention to the modifier order here.**

``` kotlin
Box {
val hazeState = remember { HazeState() }

Background(
modifier = Modifier
.haze(hazeState, zIndex = 0f)
)

// Rear card
CreditCard(
modifier = Modifier
.haze(hazeState, zIndex = 1f)
.hazeChild(hazeState)
)

// Middle card
CreditCard(
modifier = Modifier
.haze(hazeState, zIndex = 2f)
.hazeChild(hazeState),
)

// Front card
CreditCard(
modifier = Modifier
.haze(hazeState, zIndex = 3f)
.hazeChild(hazeState)
)
}
```

You will notice that there's something different here, the `zIndex` parameter.

For this to work you need to pass in the `zIndex` parameter of the node. It doesn't matter if you use `Modifier.zIndex`, or the implicit ordering from the layout, you need to explicitly pass in a valid `zIndex` value.

### zIndex

Internally, the zIndex value is how Haze knows which layers to draw in which nodes. By default, `hazeChild` will draw all layers with a `zIndex` less than the value of the sibling `Modifier.haze`. So in the example above, the middle card (`zIndex` of 2) will draw the rear card (`zIndex` of 1) and background (`zIndex` of 0).

This default behavior is usually the correct behavior for all use cases, but you can modify this behavior via the `canDrawArea` parameter, which acts as a filter when set:

``` kotlin
CreditCard(
modifier = Modifier
.haze(hazeState, zIndex = 2f, key = "foo")
.hazeChild(hazeState) {
canDrawArea = { area ->
// return true to draw
area.key != "foo"
}
},
)
```

You'll notice that we're using another parameter here, `key`. This just acts as an ID for the node allowing easier filtering. It has serves no other purpose.
48 changes: 40 additions & 8 deletions haze/api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,34 @@ package dev.chrisbanes.haze {
@kotlin.RequiresOptIn(message="Experimental Haze API", level=kotlin.RequiresOptIn.Level.WARNING) public @interface ExperimentalHazeApi {
}

@androidx.compose.runtime.Stable public final class HazeArea {
ctor public HazeArea();
method public androidx.compose.ui.graphics.layer.GraphicsLayer? getContentLayer();
method public Object? getKey();
method public long getPositionOnScreen();
method public long getSize();
method public float getZIndex();
property public final androidx.compose.ui.graphics.layer.GraphicsLayer? contentLayer;
property public final Object? key;
property public final long positionOnScreen;
property public final long size;
property public final float zIndex;
}

public final class HazeChildKt {
method @Deprecated public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, androidx.compose.ui.graphics.Shape shape, dev.chrisbanes.haze.HazeStyle style);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional dev.chrisbanes.haze.HazeStyle style, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.haze.HazeChildScope,kotlin.Unit>? block);
}

@dev.chrisbanes.haze.ExperimentalHazeApi public final class HazeChildNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.node.CompositionLocalConsumerModifierNode androidx.compose.ui.node.DrawModifierNode androidx.compose.ui.node.GlobalPositionAwareModifierNode dev.chrisbanes.haze.HazeChildScope androidx.compose.ui.node.LayoutAwareModifierNode androidx.compose.ui.node.ObserverModifierNode {
@dev.chrisbanes.haze.ExperimentalHazeApi public final class HazeChildNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.node.CompositionLocalConsumerModifierNode androidx.compose.ui.node.DrawModifierNode androidx.compose.ui.node.GlobalPositionAwareModifierNode dev.chrisbanes.haze.HazeChildScope androidx.compose.ui.node.LayoutAwareModifierNode androidx.compose.ui.modifier.ModifierLocalModifierNode androidx.compose.ui.node.ObserverModifierNode {
ctor public HazeChildNode(dev.chrisbanes.haze.HazeState state, optional dev.chrisbanes.haze.HazeStyle style, optional kotlin.jvm.functions.Function1<? super dev.chrisbanes.haze.HazeChildScope,kotlin.Unit>? block);
method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
method public float getAlpha();
method public long getBackgroundColor();
method public kotlin.jvm.functions.Function1<dev.chrisbanes.haze.HazeChildScope,kotlin.Unit>? getBlock();
method public boolean getBlurEnabled();
method public float getBlurRadius();
method public kotlin.jvm.functions.Function1<dev.chrisbanes.haze.HazeArea,java.lang.Boolean>? getCanDrawArea();
method public dev.chrisbanes.haze.HazeTint getFallbackTint();
method public dev.chrisbanes.haze.HazeInputScale getInputScale();
method public androidx.compose.ui.graphics.Brush? getMask();
Expand All @@ -32,6 +47,7 @@ package dev.chrisbanes.haze {
method public void setBlock(kotlin.jvm.functions.Function1<? super dev.chrisbanes.haze.HazeChildScope,kotlin.Unit>?);
method public void setBlurEnabled(boolean);
method public void setBlurRadius(float);
method public void setCanDrawArea(kotlin.jvm.functions.Function1<? super dev.chrisbanes.haze.HazeArea,java.lang.Boolean>?);
method public void setFallbackTint(dev.chrisbanes.haze.HazeTint);
method public void setInputScale(dev.chrisbanes.haze.HazeInputScale);
method public void setMask(androidx.compose.ui.graphics.Brush?);
Expand All @@ -45,6 +61,7 @@ package dev.chrisbanes.haze {
property public final kotlin.jvm.functions.Function1<dev.chrisbanes.haze.HazeChildScope,kotlin.Unit>? block;
property public boolean blurEnabled;
property public float blurRadius;
property public kotlin.jvm.functions.Function1<dev.chrisbanes.haze.HazeArea,java.lang.Boolean>? canDrawArea;
property public dev.chrisbanes.haze.HazeTint fallbackTint;
property public dev.chrisbanes.haze.HazeInputScale inputScale;
property public androidx.compose.ui.graphics.Brush? mask;
Expand All @@ -62,6 +79,7 @@ package dev.chrisbanes.haze {
method public long getBackgroundColor();
method public boolean getBlurEnabled();
method public float getBlurRadius();
method public kotlin.jvm.functions.Function1<dev.chrisbanes.haze.HazeArea,java.lang.Boolean>? getCanDrawArea();
method public dev.chrisbanes.haze.HazeTint getFallbackTint();
method public dev.chrisbanes.haze.HazeInputScale getInputScale();
method public androidx.compose.ui.graphics.Brush? getMask();
Expand All @@ -73,6 +91,7 @@ package dev.chrisbanes.haze {
method public void setBackgroundColor(long);
method public void setBlurEnabled(boolean);
method public void setBlurRadius(float);
method public void setCanDrawArea(kotlin.jvm.functions.Function1<? super dev.chrisbanes.haze.HazeArea,java.lang.Boolean>?);
method public void setFallbackTint(dev.chrisbanes.haze.HazeTint);
method public void setInputScale(dev.chrisbanes.haze.HazeInputScale);
method public void setMask(androidx.compose.ui.graphics.Brush?);
Expand All @@ -84,6 +103,7 @@ package dev.chrisbanes.haze {
property public abstract long backgroundColor;
property public abstract boolean blurEnabled;
property public abstract float blurRadius;
property public abstract kotlin.jvm.functions.Function1<dev.chrisbanes.haze.HazeArea,java.lang.Boolean>? canDrawArea;
property public abstract dev.chrisbanes.haze.HazeTint fallbackTint;
property public abstract dev.chrisbanes.haze.HazeInputScale inputScale;
property public abstract androidx.compose.ui.graphics.Brush? mask;
Expand Down Expand Up @@ -129,17 +149,25 @@ package dev.chrisbanes.haze {
}

public final class HazeKt {
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier haze(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier haze(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional float zIndex, optional Object? key);
}

@dev.chrisbanes.haze.ExperimentalHazeApi public final class HazeNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.node.CompositionLocalConsumerModifierNode androidx.compose.ui.node.DrawModifierNode androidx.compose.ui.node.GlobalPositionAwareModifierNode androidx.compose.ui.node.LayoutAwareModifierNode {
ctor public HazeNode(dev.chrisbanes.haze.HazeState state);
@dev.chrisbanes.haze.ExperimentalHazeApi public final class HazeNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.node.CompositionLocalConsumerModifierNode androidx.compose.ui.node.DrawModifierNode androidx.compose.ui.node.GlobalPositionAwareModifierNode androidx.compose.ui.node.LayoutAwareModifierNode androidx.compose.ui.modifier.ModifierLocalModifierNode androidx.compose.ui.node.ObserverModifierNode {
ctor public HazeNode(dev.chrisbanes.haze.HazeState state, optional float zIndex, optional Object? key);
method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
method public Object? getKey();
method public dev.chrisbanes.haze.HazeState getState();
method public float getZIndex();
method public void onGloballyPositioned(androidx.compose.ui.layout.LayoutCoordinates coordinates);
method public void onObservedReadsChanged();
method public void setKey(Object?);
method public void setState(dev.chrisbanes.haze.HazeState);
method public void setZIndex(float);
property public final Object? key;
property public androidx.compose.ui.modifier.ModifierLocalMap providedValues;
property public boolean shouldAutoInvalidate;
property public final dev.chrisbanes.haze.HazeState state;
property public final float zIndex;
field public static final String TAG = "HazeNode";
}

Expand Down Expand Up @@ -177,10 +205,14 @@ package dev.chrisbanes.haze {

@androidx.compose.runtime.Stable public final class HazeState {
ctor public HazeState();
method public androidx.compose.ui.graphics.layer.GraphicsLayer? getContentLayer();
method public long getPositionOnScreen();
property public final androidx.compose.ui.graphics.layer.GraphicsLayer? contentLayer;
property public final long positionOnScreen;
method public java.util.List<dev.chrisbanes.haze.HazeArea> getAreas();
method @Deprecated public androidx.compose.ui.graphics.layer.GraphicsLayer? getContentLayer();
method @Deprecated public long getPositionOnScreen();
method @Deprecated public void setContentLayer(androidx.compose.ui.graphics.layer.GraphicsLayer?);
method @Deprecated public void setPositionOnScreen(long);
property public final java.util.List<dev.chrisbanes.haze.HazeArea> areas;
property @Deprecated public final androidx.compose.ui.graphics.layer.GraphicsLayer? contentLayer;
property @Deprecated public final long positionOnScreen;
}

@androidx.compose.runtime.Immutable public final class HazeStyle {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 66fe85d

Please sign in to comment.