Core conceptsΒΆ
Haze separates captured input, shareable configuration, and node-owned rendering resources.
SourcesΒΆ
HazeState connects one or more hazeSource modifiers to source-backed effects:
val hazeState = rememberHazeState()
LazyColumn(
modifier = Modifier.hazeSource(hazeState),
) {
// Content
}
Sources can have a zIndex and metadata key. HazeSourceSelection.Behind is the default. With
a nearest ancestor hazeSource using the same state, it selects lower-z sources; without one, it
selects every source. HazeSourceSelection.All bypasses the ancestor relationship.
Refine either selection with where. Predicates receive only immutable key and zIndex
metadata, not captured pixels or renderer resources. Repeated refinements combine with logical
AND:
val selection = HazeSourceSelection.Behind
.where { source -> source.zIndex >= 1f }
.where { source -> source.key != "sensitive" }
Explicit inputsΒΆ
Typed effects always declare what they consume:
HazeInput.Sources(hazeState)consumes captured source content.HazeInput.Contentconsumes the modifier's own content.
Source-backed input also declares retention:
HazeInput.Sources(
state = hazeState,
retention = HazeSourceRetention.ClearWhenUnavailable,
)
KeepLastFrame smooths temporary source gaps. ClearWhenUnavailable clears retained output as
soon as no selected source is drawable.
Typed BlurΒΆ
Blur has an ordinary typed modifier:
Modifier.hazeBlur(
input = HazeInput.Sources(hazeState),
style = HazeMaterials.thin(),
sampling = HazeSampling.Adaptive,
expandLayerBounds = true,
)
Use HazeInput.Content for own-content Blur. The structural input, retention, sampling, and layer
expansion policies do not live in HazeBlurStyle.
HazeBlurStyle is an opaque replayable program:
val style = HazeBlurStyle {
blurRadius(20.dp)
colorEffects(
listOf(HazeColorEffect.tint(Color.White.copy(alpha = 0.12f))),
)
}.then {
noiseFactor(0f)
}
Resolution replays HazeBlurDefaults.style, LocalHazeBlurStyle, and the explicit Style in order.
The last write wins, and every evaluation starts fresh. Styles contain no mutable renderer or
platform state and can be shared by concurrent modifiers.
Typed custom effectsΒΆ
Custom effects use a stateless factory and one renderer per modifier node:
val factory = HazeEffectFactory<MyStyle> {
object : HazeEffectRenderer<MyStyle> {
override fun HazeEffectDrawScope.draw(style: MyStyle) {
drawInput()
// Draw the effect.
}
}
}
Modifier.hazeEffect(
factory = factory,
input = HazeInput.Content,
style = MyStyle(...),
)
The renderer can own mutable resources and releases them in dispose. Style replacement updates
the existing renderer. Factory replacement and detachment dispose it exactly once.
SamplingΒΆ
HazeSampling.DefaultandAdaptivelet built-in effects balance quality and cost automatically.HazeSampling.FullResolutionuses the full input resolution.HazeSampling.Fixed(pixelFraction)uses an explicit fraction of the full-resolution input pixels.
Start with the default. Choose FullResolution only when visual comparison shows that you need it,
or Fixed when you deliberately want a stable quality and performance trade-off.
Layer boundsΒΆ
expandLayerBounds lets an effect request a larger capture layer. Blur normally expands by its
resolved radius to avoid edge artifacts. Disable it only when the surrounding pixels must not be
captured.
Background and foreground effectsΒΆ
Source-backed effects render captured content from elsewhere in the hierarchy:
Box {
LazyColumn(
modifier = Modifier.hazeSource(hazeState),
) {
// Content
}
TopAppBar(
modifier = Modifier.hazeBlur(
input = HazeInput.Sources(hazeState),
),
)
}
Own-content effects capture and transform the modifier's content:
Box(
modifier = Modifier.hazeBlur(input = HazeInput.Content),
) {
// This content is blurred.
}
Deep UI hierarchiesΒΆ
When HazeState would otherwise pass through many composables, provide it through a composition
local:
val LocalHazeState = compositionLocalOf { HazeState() }
@Composable
fun HazeExample() {
val hazeState = rememberHazeState()
CompositionLocalProvider(LocalHazeState provides hazeState) {
Box {
Background()
Foreground()
}
}
}
@Composable
fun Foreground() {
Text(
modifier = Modifier.hazeBlur(
input = HazeInput.Sources(LocalHazeState.current),
),
)
}
Overlapping effectsΒΆ
One composable can both consume lower sources and become a source for a higher effect. Give each
source an explicit zIndex:
Box {
Background(
modifier = Modifier.hazeSource(hazeState, zIndex = 0f),
)
Card(
modifier = Modifier
.hazeSource(hazeState, zIndex = 1f)
.hazeBlur(input = HazeInput.Sources(hazeState)),
)
TopAppBar(
modifier = Modifier
.hazeSource(hazeState, zIndex = 2f)
.hazeBlur(input = HazeInput.Sources(hazeState)),
)
}
The Card consumes the Background, while the TopAppBar consumes both lower sources.
DialogsΒΆ
Mark the source before showing a dialog. Haze can then align a source and effect that live in different windows:
Box {
LazyColumn(
modifier = Modifier.hazeSource(hazeState),
) {
// Background content
}
if (showDialog) {
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
modifier = Modifier.hazeBlur(
input = HazeInput.Sources(hazeState),
),
) {
// Dialog content
}
}
}
}
Haze handles alignment between the dialog and its source automatically.
Screenshot testingΒΆ
On Android, run Robolectric screenshot tests against SDK 35 or newer. Earlier Robolectric SDK levels do not fully reproduce the blur tile modes used at effect edges:
@Config(sdk = [35])
class MyScreenshotTest {
// Tests
}
This limitation affects the test environment, not the equivalent effect on a physical device.