Core ConceptsΒΆ
This page explains the fundamental concepts and patterns used throughout Haze, regardless of which effect you're using.
HazeStateΒΆ
HazeState is a state holder that manages the rendering targets for visual effects. You create one using rememberHazeState():
val hazeState = rememberHazeState()
This state is then shared between a Modifier.hazeSource (content to blur from) and one or more Modifier.hazeEffect (content to apply the blur to).
ModifiersΒΆ
Haze provides two main modifiers that work together:
Modifier.hazeSourceΒΆ
Marks a composable as a source of content that can be blurred by effects elsewhere in the hierarchy.
LazyColumn(
modifier = Modifier
.fillMaxSize()
.hazeSource(state = hazeState)
) {
// content
}
Parameters:
- state: The
HazeStateinstance to share - zIndex: Optional z-index for layering (when using overlapping effects)
- key: Optional identifier for filtering which areas to draw
Modifier.hazeEffectΒΆ
Applies a visual effect to a composable, drawing blurred content from areas marked with Modifier.hazeSource.
TopAppBar(
modifier = Modifier.hazeEffect(state = hazeState) {
blurEffect {
style = HazeMaterials.thin()
}
}
)
The effect is configured inside the lambda block using effect-specific builders.
HazeEffectScopeΒΆ
The lambda block parameter of Modifier.hazeEffect receives a HazeEffectScope, which provides common properties applicable to all effects:
Common PropertiesΒΆ
modifier = Modifier.hazeEffect(state = hazeState) {
// Common properties
inputScale = HazeInputScale.Auto
drawContentBehind = true
canDrawArea = { area -> true }
// Effect-specific configuration
blurEffect {
// ...
}
}
inputScaleΒΆ
Controls the resolution at which the effect source content is rendered. This is a performance optimization that allows the effect to be applied at a lower resolution before being scaled back up.
Options:
HazeInputScale.None: No scaling (default)HazeInputScale.Auto: Automatic scaling with platform defaultsHazeInputScale.Fixed(value): Fixed scaling factor (0.0 to 1.0)
drawContentBehindΒΆ
When true, the original source content is drawn before the effect is applied. When false, only the effect is drawn. Defaults to true.
canDrawAreaΒΆ
An optional filter function that controls which source areas should be included in the effect rendering. Useful for excluding specific layers:
canDrawArea = { area ->
// return true to include, false to exclude
area.key != "exclude_me"
}
Foreground vs Background EffectsΒΆ
Haze supports two modes of applying effects:
Background Effect (Most Common)ΒΆ
The effect blurs content from elsewhere (behind the composable). Requires both hazeSource and hazeEffect:
Box {
LazyColumn(
modifier = Modifier.hazeSource(state = hazeState)
) {
// content
}
TopAppBar(
modifier = Modifier.hazeEffect(state = hazeState) {
blurEffect { /* ... */ }
}
)
}
Foreground EffectΒΆ
The effect blurs the content within the composable itself. Only requires hazeEffect:
Box(
modifier = Modifier.hazeEffect {
blurEffect { /* ... */ }
}
) {
// This content will be blurred
}
Deep UI HierarchiesΒΆ
When HazeState needs to be passed through many levels of nested composables, you can use a composition local instead:
val LocalHazeState = compositionLocalOf { HazeState() }
@Composable
fun HazeExample() {
val hazeState = rememberHazeState()
CompositionLocalProvider(LocalHazeState provides hazeState) {
Box {
Background()
Foreground()
}
}
}
@Composable
fun Foreground() {
Text(
modifier = Modifier.hazeEffect(state = LocalHazeState.current) {
blurEffect { /* ... */ }
}
)
}
Overlapping EffectsΒΆ
You can have multiple composables that both draw effects from the same source and serve as sources for other effects. This enables complex layering:
Box {
val hazeState = rememberHazeState()
Background(
modifier = Modifier.hazeSource(hazeState, zIndex = 0f)
)
Card(
modifier = Modifier
.hazeSource(hazeState, zIndex = 1f)
.hazeEffect(hazeState)
)
TopAppBar(
modifier = Modifier
.hazeSource(hazeState, zIndex = 2f)
.hazeEffect(hazeState)
)
}
In this example: - Background is at zIndex 0 - Card at zIndex 1 draws the background through its effect - TopAppBar at zIndex 2 draws both background and card through its effect
DialogsΒΆ
When using effects with dialogs, the effect source must be marked before the dialog is shown:
val hazeState = rememberHazeState()
var showDialog by remember { mutableStateOf(false) }
Box {
LazyColumn(
modifier = Modifier.hazeSource(state = hazeState)
) {
// background content
}
if (showDialog) {
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
modifier = Modifier.hazeEffect(state = hazeState) {
blurEffect { /* ... */ }
}
) {
// dialog content
}
}
}
}
Position StrategyΒΆ
Haze needs to calculate the position of source and effect nodes to align the blur correctly. By default, HazeState uses HazePositionStrategy.Auto, which works for most scenarios without any configuration.
val hazeState = rememberHazeState() // Auto strategy (default)
How it worksΒΆ
- Same window: Haze uses root-relative coordinates (
positionInRoot()). This is the most common case and the most performant. - Cross-window (dialogs, popups): Haze automatically detects when source and effect are in different windows and promotes to screen-level coordinates.
Manual overrideΒΆ
In rare cases you may want to force a specific strategy:
// Force root-relative coordinates (same-window only)
val hazeState = rememberHazeState(positionStrategy = HazePositionStrategy.Local)
// Force screen-level coordinates
val hazeState = rememberHazeState(positionStrategy = HazePositionStrategy.Screen)
| Strategy | Coordinates | Use case |
|---|---|---|
Auto |
Adapts automatically | Default β handles everything |
Local |
positionInRoot() |
Same-window; immune to split-window offset issues |
Screen |
positionOnScreen() |
Cross-window setups |
Screenshot TestingΒΆ
Haze supports screenshot testing with platform-specific considerations:
Android with RobolectricΒΆ
The RenderEffect.createBlurEffect() tile mode support was only recently added to Robolectric. For correct results at effect edges, run tests against SDK 35+:
@Config(sdk = [35])
class MyScreenshotTest {
// tests
}
Without this, you may see strange results at effect boundaries on earlier SDK levels (though this doesn't affect real devices).
Other screenshot testing libraries may work but haven't been tested. YMMV.