diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7fec109d..8cfcbd57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ androidx-benchmark = "1.3.3" androidx-media3 = "1.4.1" androidx-test-ext-junit = "1.2.1" assertk = "0.28.1" +cache4k = "0.13.0" coil = "3.0.0" compose-multiplatform = "1.7.0" kotlinx-coroutines-swing = "1.9.0" @@ -35,7 +36,6 @@ mavenpublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } [libraries] androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" } androidx-core = "androidx.core:core-ktx:1.13.1" -androidx-collection = "androidx.collection:collection:1.4.5" androidx-activity-compose = "androidx.activity:activity-compose:1.9.3" androidx-compose-ui-test-manifest = "androidx.compose.ui:ui-test-manifest:1.7.5" androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } @@ -49,6 +49,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } +cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } diff --git a/haze/build.gradle.kts b/haze/build.gradle.kts index 85bd2462..b82b0d8d 100644 --- a/haze/build.gradle.kts +++ b/haze/build.gradle.kts @@ -39,6 +39,7 @@ kotlin { dependencies { api(compose.ui) implementation(compose.foundation) + implementation(libs.cache4k) } } @@ -48,8 +49,6 @@ kotlin { // Can remove this once CMP goes stable api(libs.androidx.compose.ui) implementation(libs.androidx.compose.foundation) - - implementation(libs.androidx.collection) implementation(libs.androidx.core) } } diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeChildNode.android.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeChildNode.android.kt index 606b0d52..355e0057 100644 --- a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeChildNode.android.kt +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeChildNode.android.kt @@ -27,7 +27,7 @@ internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect( ) { if (Build.VERSION.SDK_INT >= 33) { with(drawScope) { - contentLayer.renderEffect = createRenderEffect( + contentLayer.renderEffect = getOrCreateRenderEffect( blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(), noiseFactor = resolveNoiseFactor(), tints = resolveTints(), @@ -44,7 +44,7 @@ internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect( } } else if (progressive.preferPerformance) { with(drawScope) { - contentLayer.renderEffect = createRenderEffect( + contentLayer.renderEffect = getOrCreateRenderEffect( blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(), noiseFactor = resolveNoiseFactor(), tints = resolveTints(), @@ -116,7 +116,7 @@ private fun HazeChildNode.drawLinearGradientProgressiveEffectUsingLayers( val min = min(progressive.startIntensity, progressive.endIntensity) val max = max(progressive.startIntensity, progressive.endIntensity) - layer.renderEffect = createRenderEffect( + layer.renderEffect = getOrCreateRenderEffect( blurRadiusPx = intensity * blurRadiusPx, noiseFactor = noiseFactor, tints = tints, diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt index 74feccb4..8ef827d6 100644 --- a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/RenderEffect.android.kt @@ -3,6 +3,7 @@ package dev.chrisbanes.haze +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.BitmapShader @@ -15,7 +16,6 @@ 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.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size @@ -30,54 +30,39 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.platform.LocalContext +import io.github.reactivecircus.cache4k.Cache import kotlin.concurrent.getOrSet import kotlin.math.roundToInt -internal actual fun HazeChildNode.createRenderEffect( - blurRadiusPx: Float, - noiseFactor: Float, - tints: List, - tintAlphaModulate: Float, - contentSize: Size, - contentOffset: Offset, - layerSize: Size, - mask: Brush?, - progressive: Brush?, -): RenderEffect? { +internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect? { if (Build.VERSION.SDK_INT < 31) return null - log(HazeChildNode.TAG) { - "createRenderEffect. " + - "blurRadiusPx=$blurRadiusPx, " + - "noiseFactor=$noiseFactor, " + - "tints=$tints, " + - "contentSize=$contentSize, " + - "contentOffset=$contentOffset, " + - "layerSize=$layerSize" - } + require(params.blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" } - require(blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" } - - val progressiveShader = progressive?.toShader(contentSize) + val progressiveShader = params.progressive?.toShader(params.contentSize) val blur = when { - blurRadiusPx >= 0.005f && Build.VERSION.SDK_INT >= 33 && progressiveShader != null -> { + params.blurRadiusPx >= 0.005f && Build.VERSION.SDK_INT >= 33 && progressiveShader != null -> { // If we've been provided with a progressive/gradient blur shader, we need to use // our custom blur via a runtime shader createBlurImageFilterWithMask( - blurRadiusPx = blurRadiusPx, - bounds = Rect(contentOffset, contentSize), + blurRadiusPx = params.blurRadiusPx, + bounds = Rect(params.contentOffset, params.contentSize), mask = progressiveShader, ) } - blurRadiusPx >= 0.005f -> { + params.blurRadiusPx >= 0.005f -> { try { - AndroidRenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.CLAMP) + AndroidRenderEffect.createBlurEffect( + params.blurRadiusPx, + params.blurRadiusPx, + Shader.TileMode.CLAMP, + ) } catch (e: IllegalArgumentException) { throw IllegalArgumentException( "Error whilst calling RenderEffect.createBlurEffect. " + - "This is likely because this device does not support a blur radius of $blurRadiusPx px", + "This is likely because this device does not support a blur radius of ${params.blurRadiusPx} px", e, ) } @@ -89,9 +74,9 @@ internal actual fun HazeChildNode.createRenderEffect( } return blur - .withNoise(noiseFactor) - .withTints(tints, tintAlphaModulate, progressiveShader, contentOffset) - .withMask(mask, contentSize, contentOffset) + .withNoise(currentValueOf(LocalContext), params.noiseFactor) + .withTints(params.tints, params.tintAlphaModulate, progressiveShader, params.contentOffset) + .withMask(params.mask, params.contentSize, params.contentOffset) .asComposeRenderEffect() } @@ -99,28 +84,32 @@ internal actual fun DrawScope.useGraphicLayers(): Boolean { return Build.VERSION.SDK_INT >= 32 && drawContext.canvas.nativeCanvas.isHardwareAccelerated } -private val noiseTextureCache = lruCache(3) +private val noiseTextureCache by lazy { + Cache.Builder() + .maximumCacheSize(3) + .build() +} -context(CompositionLocalConsumerModifierNode) -private fun getNoiseTexture(noiseFactor: Float): Bitmap { +private fun Context.getNoiseTexture(noiseFactor: Float): Bitmap { val noiseAlphaInt = (noiseFactor * 255).roundToInt().coerceIn(0, 255) - val cached = noiseTextureCache[noiseAlphaInt] + val cached = noiseTextureCache.get(noiseAlphaInt) if (cached != null && !cached.isRecycled) { return cached } // We draw the noise with the given opacity - val resources = currentValueOf(LocalContext).resources return BitmapFactory.decodeResource(resources, R.drawable.haze_noise) .withAlpha(noiseAlphaInt) .also { noiseTextureCache.put(noiseAlphaInt, it) } } -context(CompositionLocalConsumerModifierNode) @RequiresApi(31) -private fun AndroidRenderEffect.withNoise(noiseFactor: Float): AndroidRenderEffect = when { +private fun AndroidRenderEffect.withNoise( + context: Context, + noiseFactor: Float, +): AndroidRenderEffect = when { noiseFactor >= 0.005f -> { - val noiseShader = BitmapShader(getNoiseTexture(noiseFactor), REPEAT, REPEAT) + val noiseShader = BitmapShader(context.getNoiseTexture(noiseFactor), REPEAT, REPEAT) AndroidRenderEffect.createBlendModeEffect( AndroidRenderEffect.createShaderEffect(noiseShader), // dst this, // src diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt index 0071adbd..2fba18b7 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntSize import androidx.compose.ui.unit.takeOrElse import androidx.compose.ui.unit.toSize +import io.github.reactivecircus.cache4k.Cache /** * The [Modifier.Node] implementation used by [Modifier.hazeChild]. @@ -358,7 +359,7 @@ class HazeChildNode( private fun DrawScope.updateRenderEffectIfDirty() { if (renderEffectDirty) { - renderEffect = createRenderEffect( + renderEffect = getOrCreateRenderEffect( blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(), noiseFactor = resolveNoiseFactor(), tints = resolveTints(), @@ -495,7 +496,25 @@ sealed interface HazeProgressive { } } -internal expect fun HazeChildNode.createRenderEffect( +private val renderEffectCache by lazy { + Cache.Builder() + .maximumCacheSize(10) + .build() +} + +internal data class RenderEffectParams( + val blurRadiusPx: Float, + val noiseFactor: Float, + val tints: List = emptyList(), + val tintAlphaModulate: Float = 1f, + val contentSize: Size, + val contentOffset: Offset, + val layerSize: Size, + val mask: Brush? = null, + val progressive: Brush? = null, +) + +internal fun CompositionLocalConsumerModifierNode.getOrCreateRenderEffect( blurRadiusPx: Float, noiseFactor: Float, tints: List = emptyList(), @@ -505,7 +524,34 @@ internal expect fun HazeChildNode.createRenderEffect( layerSize: Size, mask: Brush? = null, progressive: Brush? = null, -): RenderEffect? +): RenderEffect? = getOrCreateRenderEffect( + RenderEffectParams( + blurRadiusPx = blurRadiusPx, + noiseFactor = noiseFactor, + tints = tints, + tintAlphaModulate = tintAlphaModulate, + contentSize = contentSize, + contentOffset = contentOffset, + layerSize = layerSize, + mask = mask, + progressive = progressive, + ), +) + +internal fun CompositionLocalConsumerModifierNode.getOrCreateRenderEffect(params: RenderEffectParams): RenderEffect? { + log(HazeChildNode.TAG) { "getOrCreateRenderEffect: $params" } + val cached = renderEffectCache.get(params) + if (cached != null) { + log(HazeChildNode.TAG) { "getOrCreateRenderEffect. Returning cached: $params" } + return cached + } + + log(HazeChildNode.TAG) { "getOrCreateRenderEffect. Creating: $params" } + return createRenderEffect(params) + ?.also { renderEffectCache.put(params, it) } +} + +internal expect fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect? internal expect fun HazeChildNode.drawLinearGradientProgressiveEffect( drawScope: DrawScope, diff --git a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeChildNode.skiko.kt b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeChildNode.skiko.kt index 3c37f314..2ca9a6f6 100644 --- a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeChildNode.skiko.kt +++ b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeChildNode.skiko.kt @@ -14,7 +14,7 @@ internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect( progressive: HazeProgressive.LinearGradient, contentLayer: GraphicsLayer, ) = with(drawScope) { - contentLayer.renderEffect = createRenderEffect( + contentLayer.renderEffect = getOrCreateRenderEffect( blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(), noiseFactor = resolveNoiseFactor(), tints = resolveTints(), diff --git a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt index 697c672a..4654ed1b 100644 --- a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt +++ b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.asComposeRenderEffect import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import org.jetbrains.skia.BlendMode import org.jetbrains.skia.ColorFilter import org.jetbrains.skia.FilterTileMode @@ -25,45 +26,25 @@ import org.jetbrains.skia.Shader internal actual fun DrawScope.useGraphicLayers(): Boolean = true -internal actual fun HazeChildNode.createRenderEffect( - blurRadiusPx: Float, - noiseFactor: Float, - tints: List, - tintAlphaModulate: Float, - contentSize: Size, - contentOffset: Offset, - layerSize: Size, - mask: Brush?, - progressive: Brush?, -): RenderEffect? { - log(HazeChildNode.TAG) { - "createRenderEffect. " + - "blurRadiusPx=$blurRadiusPx, " + - "noiseFactor=$noiseFactor, " + - "tints=$tints, " + - "contentSize=$contentSize, " + - "contentOffset=$contentOffset, " + - "layerSize=$layerSize" - } - - require(blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" } +internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect? { + require(params.blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" } val compositeShaderBuilder = RuntimeShaderBuilder(RUNTIME_SHADER).apply { - uniform("noiseFactor", noiseFactor.coerceIn(0f, 1f)) + uniform("noiseFactor", params.noiseFactor.coerceIn(0f, 1f)) child("noise", NOISE_SHADER) } - val progressiveShader = progressive?.toShader(contentSize) + val progressiveShader = params.progressive?.toShader(params.contentSize) val blur = if (progressiveShader != null) { // If we've been provided with a progressive/gradient blur shader, we need to use // our custom blur via a runtime shader createBlurImageFilterWithMask( - blurRadiusPx = blurRadiusPx, - bounds = Rect(contentOffset, contentSize), + blurRadiusPx = params.blurRadiusPx, + bounds = Rect(params.contentOffset, params.contentSize), mask = progressiveShader, ) } else { - createBlurImageFilter(blurRadiusPx = blurRadiusPx, bounds = layerSize.toRect()) + createBlurImageFilter(blurRadiusPx = params.blurRadiusPx, bounds = params.layerSize.toRect()) } return ImageFilter @@ -72,8 +53,8 @@ internal actual fun HazeChildNode.createRenderEffect( shaderNames = arrayOf("content", "blur"), inputs = arrayOf(null, blur), ) - .withTints(tints, tintAlphaModulate, progressiveShader, contentOffset) - .withMask(mask, contentSize, contentOffset) + .withTints(params.tints, params.tintAlphaModulate, progressiveShader, params.contentOffset) + .withMask(params.mask, params.contentSize, params.contentOffset) .asComposeRenderEffect() }