Skip to content

Commit

Permalink
Simplify code paths
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Jul 5, 2024
1 parent 87f615f commit 7f0bc2d
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 53 deletions.
27 changes: 14 additions & 13 deletions haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNode.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapShader
import android.graphics.BlendMode
import android.graphics.RenderEffect
import android.graphics.RenderEffect as AndroidRenderEffect
import android.graphics.Shader
import android.graphics.Shader.TileMode.REPEAT
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.collection.lruCache
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
Expand All @@ -22,22 +23,22 @@ import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import kotlin.math.roundToInt

internal actual fun HazeNode.updateRenderEffect(effect: Effect) = with(effect) {
if (Build.VERSION.SDK_INT >= 32) {
val blurRadiusPx = with(currentValueOf(LocalDensity)) { blurRadius.toPx() }
renderEffect =
RenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.CLAMP)
internal actual fun HazeNode.createRenderEffect(effect: Effect, density: Density): RenderEffect? =
with(effect) {
if (Build.VERSION.SDK_INT >= 32) {
val blurRadiusPx = with(density) { blurRadius.toPx() }
return AndroidRenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.CLAMP)
.withNoise(noiseFactor)
.asComposeRenderEffect()
}
return null
}
renderEffectDirty = false
}

internal actual fun HazeNode.usingGraphicsLayers(): Boolean = Build.VERSION.SDK_INT >= 32
internal actual fun HazeNode.useGraphicsLayers(): Boolean = Build.VERSION.SDK_INT >= 32

internal actual fun HazeNode.drawEffect(
drawScope: DrawScope,
Expand Down Expand Up @@ -78,11 +79,11 @@ private fun Color.boostAlphaForBlurRadius(blurRadius: Dp): Color {

context(CompositionLocalConsumerModifierNode)
@RequiresApi(31)
private fun RenderEffect.withNoise(noiseFactor: Float): RenderEffect = when {
private fun AndroidRenderEffect.withNoise(noiseFactor: Float): AndroidRenderEffect = when {
noiseFactor >= 0.005f -> {
val noiseShader = BitmapShader(getNoiseTexture(noiseFactor), REPEAT, REPEAT)
RenderEffect.createBlendModeEffect(
RenderEffect.createShaderEffect(noiseShader), // dst
AndroidRenderEffect.createBlendModeEffect(
AndroidRenderEffect.createShaderEffect(noiseShader), // dst
this, // src
BlendMode.DST_ATOP, // blendMode
)
Expand Down
83 changes: 49 additions & 34 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.layer.setOutline
import androidx.compose.ui.graphics.withSave
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInWindow
Expand Down Expand Up @@ -106,9 +107,11 @@ internal class HazeNode(
}

// First we need to make sure that the effects are updated (if necessary)
effects.forEach { it.update() }
for (effect in effects) {
effect.update(layoutDirection, drawContext.density)
}

if (!usingGraphicsLayers()) {
if (!useGraphicsLayers()) {
// If we're not using graphics layers, our code path is much simpler.
// We just draw the content directly to the canvas, and then draw each effect over it
drawContent()
Expand All @@ -124,8 +127,6 @@ internal class HazeNode(
return
}

val graphicsContext = currentValueOf(LocalGraphicsContext)

// First we draw the composable content into a graphics layer
contentLayer.record(size = size.roundToIntSize()) {
this@draw.drawContent()
Expand All @@ -150,7 +151,7 @@ internal class HazeNode(
for (effect in effects) {
// Now we need to draw `contentNode` into each of an 'effect' graphic layers.
// The RenderEffect applied will provide the blurring effect.
val effectLayer = graphicsContext.createGraphicsLayer()
val effectLayer = requireNotNull(effect.layer)

// We need to inflate the bounds by the blur radius, so that the effect
// has access to the pixels it needs in the clipRect
Expand All @@ -162,28 +163,16 @@ internal class HazeNode(
}
}

// Now position the effect, and apply the render effect
effectLayer.topLeft = effect.bounds.topLeft.round()
effectLayer.renderEffect = effect.renderEffect
effectLayer.colorFilter = when {
effect.tint.alpha >= 0.005f -> ColorFilter.tint(effect.tint, BlendMode.SrcOver)
else -> null
}

// We draw the 'effect' to the window canvas, drawing on top of the original content
clipShape(
shape = effect.shape,
bounds = effect.bounds,
path = { effect.getUpdatedPath(layoutDirection, drawContext.density) },
block = { drawEffect(this, effect, effectLayer) },
)

graphicsContext.releaseGraphicsLayer(effectLayer)
drawEffect(this, effect, effectLayer)
}
}

override fun onDetach() {
currentValueOf(LocalGraphicsContext).releaseGraphicsLayer(contentLayer)
val graphicsContext = currentValueOf(LocalGraphicsContext)
graphicsContext.releaseGraphicsLayer(contentLayer)
effects.asSequence()
.mapNotNull { it.layer }
.forEach { graphicsContext.releaseGraphicsLayer(it) }
}

private fun update(
Expand All @@ -201,6 +190,10 @@ internal class HazeNode(
// We re-use any current effects, otherwise we need to create a new one
currentEffects.remove(area) ?: Effect(
area = area,
layer = when {
useGraphicsLayers() -> currentValueOf(LocalGraphicsContext).createGraphicsLayer()
else -> null
},
path = pathPool.acquireOrCreate(::Path),
contentClipPath = pathPool.acquireOrCreate(::Path),
)
Expand Down Expand Up @@ -229,21 +222,44 @@ internal class HazeNode(
return invalidateCount > 0 || (effects.isEmpty() != currentEffectsIsEmpty)
}

private fun Effect.update() {
if (renderEffectDirty && usingGraphicsLayers()) updateRenderEffect(this)
private fun Effect.updateLayer(
layoutDirection: LayoutDirection,
density: Density,
) {
layer?.apply {
topLeft = bounds.topLeft.round()
colorFilter = when {
tint.alpha >= 0.005f -> ColorFilter.tint(tint, BlendMode.SrcOver)
else -> null
}
clip = true
setOutline(shape.createOutline(bounds.size, layoutDirection, density))
renderEffect = createRenderEffect(this@updateLayer, density)
}
layerDirty = false
}

private fun Effect.update(layoutDirection: LayoutDirection, density: Density) {
if (layerDirty) updateLayer(layoutDirection, density)

// We don't update the path here as we may not need it. Let draw request it
// via getUpdatedPath if it needs it
}

private fun Effect.recycle() {
pathPool.release(path)
pathPool.release(contentClipPath)
layer?.let { currentValueOf(LocalGraphicsContext).releaseGraphicsLayer(it) }
}
}

internal expect fun HazeNode.updateRenderEffect(effect: Effect)
internal expect fun HazeNode.usingGraphicsLayers(): Boolean
internal expect fun HazeNode.drawEffect(drawScope: DrawScope, effect: Effect, graphicsLayer: GraphicsLayer? = null)
internal expect fun HazeNode.createRenderEffect(effect: Effect, density: Density): RenderEffect?
internal expect fun HazeNode.useGraphicsLayers(): Boolean
internal expect fun HazeNode.drawEffect(
drawScope: DrawScope,
effect: Effect,
graphicsLayer: GraphicsLayer? = null,
)

internal fun Effect.updateParameters(
bounds: Rect,
Expand All @@ -257,7 +273,7 @@ internal fun Effect.updateParameters(
this.noiseFactor = noiseFactor
this.shape = shape
this.tint = tint
return renderEffectDirty || layerDirty || pathsDirty
return layerDirty || layerDirty || pathsDirty
}

private fun Effect.getUpdatedPath(layoutDirection: LayoutDirection, density: Density): Path {
Expand Down Expand Up @@ -298,8 +314,7 @@ internal class Effect(
val area: HazeArea,
val path: Path,
val contentClipPath: Path,
var renderEffect: RenderEffect? = null,
var renderEffectDirty: Boolean = true,
var layer: GraphicsLayer? = null,
var pathsDirty: Boolean = true,
var layerDirty: Boolean = true,
) {
Expand All @@ -326,23 +341,23 @@ internal class Effect(
var blurRadius: Dp = Dp.Unspecified
set(value) {
if (value != field) {
renderEffectDirty = true
layerDirty = true
field = value
}
}

var noiseFactor: Float = 0f
set(value) {
if (value != field) {
renderEffectDirty = true
layerDirty = true
field = value
}
}

var tint: Color = Color.Unspecified
set(value) {
if (value != field) {
renderEffectDirty = true
layerDirty = true
field = value
}
}
Expand Down
12 changes: 6 additions & 6 deletions haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeNode.skiko.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ package dev.chrisbanes.haze
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.BlurEffect
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.FilterTileMode
import org.jetbrains.skia.IRect
import org.jetbrains.skia.ImageFilter
Expand All @@ -25,15 +25,15 @@ internal actual fun HazeNode.drawEffect(
drawLayer(requireNotNull(graphicsLayer))
}

internal actual fun HazeNode.usingGraphicsLayers(): Boolean = true
internal actual fun HazeNode.useGraphicsLayers(): Boolean = true

internal actual fun HazeNode.updateRenderEffect(effect: Effect) {
internal actual fun HazeNode.createRenderEffect(effect: Effect, density: Density): RenderEffect? {
val compositeShaderBuilder = RuntimeShaderBuilder(RUNTIME_SHADER).apply {
uniform("noiseFactor", effect.noiseFactor)
child("noise", NOISE_SHADER)
}
// For CLAMP to work, we need to provide the crop rect
val blurRadiusPx = with(currentValueOf(LocalDensity)) { effect.blurRadius.toPx() }
val blurRadiusPx = with(density) { effect.blurRadius.toPx() }
val blurFilter = createBlurImageFilter(blurRadiusPx, effect.bounds.size.toRect())

val filter = ImageFilter.makeRuntimeShader(
Expand All @@ -42,7 +42,7 @@ internal actual fun HazeNode.updateRenderEffect(effect: Effect) {
inputs = arrayOf(null, blurFilter),
)

effect.renderEffect = filter.asComposeRenderEffect()
return filter.asComposeRenderEffect()
}

private fun createBlurImageFilter(blurRadiusPx: Float, cropRect: Rect? = null): ImageFilter {
Expand Down

0 comments on commit 7f0bc2d

Please sign in to comment.