Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow override tint on hazeChild #81

Merged
merged 1 commit into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions haze-jetpack-compose/src/main/kotlin/dev/chrisbanes/haze/Haze.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class HazeArea(
size: Size = Size.Unspecified,
positionInRoot: Offset = Offset.Unspecified,
shape: Shape = RectangleShape,
tint: Color = Color.Unspecified,
) {
var size: Size by mutableStateOf(size)
internal set
Expand All @@ -85,6 +86,9 @@ class HazeArea(
var shape: Shape by mutableStateOf(shape)
internal set

var tint: Color by mutableStateOf(tint)
internal set

val isValid: Boolean
get() = size.isSpecified && positionInRoot.isSpecified && !size.isEmpty()
}
Expand All @@ -96,6 +100,19 @@ internal fun HazeArea.boundsInLocal(hazePositionInRoot: Offset): Rect? {
return size.toRect().translate(positionInRoot - hazePositionInRoot)
}

internal fun HazeArea.updatePath(
path: Path,
area: Rect,
layoutDirection: LayoutDirection,
density: Density,
) {
path.reset()
path.addOutline(
outline = shape.createOutline(size, layoutDirection, density),
offset = area.topLeft,
)
}

/**
* Draw content within the provided [HazeState.areas] blurred in a 'glassmorphism' style.
*
Expand All @@ -105,8 +122,8 @@ internal fun HazeArea.boundsInLocal(hazePositionInRoot: Offset): Rect? {
*
* @param backgroundColor Background color of the content. Typically you would provide
* `MaterialTheme.colorScheme.surface` or similar.
* @param tint Color to tint the blurred content. Should be translucent, otherwise you will not see
* the blurred content.
* @param tint Default color to tint the blurred content. Should be translucent, otherwise you will not see
* the blurred content. Can be overridden by the `tint` parameter on [hazeChild].
* @param blurRadius Radius of the blur.
* @param noiseFactor Amount of noise applied to the content, in the range `0f` to `1f`.
*/
Expand Down Expand Up @@ -168,7 +185,7 @@ internal data class HazeNodeElement(
override fun update(node: HazeNode) {
node.state = state
node.backgroundColor = backgroundColor
node.tint = tint
node.defaultTint = tint
node.blurRadius = blurRadius
node.noiseFactor = noiseFactor
node.onUpdate()
Expand All @@ -186,7 +203,7 @@ internal data class HazeNodeElement(
internal abstract class HazeNode(
var state: HazeState,
var backgroundColor: Color,
var tint: Color,
var defaultTint: Color,
var blurRadius: Dp,
var noiseFactor: Float,
) : Modifier.Node() {
Expand Down
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.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.LayoutCoordinates
Expand All @@ -18,37 +19,48 @@ import androidx.compose.ui.unit.toSize
*
* 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 blurred content.
* @param tint Color to tint the blurred content. Should be translucent, otherwise you will not see
* the blurred content. If the provided color is [Color.Unspecified] then the default tint
* provided to [haze] will be used.
*/
fun Modifier.hazeChild(
state: HazeState,
shape: Shape = RectangleShape,
): Modifier = this then HazeChildNodeElement(state, shape)
tint: Color = Color.Unspecified,
): Modifier = this then HazeChildNodeElement(state, shape, tint)

private data class HazeChildNodeElement(
val state: HazeState,
val shape: Shape,
val tint: Color,
) : ModifierNodeElement<HazeChildNode>() {
override fun create(): HazeChildNode = HazeChildNode(state, shape)
override fun create(): HazeChildNode = HazeChildNode(state, shape, tint)

override fun update(node: HazeChildNode) {
node.state = state
node.shape = shape
node.tint = tint
node.onUpdate()
}

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

private data class HazeChildNode(
var state: HazeState,
var shape: Shape,
var tint: Color,
) : Modifier.Node(), LayoutAwareModifierNode {

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

private var attachedState: HazeState? = null
Expand All @@ -60,6 +72,7 @@ private data class HazeChildNode(
fun onUpdate() {
// Propagate any shape changes to the HazeArea
area.shape = shape
area.tint = tint

if (state != attachedState) {
// The provided HazeState has changed, so we need to detach from the old one,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
Expand Down Expand Up @@ -40,17 +41,17 @@ internal class HazeNodeBase(
LayoutAwareModifierNode,
CompositionLocalConsumerModifierNode {

private val path = Path()
private var pathDirty = false
private var pathsDirty = true
private var paths: List<PathHolder> = emptyList()

private var positionInRoot by observable(Offset.Unspecified) { _, oldValue, newValue ->
if (oldValue != newValue) {
invalidatePath()
invalidatePaths()
}
}
private var size by observable(Size.Unspecified) { _, oldValue, newValue ->
if (oldValue != newValue) {
invalidatePath()
invalidatePaths()
}
}

Expand All @@ -59,11 +60,11 @@ internal class HazeNodeBase(
}

override fun onObservedReadsChanged() {
invalidatePath()
invalidatePaths()
}

private fun invalidatePath() {
pathDirty = true
private fun invalidatePaths() {
pathsDirty = true
invalidateDraw()
}

Expand All @@ -77,22 +78,47 @@ internal class HazeNodeBase(
}

override fun ContentDrawScope.draw() {
if (pathDirty) {
observeReads { updatePath(layoutDirection, currentValueOf(LocalDensity)) }
}

drawContent()

drawPath(
path = path,
// We need to boost the alpha as we don't have a blur effect
color = tint.copy(alpha = (tint.alpha * 1.35f).coerceAtMost(1f)),
)
}
if (pathsDirty) {
observeReads {
paths = buildPaths(layoutDirection, currentValueOf(LocalDensity))
pathsDirty = false
}
}

private fun updatePath(layoutDirection: LayoutDirection, density: Density) {
path.reset()
state.addAreasToPath(path, positionInRoot, layoutDirection, density)
pathDirty = false
for (pathHolder in paths) {
drawPath(
path = pathHolder.path,
color = pathHolder.tint,
)
}
}

private fun buildPaths(
layoutDirection: LayoutDirection,
density: Density,
): List<PathHolder> = state.areas.asSequence()
.filter { it.isValid }
.mapNotNull { area ->
val bounds = area.boundsInLocal(positionInRoot) ?: return@mapNotNull null

// TODO: Should try and re-use this
val path = Path()
area.updatePath(path, bounds, layoutDirection, density)

PathHolder(
path = path,
tint = when {
area.tint.isSpecified -> area.tint
// We need to boost the alpha as we don't have a blur effect
else -> defaultTint.copy(alpha = (defaultTint.alpha * 1.35f).coerceAtMost(1f))
},
)
}.toList()

private class PathHolder(
val tint: Color,
val path: Path = Path(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.LayoutCoordinates
Expand Down Expand Up @@ -58,7 +58,7 @@ internal class HazeNodeRenderEffect(
) : HazeNode(
state = state,
backgroundColor = backgroundColor,
tint = tint,
defaultTint = tint,
blurRadius = blurRadius,
noiseFactor = noiseFactor,
),
Expand Down Expand Up @@ -145,6 +145,7 @@ internal class HazeNodeRenderEffect(
if (effectsDirty) {
observeReads {
effects = buildEffects(layoutDirection, currentValueOf(LocalDensity))
effectsDirty = false
}
}
// Now we need to draw `contentNode` into each of our 'effect' RenderNodes, allowing
Expand Down Expand Up @@ -180,7 +181,7 @@ internal class HazeNodeRenderEffect(

// This is our RenderEffect. It first applies a blur effect, and then a color filter effect
// to allow content to be visible on top
val effect = RenderEffect.createBlurEffect(
val baseEffect = RenderEffect.createBlurEffect(
blurRadiusPx,
blurRadiusPx,
Shader.TileMode.DECAL,
Expand All @@ -192,16 +193,6 @@ internal class HazeNodeRenderEffect(
it,
BlendMode.HARD_LIGHT,
)
}.let {
if (tint.alpha >= 0.005f) {
// If we have an tint with a non-zero alpha value, wrap the effect with a color filter
RenderEffect.createColorFilterEffect(
BlendModeColorFilter(tint.toArgb(), BlendMode.SRC_OVER),
it,
)
} else {
it
}
}

// We create a RenderNode for each of the areas we need to apply our effect to
Expand All @@ -214,20 +205,24 @@ internal class HazeNodeRenderEffect(
val expandedRect = bounds.inflate(blurRadiusPx)

val node = RenderNode("blur").apply {
setRenderEffect(effect)
setRenderEffect(
baseEffect
.applyTint(if (area.tint.isSpecified) area.tint else defaultTint),
)
setPosition(0, 0, expandedRect.width.toInt(), expandedRect.height.toInt())
translationX = expandedRect.left
translationY = expandedRect.top
}

// TODO: Should try and re-use this
val path = Path()
area.updatePath(path, bounds, layoutDirection, density)

EffectHolder(
path = path,
renderNode = node,
renderNodeDrawArea = expandedRect,
area = bounds,
shape = area.shape,
).apply {
updatePath(layoutDirection, density)
}
)
}.toList()
}

Expand All @@ -245,21 +240,11 @@ internal class HazeNodeRenderEffect(
R.drawable.haze_noise,
).withAlpha(noiseFactor)
}
}

private class EffectHolder(
val renderNode: RenderNode,
val renderNodeDrawArea: Rect,
val area: Rect,
val shape: Shape,
val path: Path = Path(),
)

private fun EffectHolder.updatePath(layoutDirection: LayoutDirection, density: Density) {
path.reset()
path.addOutline(
outline = shape.createOutline(area.size, layoutDirection, density),
offset = area.topLeft,
private class EffectHolder(
val renderNode: RenderNode,
val renderNodeDrawArea: Rect,
val path: Path,
)
}

Expand All @@ -280,3 +265,15 @@ private fun Bitmap.withAlpha(alpha: Float): Bitmap {
}
}
}

@RequiresApi(31)
private fun RenderEffect.applyTint(tint: Color): RenderEffect = when {
tint.alpha >= 0.005f -> {
// If we have an tint with a non-zero alpha value, wrap the effect with a color filter
RenderEffect.createColorFilterEffect(
BlendModeColorFilter(tint.toArgb(), BlendMode.SRC_OVER),
this,
)
}
else -> this
}
Loading
Loading