Skip to content

Commit

Permalink
Remove shape clipping (#287)
Browse files Browse the repository at this point in the history
It's no longer necessary for Haze to draw and clip the effect, as we're
drawing in the child node. This means that developers can either use
`Modifier.clip` or shape theming to clip the content.
  • Loading branch information
chrisbanes authored Aug 1, 2024
1 parent a2fc88f commit 0113290
Show file tree
Hide file tree
Showing 15 changed files with 90 additions and 244 deletions.
23 changes: 0 additions & 23 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,29 +43,6 @@ A tint effect is applied, primarily to maintain contrast and legibility. By defa

Some visual noise is applied, to provide some tactility. This is completely optional, and defaults to a value of `0.15f` (15% strength). You can disable this by providing `0f`.

## Shapes

Haze has some support for blurring of a provided `Shape`, passed into [Modifier.hazeChild](../api/haze/dev.chrisbanes.haze/haze-child.html).

The platforms have varying support:

- Android: full support, through `clipPath`
- iOS and Desktop: limited support. Only `RoundedCornerShape`s currently works.

``` kotlin hl_lines="8"
Box {
// rest of sample from above

LargeTopAppBar(
modifier = Modifier
.hazeChild(
...
shape = RoundedCornerShape(16.dp),
),
)
}
```

## Scaffold

Make the content behind app bars is a common use case, so how can we use Haze with `Scaffold`? It's pretty much the same as above:
Expand Down
11 changes: 7 additions & 4 deletions haze/api/api.txt
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
// Signature format: 4.0
package dev.chrisbanes.haze {

public final class CanvasKt {
method public static inline void translate(androidx.compose.ui.graphics.drawscope.DrawScope, long offset, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
}

@androidx.compose.runtime.Stable public final class HazeArea {
ctor public HazeArea(optional long size, optional long positionOnScreen, optional androidx.compose.ui.graphics.Shape shape, optional dev.chrisbanes.haze.HazeStyle style, optional androidx.compose.ui.graphics.Brush? mask);
ctor public HazeArea(optional long size, optional long positionOnScreen, optional dev.chrisbanes.haze.HazeStyle style, optional androidx.compose.ui.graphics.Brush? mask);
method public androidx.compose.ui.graphics.Brush? getMask();
method public long getPositionOnScreen();
method public androidx.compose.ui.graphics.Shape getShape();
method public long getSize();
method public dev.chrisbanes.haze.HazeStyle getStyle();
method public boolean isValid();
property public final boolean isValid;
property public final androidx.compose.ui.graphics.Brush? mask;
property public final long positionOnScreen;
property public final androidx.compose.ui.graphics.Shape shape;
property public final long size;
property public final dev.chrisbanes.haze.HazeStyle style;
}

public final class HazeChildKt {
method public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional androidx.compose.ui.graphics.Shape shape, optional dev.chrisbanes.haze.HazeStyle style, optional androidx.compose.ui.graphics.Brush? mask);
method @Deprecated public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional androidx.compose.ui.graphics.Shape shape, optional dev.chrisbanes.haze.HazeStyle style, optional androidx.compose.ui.graphics.Brush? mask);
method public static androidx.compose.ui.Modifier hazeChild(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state, optional dev.chrisbanes.haze.HazeStyle style, optional androidx.compose.ui.graphics.Brush? mask);
}

public final class HazeDefaults {
Expand Down
39 changes: 0 additions & 39 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Canvas.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,11 @@
package dev.chrisbanes.haze

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.ClipOp
import androidx.compose.ui.graphics.GraphicsContext
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer

internal fun DrawScope.clipShape(
shape: Shape,
size: Size,
offset: Offset = Offset.Unspecified,
clipOp: ClipOp = ClipOp.Intersect,
path: () -> Path,
block: DrawScope.() -> Unit,
) {
if (shape == RectangleShape) {
val offsetOrZero = offset.takeOrElse { Offset.Zero }
clipRect(
left = offsetOrZero.x,
top = offsetOrZero.y,
right = size.width + offsetOrZero.x,
bottom = size.height + offsetOrZero.y,
clipOp = clipOp,
block = block,
)
} else {
if (offset.isUnspecified || offset == Offset.Zero) {
clipPath(path(), clipOp, block)
} else {
pathPool.usePath { tmpPath ->
tmpPath.addPath(path(), offset)
clipPath(tmpPath, clipOp, block)
}
}
}
}

internal inline fun GraphicsContext.useGraphicsLayer(block: (GraphicsLayer) -> Unit) {
val layer = createGraphicsLayer()
try {
Expand Down
6 changes: 0 additions & 6 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.takeOrElse
Expand Down Expand Up @@ -44,7 +42,6 @@ class HazeState {
class HazeArea(
size: Size = Size.Unspecified,
positionOnScreen: Offset = Offset.Unspecified,
shape: Shape = RectangleShape,
style: HazeStyle = HazeStyle.Unspecified,
mask: Brush? = null,
) {
Expand All @@ -54,9 +51,6 @@ class HazeArea(
var positionOnScreen: Offset by mutableStateOf(positionOnScreen)
internal set

var shape: Shape by mutableStateOf(shape)
internal set

var style: HazeStyle by mutableStateOf(style)
internal set

Expand Down
37 changes: 28 additions & 9 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package dev.chrisbanes.haze

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
Expand All @@ -26,54 +27,72 @@ import kotlinx.coroutines.launch
* since we need to use path clipping.
* @param style The [HazeStyle] to use on this content. Any specified values in the given
* style will override that value from the default style, provided to [haze].
* @param mask An optional mask which allows effects such as fading via [Brush.verticalGradient] or similar.
*/
@Deprecated(
message = "Shape clipping is no longer necessary with Haze. You can use `Modifier.clip` or similar.",
replaceWith = ReplaceWith("clip(shape).hazeChild(state, style, mask)"),
)
fun Modifier.hazeChild(
state: HazeState,
shape: Shape = RectangleShape,
style: HazeStyle = HazeStyle.Unspecified,
mask: Brush? = null,
): Modifier = this then HazeChildNodeElement(state, shape, style, mask)
): Modifier = clip(shape).hazeChild(state, style, mask)

/**
* Mark this composable as being a Haze child composable.
*
* This will update the given [HazeState] whenever the layout is placed, enabling any layouts using
* [Modifier.haze] to blur any content behind the host composable.
*
* @param shape The shape of the content. This will affect the the bounds and outline of
* the content. Please be aware that using non-rectangular shapes has an effect on performance,
* since we need to use path clipping.
* @param style The [HazeStyle] to use on this content. Any specified values in the given
* style will override that value from the default style, provided to [haze].
* @param mask An optional mask which allows effects such as fading via [Brush.verticalGradient] or similar.
*/
fun Modifier.hazeChild(
state: HazeState,
style: HazeStyle = HazeStyle.Unspecified,
mask: Brush? = null,
): Modifier = this then HazeChildNodeElement(state, style, mask)

private data class HazeChildNodeElement(
val state: HazeState,
val shape: Shape,
val style: HazeStyle,
val mask: Brush?,
) : ModifierNodeElement<HazeChildNode>() {
override fun create(): HazeChildNode = HazeChildNode(state, shape, style, mask)
override fun create(): HazeChildNode = HazeChildNode(state, style, mask)

override fun update(node: HazeChildNode) {
node.state = state
node.shape = shape
node.style = style
node.mask = mask
node.update()
}

override fun InspectorInfo.inspectableProperties() {
name = "HazeChild"
properties["shape"] = shape
properties["style"] = style
properties["mask"] = mask
}
}

private class HazeChildNode(
override var state: HazeState,
var shape: Shape,
var style: HazeStyle,
var mask: Brush?,
) : HazeEffectNode() {

private val area: HazeArea by lazy {
HazeArea(shape = shape, style = style, mask = mask)
HazeArea(style = style, mask = mask)
}

private var drawWithoutContentLayerCount = 0

override fun update() {
// Propagate any changes to the HazeArea
area.shape = shape
area.style = style
area.mask = mask

Expand Down
77 changes: 15 additions & 62 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeEffect.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.layer.GraphicsLayer
Expand All @@ -35,7 +33,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalGraphicsContext
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.unit.takeOrElse
Expand Down Expand Up @@ -110,13 +107,11 @@ internal abstract class HazeEffectNode :
effect.noiseFactor = resolvedStyle.noiseFactor
effect.tint = resolvedStyle.tint
effect.backgroundColor = resolvedStyle.backgroundColor
effect.shape = effect.area.shape
effect.mask = effect.area.mask
}
.forEach(_effects::add)

// Any effects left in currentEffects are no longer used, so recycle them
currentEffects.forEach { (_, effect) -> effect.recycle() }
// Any effects left in currentEffects are no longer used
currentEffects.clear()

val needInvalidate = effects.any { it.needInvalidation }
Expand Down Expand Up @@ -160,39 +155,29 @@ internal abstract class HazeEffectNode :

// Draw the effect to our canvas, translated to the correct position
translate(offset = effect.positionOnScreen - positionOnScreen) {
clipShape(
shape = effect.shape,
size = effect.size,
path = { effect.getUpdatedPath(layoutDirection, drawContext.density) },
block = {
// Since we included a border around the content, we need to translate so that
// we don't see it (but it still affects the RenderEffect)
translate(-inflatedOffset.x, -inflatedOffset.y) {
drawEffect(this, effect, layer)
}
},
)
// Since we included a border around the content, we need to translate so that
// we don't see it (but it still affects the RenderEffect)
translate(-inflatedOffset.x, -inflatedOffset.y) {
drawEffect(this, effect, layer)
}
}
}
}
}

protected fun DrawScope.drawEffectsWithScrim() {
for (effect in effects) {
clipShape(
shape = effect.shape,
size = effect.size,
offset = effect.positionOnScreen - positionOnScreen,
path = { effect.getUpdatedPath(layoutDirection, drawContext.density) },
val offset = (effect.positionOnScreen - positionOnScreen).takeOrElse { Offset.Zero }
clipRect(
left = offset.x,
top = offset.y,
right = size.width + offset.x,
bottom = size.height + offset.y,
block = { drawEffect(this, effect) },
)
}
}

override fun onDetach() {
_effects.onEach(HazeEffect::recycle).clear()
}

protected open fun calculateHazeAreas(): Sequence<HazeArea> = emptySequence()

protected fun HazeEffect.onPreDraw(density: Density) {
Expand All @@ -217,9 +202,6 @@ internal expect fun HazeEffectNode.createRenderEffect(
): RenderEffect?

internal class HazeEffect(val area: HazeArea) {
val path by lazy { pathPool.acquireOrCreate(::Path) }
var pathDirty: Boolean = true

var renderEffect: RenderEffect? = null
var renderEffectDirty: Boolean = true

Expand All @@ -228,7 +210,6 @@ internal class HazeEffect(val area: HazeArea) {
if (value != field) {
// We use the size for crop rects/brush sizing
renderEffectDirty = true
pathDirty = true
field = value
}
}
Expand Down Expand Up @@ -282,37 +263,9 @@ internal class HazeEffect(val area: HazeArea) {
field = value
}
}

var shape: Shape = RectangleShape
set(value) {
if (value != field) {
pathDirty = true
}
field = value
}

fun recycle() {
pathPool.release(path)
}
}

internal val HazeEffect.blurRadiusOrZero: Dp get() = blurRadius.takeOrElse { 0.dp }

internal val HazeEffect.needInvalidation: Boolean
get() = renderEffectDirty || pathDirty

internal fun HazeEffect.getUpdatedPath(
layoutDirection: LayoutDirection,
density: Density,
): Path {
if (pathDirty) updatePath(layoutDirection, density)
return path
}

private fun HazeEffect.updatePath(layoutDirection: LayoutDirection, density: Density) {
path.rewind()
if (!size.isEmpty()) {
path.addOutline(shape.createOutline(size, layoutDirection, density))
}
pathDirty = false
}
get() = renderEffectDirty
Loading

0 comments on commit 0113290

Please sign in to comment.