diff --git a/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/CupertinoMaterials.kt b/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/CupertinoMaterials.kt index ebd68c0e..cf33f702 100644 --- a/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/CupertinoMaterials.kt +++ b/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/CupertinoMaterials.kt @@ -103,11 +103,11 @@ object CupertinoMaterials { blurRadius = 24.dp, backgroundColor = MaterialTheme.colorScheme.surface, tints = listOf( - HazeTint.Color( + HazeTint( color = if (isDark) darkBackgroundColor else lightBackgroundColor, blendMode = if (isDark) BlendMode.Overlay else BlendMode.ColorDodge, ), - HazeTint.Color(color = if (isDark) darkForegroundColor else lightForegroundColor), + HazeTint(color = if (isDark) darkForegroundColor else lightForegroundColor), ), ) } diff --git a/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/FluentMaterials.kt b/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/FluentMaterials.kt index eb8c20fa..771b8b54 100644 --- a/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/FluentMaterials.kt +++ b/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/FluentMaterials.kt @@ -199,20 +199,20 @@ object FluentMaterials { noiseFactor = noiseFactor, backgroundColor = containerColor, tints = listOf( - HazeTint.Color( + HazeTint( color = containerColor.copy( alpha = if (isDark) darkTintOpacity else lightTintOpacity, ), blendMode = BlendMode.Color, ), - HazeTint.Color( + HazeTint( color = containerColor.copy( alpha = if (isDark) darkLuminosityOpacity else lightLuminosityOpacity, ), blendMode = BlendMode.Luminosity, ), ), - fallbackTint = HazeTint.Color(fallbackColor), + fallbackTint = HazeTint(fallbackColor), ) @ReadOnlyComposable diff --git a/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/HazeMaterials.kt b/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/HazeMaterials.kt index 9f815ec7..d9c5be2d 100644 --- a/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/HazeMaterials.kt +++ b/haze-materials/src/commonMain/kotlin/dev/chrisbanes/haze/materials/HazeMaterials.kt @@ -107,7 +107,7 @@ object HazeMaterials { ): HazeStyle = HazeStyle( blurRadius = 24.dp, backgroundColor = containerColor, - tint = HazeTint.Color( + tint = HazeTint( containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) lightAlpha else darkAlpha), ), ) diff --git a/haze/api/api.txt b/haze/api/api.txt index 1848a1dd..7bed3de7 100644 --- a/haze/api/api.txt +++ b/haze/api/api.txt @@ -15,6 +15,7 @@ package dev.chrisbanes.haze { method public dev.chrisbanes.haze.HazeTint? getFallbackTint(); method public androidx.compose.ui.graphics.Brush? getMask(); method public float getNoiseFactor(); + method public dev.chrisbanes.haze.HazeProgressive? getProgressive(); method public java.util.List getTints(); method public void setAlpha(float); method public void setBackgroundColor(long); @@ -22,13 +23,15 @@ package dev.chrisbanes.haze { method public void setFallbackTint(dev.chrisbanes.haze.HazeTint?); method public void setMask(androidx.compose.ui.graphics.Brush?); method public void setNoiseFactor(float); - method public void setTints(java.util.List); + method public void setProgressive(dev.chrisbanes.haze.HazeProgressive?); + method public void setTints(java.util.List); property public abstract float alpha; property public abstract long backgroundColor; property public abstract float blurRadius; property public abstract dev.chrisbanes.haze.HazeTint? fallbackTint; property public abstract androidx.compose.ui.graphics.Brush? mask; property public abstract float noiseFactor; + property public abstract dev.chrisbanes.haze.HazeProgressive? progressive; property public abstract java.util.List tints; } @@ -47,6 +50,42 @@ package dev.chrisbanes.haze { method public static androidx.compose.ui.Modifier haze(androidx.compose.ui.Modifier, dev.chrisbanes.haze.HazeState state); } + public sealed interface HazeProgressive { + method public int getSteps(); + property public abstract int steps; + field public static final dev.chrisbanes.haze.HazeProgressive.Companion Companion; + field public static final int STEPS_AUTO_BALANCED = -1; // 0xffffffff + } + + public static final class HazeProgressive.Companion { + method public dev.chrisbanes.haze.HazeProgressive.LinearGradient horizontalGradient(optional int steps, optional androidx.compose.animation.core.Easing easing, optional float startX, optional float startIntensity, optional float endX, optional float endIntensity); + method public dev.chrisbanes.haze.HazeProgressive.LinearGradient verticalGradient(optional int steps, optional androidx.compose.animation.core.Easing easing, optional float startY, optional float startIntensity, optional float endY, optional float endIntensity); + field public static final int STEPS_AUTO_BALANCED = -1; // 0xffffffff + } + + public static final class HazeProgressive.LinearGradient implements dev.chrisbanes.haze.HazeProgressive { + ctor public HazeProgressive.LinearGradient(optional int steps, optional androidx.compose.animation.core.Easing easing, optional long start, optional float startIntensity, optional long end, optional float endIntensity); + method public int component1(); + method public androidx.compose.animation.core.Easing component2(); + method public long component3-F1C5BW0(); + method public float component4(); + method public long component5-F1C5BW0(); + method public float component6(); + method public dev.chrisbanes.haze.HazeProgressive.LinearGradient copy-owaVgss(int steps, androidx.compose.animation.core.Easing easing, long start, float startIntensity, long end, float endIntensity); + method public androidx.compose.animation.core.Easing getEasing(); + method public long getEnd(); + method public float getEndIntensity(); + method public long getStart(); + method public float getStartIntensity(); + method public int getSteps(); + property public final androidx.compose.animation.core.Easing easing; + property public final long end; + property public final float endIntensity; + property public final long start; + property public final float startIntensity; + property public int steps; + } + @androidx.compose.runtime.Stable public final class HazeState { ctor public HazeState(); method public androidx.compose.ui.graphics.layer.GraphicsLayer? getContentLayer(); @@ -57,13 +96,13 @@ package dev.chrisbanes.haze { @androidx.compose.runtime.Immutable public final class HazeStyle { ctor public HazeStyle(optional long backgroundColor, optional dev.chrisbanes.haze.HazeTint? tint, optional float blurRadius, optional float noiseFactor, optional dev.chrisbanes.haze.HazeTint? fallbackTint); - ctor public HazeStyle(optional long backgroundColor, optional java.util.List tints, optional float blurRadius, optional float noiseFactor, optional dev.chrisbanes.haze.HazeTint? fallbackTint); + ctor public HazeStyle(optional long backgroundColor, optional java.util.List tints, optional float blurRadius, optional float noiseFactor, optional dev.chrisbanes.haze.HazeTint? fallbackTint); method public long component1-0d7_KjU(); method public java.util.List component2(); method public float component3-D9Ej5fM(); method public float component4(); method public dev.chrisbanes.haze.HazeTint? component5(); - method public dev.chrisbanes.haze.HazeStyle copy-cq6XJ1M(long backgroundColor, java.util.List tints, float blurRadius, float noiseFactor, dev.chrisbanes.haze.HazeTint? fallbackTint); + method public dev.chrisbanes.haze.HazeStyle copy-cq6XJ1M(long backgroundColor, java.util.List tints, float blurRadius, float noiseFactor, dev.chrisbanes.haze.HazeTint? fallbackTint); method public long getBackgroundColor(); method public float getBlurRadius(); method public dev.chrisbanes.haze.HazeTint? getFallbackTint(); @@ -82,25 +121,11 @@ package dev.chrisbanes.haze { property public final dev.chrisbanes.haze.HazeStyle Unspecified; } - @androidx.compose.runtime.Stable public interface HazeTint { - } - - public static final class HazeTint.Brush implements dev.chrisbanes.haze.HazeTint { - ctor public HazeTint.Brush(androidx.compose.ui.graphics.Brush brush, optional int blendMode); - method public androidx.compose.ui.graphics.Brush component1(); - method public int component2-0nO6VwU(); - method public dev.chrisbanes.haze.HazeTint.Brush copy-GB0RdKg(androidx.compose.ui.graphics.Brush brush, int blendMode); - method public int getBlendMode(); - method public androidx.compose.ui.graphics.Brush getBrush(); - property public final int blendMode; - property public final androidx.compose.ui.graphics.Brush brush; - } - - public static final class HazeTint.Color implements dev.chrisbanes.haze.HazeTint { - ctor public HazeTint.Color(long color, optional int blendMode); + @androidx.compose.runtime.Stable public final class HazeTint { + ctor public HazeTint(long color, optional int blendMode); method public long component1-0d7_KjU(); method public int component2-0nO6VwU(); - method public dev.chrisbanes.haze.HazeTint.Color copy-xETnrds(long color, int blendMode); + method public dev.chrisbanes.haze.HazeTint copy-xETnrds(long color, int blendMode); method public int getBlendMode(); method public long getColor(); property public final int blendMode; diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_horiz.png b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_horiz.png new file mode 100644 index 00000000..faf6a282 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_horiz.png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_horiz[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_horiz[28].png new file mode 100644 index 00000000..515560bd Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_horiz[28].png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_vertical.png b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_vertical.png new file mode 100644 index 00000000..7ad94973 Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_vertical.png differ diff --git a/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_vertical[28].png b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_vertical[28].png new file mode 100644 index 00000000..515560bd Binary files /dev/null and b/haze/screenshots/android/HazeScreenshotTest.creditCard_progressive_vertical[28].png differ diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard_progressive_horiz.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_progressive_horiz.png new file mode 100644 index 00000000..c4ff2058 Binary files /dev/null and b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_progressive_horiz.png differ diff --git a/haze/screenshots/desktop/HazeScreenshotTest.creditCard_progressive_vertical.png b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_progressive_vertical.png new file mode 100644 index 00000000..ef9fcf1f Binary files /dev/null and b/haze/screenshots/desktop/HazeScreenshotTest.creditCard_progressive_vertical.png differ diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNode.android.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNode.android.kt index d5d44429..b8b372eb 100644 --- a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNode.android.kt +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNode.android.kt @@ -16,90 +16,44 @@ import androidx.annotation.RequiresApi import androidx.collection.lruCache import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size -import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.RenderEffect import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.asComposeRenderEffect import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.graphics.layer.GraphicsLayer -import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.graphics.withSaveLayer import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.Density import kotlin.math.roundToInt internal actual fun HazeChildNode.createRenderEffect( - effect: ReusableHazeEffect, - density: Density, -): RenderEffect? = - with(effect) { - val blurRadiusPx = with(density) { blurRadiusOrZero.toPx() } - if (Build.VERSION.SDK_INT >= 31 && blurRadiusPx >= 0.005f) { - val bounds = Rect(effect.layerOffset, effect.size) - return AndroidRenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.CLAMP) - .withNoise(noiseFactor) - .withTints(effect.tints, bounds) - .withMask(effect.mask, bounds) - .asComposeRenderEffect() - } - return null + blurRadiusPx: Float, + noiseFactor: Float, + tints: List, + tintAlphaModulate: Float, + boundsInLayer: Rect, + layerSize: Size, + mask: Brush?, +): RenderEffect? { + log("HazeChildNode") { + "createRenderEffect. blurRadiusPx=$blurRadiusPx, noiseFactor=$noiseFactor, " + + "tints=$tints, layerBounds=$boundsInLayer" } -internal actual fun DrawScope.useGraphicLayers(): Boolean { - return Build.VERSION.SDK_INT >= 32 && drawContext.canvas.nativeCanvas.isHardwareAccelerated + if (Build.VERSION.SDK_INT >= 31 && blurRadiusPx >= 0.005f) { + return AndroidRenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.CLAMP) + .withNoise(noiseFactor) + .withTints(tints, tintAlphaModulate) + .withMask(mask, boundsInLayer) + .asComposeRenderEffect() + } + return null } -internal actual fun HazeChildNode.drawEffect( - drawScope: DrawScope, - effect: ReusableHazeEffect, - graphicsLayer: GraphicsLayer?, -) = with(drawScope) { - if (graphicsLayer != null && drawContext.canvas.nativeCanvas.isHardwareAccelerated) { - drawLayer(graphicsLayer) - } else { - val mask = effect.mask - val tint = effect.fallbackTint - ?.takeIf { - when (it) { - is HazeTint.Color -> it.color.isSpecified - else -> true - } - } ?: effect.tints.firstOrNull()?.boostForFallback(effect.blurRadiusOrZero) - - println("Drawing effect with scrim: tint=$tint, mask=$mask, alpha=${effect.alpha}") - - fun scrim() { - if (tint is HazeTint.Color) { - if (mask != null) { - drawRect(brush = mask, colorFilter = ColorFilter.tint(tint.color)) - } else { - drawRect(color = tint.color, blendMode = tint.blendMode) - } - } else if (tint is HazeTint.Brush) { - // We don't currently support mask + brush tints - drawRect(brush = tint.brush, blendMode = tint.blendMode) - } - } - - if (effect.alpha != 1f) { - val paint = Paint().apply { - this.alpha = effect.alpha - } - drawContext.canvas.withSaveLayer(size.toRect(), paint) { - scrim() - } - } else { - scrim() - } - } +internal actual fun DrawScope.useGraphicLayers(): Boolean { + return Build.VERSION.SDK_INT >= 32 && drawContext.canvas.nativeCanvas.isHardwareAccelerated } private val noiseTextureCache = lruCache(3) @@ -165,28 +119,34 @@ private fun Brush.toShader(size: Size): Shader? = when (this) { @RequiresApi(31) private fun AndroidRenderEffect.withTints( tints: List, - bounds: Rect, + alphaModulate: Float, ): AndroidRenderEffect { return tints.fold(this) { acc, next -> - acc.withTint(next, bounds) + acc.withTint(next, alphaModulate) } } @RequiresApi(31) private fun AndroidRenderEffect.withTint( tint: HazeTint?, - bounds: Rect, -): AndroidRenderEffect = when { - tint is HazeTint.Color && tint.color.alpha >= 0.005f -> { - AndroidRenderEffect.createColorFilterEffect( - BlendModeColorFilter(tint.color.toArgb(), tint.blendMode.toAndroidBlendMode()), - this, - ) + alphaModulate: Float, +): AndroidRenderEffect { + if (tint != null) { + val color = tint.color + val modulated = color.copy(alpha = color.alpha * alphaModulate) + + if (modulated.alpha >= 0.005f) { + return AndroidRenderEffect.createColorFilterEffect( + BlendModeColorFilter( + modulated.toArgb(), + tint.blendMode.toAndroidBlendMode(), + ), + this, + ) + } } - tint is HazeTint.Brush -> withBrush(tint.brush, bounds, tint.blendMode.toAndroidBlendMode()) - - else -> this + return this } /** diff --git a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Log.android.kt b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Log.android.kt index 295abfa3..0f89c832 100644 --- a/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Log.android.kt +++ b/haze/src/androidMain/kotlin/dev/chrisbanes/haze/Log.android.kt @@ -6,7 +6,7 @@ package dev.chrisbanes.haze import android.util.Log internal actual fun log(tag: String, message: () -> String) { - if (LOG_ENABLED && Log.isLoggable(tag, Log.DEBUG)) { + if (LOG_ENABLED) { Log.d(tag, message()) } } diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt index bb56fc13..2e0328b5 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt @@ -74,11 +74,11 @@ object HazeDefaults { fun tint(color: Color): HazeTint = when { color.isSpecified -> color.copy(alpha = color.alpha * tintAlpha) else -> color - }.let { HazeTint.Color(it) } + }.let(::HazeTint) @Deprecated( "Migrate to HazeTint for tint", - ReplaceWith("HazeStyle(backgroundColor, HazeTint.Color(tint), blurRadius, noiseFactor)"), + ReplaceWith("HazeStyle(backgroundColor, HazeTint(tint), blurRadius, noiseFactor)"), ) fun style( backgroundColor: Color = Color.Unspecified, @@ -159,17 +159,10 @@ data class HazeStyle( } @Stable -interface HazeTint { - data class Color( - val color: androidx.compose.ui.graphics.Color, - val blendMode: BlendMode = BlendMode.SrcOver, - ) : HazeTint - - data class Brush( - val brush: androidx.compose.ui.graphics.Brush, - val blendMode: BlendMode = BlendMode.SrcOver, - ) : HazeTint -} +data class HazeTint( + val color: Color, + val blendMode: BlendMode = BlendMode.SrcOver, +) /** * Resolves the style which should be used by renderers. The style returned from here diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt index 7d5ed387..fd03b7d4 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChild.kt @@ -19,7 +19,11 @@ interface HazeChildScope { var alpha: Float /** - * Optional mask which allows effects, such as fading via a [Brush.verticalGradient] or similar. + * Optional alpha mask which allows effects such as fading via a + * [Brush.verticalGradient] or similar. This is only applied when [progressive] is null. + * + * An alpha mask provides a similar effect as that provided as [HazeProgressive], in a more + * performant way, but may provide a less pleasing visual result. */ var mask: Brush? @@ -51,6 +55,17 @@ interface HazeChildScope { */ var fallbackTint: HazeTint? + /** + * Parameters for enabling a progressive (or gradient) blur effect, or null for a uniform + * blurring effect. Defaults to null. + * + * Please note: progressive blurring effects can be expensive, so you should test on a variety + * of devices to verify that performance is acceptable for your use case. An alternative and + * more performant way to achieve this effect is via the [mask] parameter, at the cost of + * visual finesse. + */ + var progressive: HazeProgressive? + /** * Apply the given [HazeStyle] to this block. */ @@ -77,7 +92,7 @@ fun Modifier.hazeChild( state: HazeState, shape: Shape, style: HazeStyle, -): Modifier = this.clip(shape).hazeChild(state, style) +): Modifier = clip(shape).hazeChild(state, style) /** * Mark this composable as being a Haze child composable. @@ -110,6 +125,7 @@ private data class HazeChildNodeElement( val state: HazeState, val block: HazeChildScope.() -> Unit, ) : ModifierNodeElement() { + override fun create(): HazeChildNode = HazeChildNode(state, block) override fun update(node: HazeChildNode) { diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt index 1dbb1288..7a587f61 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt @@ -3,16 +3,22 @@ package dev.chrisbanes.haze +import androidx.compose.animation.core.EaseIn +import androidx.compose.animation.core.Easing import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.geometry.takeOrElse +import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.RenderEffect import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope @@ -20,6 +26,7 @@ import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.withSaveLayer import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.node.CompositionLocalConsumerModifierNode @@ -38,6 +45,10 @@ 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 kotlin.math.ceil +import kotlin.math.hypot +import kotlin.math.max +import kotlin.math.min internal class HazeChildNode( var state: HazeState, @@ -97,10 +108,10 @@ internal class HazeChildNode( if (useGraphicLayers()) { val contentLayer = state.contentLayer if (contentLayer != null) { - drawEffectsWithGraphicsLayer(contentLayer) + drawEffectWithGraphicsLayer(contentLayer) } } else { - drawEffectsWithScrim() + drawEffectWithScrim() } // Finally we draw the content @@ -121,62 +132,180 @@ internal class HazeChildNode( } } - private fun DrawScope.drawEffectsWithGraphicsLayer(contentLayer: GraphicsLayer) { + private fun DrawScope.drawEffectWithGraphicsLayer(contentLayer: GraphicsLayer) { // Now we need to draw `contentNode` into each of an 'effect' graphic layers. // The RenderEffect applied will provide the blurring effect. - currentValueOf(LocalGraphicsContext).useGraphicsLayer { layer -> - layer.renderEffect = effect.renderEffect - layer.alpha = effect.alpha + val graphicsContext = currentValueOf(LocalGraphicsContext) + val clippedContentLayer = graphicsContext.createGraphicsLayer() + + // The layer size is usually than the bounds. This is so that we include enough + // content around the edges to keep the blurring uniform. Without the extra border, + // the blur will naturally fade out at the edges. + val inflatedSize = effect.layerSize + // This is the topLeft in the inflated bounds where the real are should be at [0,0] + val inflatedOffset = effect.layerOffset + + clippedContentLayer.record(inflatedSize.roundToIntSize()) { + require(effect.backgroundColor.isSpecified) { + "backgroundColor not specified. Please provide a color." + } + drawRect(effect.backgroundColor) - // The layer size is usually than the bounds. This is so that we include enough - // content around the edges to keep the blurring uniform. Without the extra border, - // the blur will naturally fade out at the edges. - val inflatedSize = effect.layerSize - // This is the topLeft in the inflated bounds where the real are should be at [0,0] - val inflatedOffset = effect.layerOffset - - layer.record(inflatedSize.roundToIntSize()) { - if (effect.backgroundColor.isSpecified) { - drawRect(effect.backgroundColor) - } else { - error("HazeStyle.backgroundColor not specified. Please provide a color.") - } - - translate(inflatedOffset + state.positionOnScreen - effect.positionOnScreen) { - // Draw the content into our effect layer - drawLayer(contentLayer) - } + translate(inflatedOffset + state.positionOnScreen - effect.positionOnScreen) { + // Draw the content into our effect layer + drawLayer(contentLayer) } + } - drawEffect(effect = effect, innerDrawOffset = -inflatedOffset, layer = layer) + val progressive = effect.progressive + if (progressive is HazeProgressive.LinearGradient && useGraphicLayers()) { + drawLinearGradientProgressiveEffect( + effect = effect, + progressive = progressive, + innerDrawOffset = -inflatedOffset, + contentLayer = clippedContentLayer, + ) + } else { + clippedContentLayer.renderEffect = effect.renderEffect + clippedContentLayer.alpha = effect.alpha + + withPositionAndClip( + effectPositionOnScreen = effect.positionOnScreen, + size = effect.size, + innerDrawOffset = -inflatedOffset, + ) { + drawLayer(clippedContentLayer) + } } + + graphicsContext.releaseGraphicsLayer(clippedContentLayer) } - private fun DrawScope.drawEffectsWithScrim() { - drawEffect(effect) + private fun DrawScope.drawEffectWithScrim() { + // Maybe we can do this progressive too? + drawFallbackEffect( + alpha = effect.alpha, + blurRadius = effect.blurRadius, + tints = effect.tints, + fallbackTint = effect.fallbackTint, + mask = effect.mask, + ) } - private fun DrawScope.drawEffect( + private fun DrawScope.drawLinearGradientProgressiveEffect( effect: ReusableHazeEffect, - innerDrawOffset: Offset = Offset.Zero, - layer: GraphicsLayer? = null, + progressive: HazeProgressive.LinearGradient, + innerDrawOffset: Offset, + contentLayer: GraphicsLayer, ) { - val drawOffset = (effect.positionOnScreen - positionOnScreen).takeOrElse { Offset.Zero } + require(progressive.steps == HazeProgressive.STEPS_AUTO_BALANCED || progressive.steps > 1) { + "steps needs to be STEPS_AUTO_BALANCED, or a value greater than 1" + } + require(progressive.startIntensity in 0f..1f) + require(progressive.endIntensity in 0f..1f) + + var steps = progressive.steps + if (steps == HazeProgressive.STEPS_AUTO_BALANCED) { + // Here we're going to calculate an appropriate amount of steps for the length. + // We use a calculation of 48dp per step, which is a good balance between + // quality vs performance + val stepHeightPx = with(drawContext.density) { 48.dp.toPx() } + val length = calculateLength(progressive.start, progressive.end, effect.size) + steps = ceil(length / stepHeightPx).toInt().coerceAtLeast(2) + } + + val graphicsContext = currentValueOf(LocalGraphicsContext) + + val seq = when { + progressive.endIntensity >= progressive.startIntensity -> 0..steps + else -> steps downTo 0 + } + + for (i in seq) { + val fraction = i / steps.toFloat() + val intensity = lerp( + progressive.startIntensity, + progressive.endIntensity, + progressive.easing.transform(fraction), + ) + + val layer = graphicsContext.createGraphicsLayer() + layer.record(contentLayer.size) { + drawLayer(contentLayer) + } + + val maskStops = buildList { + val min = min(progressive.startIntensity, progressive.endIntensity) + val max = max(progressive.startIntensity, progressive.endIntensity) + add(lerp(min, max, (i - 2f) / steps) to Color.Transparent) + add(lerp(min, max, (i - 1f) / steps) to Color.Black) + add(lerp(min, max, (i + 0f) / steps) to Color.Black) + add(lerp(min, max, (i + 1f) / steps) to Color.Transparent) + } + + log("HazeChildNode") { + "drawProgressiveEffect. " + + "step=$i, " + + "fraction=$fraction, " + + "intensity=$intensity, " + + "maskStops=${maskStops.map { it.first to it.second.alpha }}" + } + + val blurRadiusPx = with(drawContext.density) { effect.blurRadiusOrZero.toPx() } + val boundsInLayer = Rect(effect.layerOffset, effect.size) + + layer.alpha = effect.alpha + layer.renderEffect = createRenderEffect( + blurRadiusPx = intensity * blurRadiusPx, + noiseFactor = effect.noiseFactor, + tints = effect.tints, + tintAlphaModulate = intensity, + boundsInLayer = boundsInLayer, // cache this + layerSize = effect.layerSize, + mask = Brush.linearGradient( + *maskStops.toTypedArray(), + start = progressive.start, + end = progressive.end, + ), + ) + + withPositionAndClip( + effectPositionOnScreen = effect.positionOnScreen, + size = effect.size, + innerDrawOffset = innerDrawOffset, + block = { drawLayer(layer) }, + ) + + graphicsContext.releaseGraphicsLayer(layer) + } + } + private inline fun DrawScope.withPositionAndClip( + effectPositionOnScreen: Offset, + size: Size, + innerDrawOffset: Offset = Offset.Zero, + block: DrawScope.() -> Unit, + ) { + val drawOffset = (effectPositionOnScreen - positionOnScreen).takeOrElse { Offset.Zero } translate(drawOffset) { - clipRect(right = effect.size.width, bottom = effect.size.height) { + clipRect(right = size.width, bottom = size.height) { // 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(innerDrawOffset) { - drawEffect(this, effect, layer) - } + translate(innerDrawOffset, block) } } } private fun ReusableHazeEffect.onPreDraw(density: Density) { if (renderEffectDirty) { - renderEffect = createRenderEffect(this, density) + renderEffect = createRenderEffect( + blurRadiusPx = with(density) { blurRadiusOrZero.toPx() }, // cache this + noiseFactor = noiseFactor, + tints = tints, + boundsInLayer = Rect(layerOffset, size), // cache this + layerSize = layerSize, + mask = mask, + ) renderEffectDirty = false } // We don't update the path here as we may not need it. Let draw request it @@ -192,21 +321,130 @@ internal class HazeChildNode( } } -internal expect fun HazeChildNode.drawEffect( - drawScope: DrawScope, - effect: ReusableHazeEffect, - graphicsLayer: GraphicsLayer? = null, -) +/** + * Parameters for applying a progressive blur effect. + */ +sealed interface HazeProgressive { + /** + * The number of discrete steps which should be used to make up the progressive / gradient + * effect. More steps results in a higher quality effect, but at the cost of performance. + * + * Set to [STEPS_AUTO_BALANCED] so that the value is automatically computed, balancing quality + * and performance. + */ + val steps: Int + + /** + * A linear gradient effect. + * + * You may wish to use the convenience builder functions provided in [horizontalGradient] and + * [verticalGradient] for more common use cases. + * + * @param steps - The number of steps in the effect. See [HazeProgressive.steps] for information. + * @param easing - The easing function to use when applying the effect. Defaults to a + * linear easing effect. + * @param start - Starting position of the gradient. Defaults to [Offset.Zero] which + * represents the top-left of the drawing area. + * @param startIntensity - The intensity of the haze effect at the start, in the range `0f`..`1f`. + * @param end - Ending position of the gradient. Defaults to + * [Offset.Infinite] which represents the bottom-right of the drawing area. + * @param endIntensity - The intensity of the haze effect at the end, in the range `0f`..`1f` + */ + data class LinearGradient( + override val steps: Int = STEPS_AUTO_BALANCED, + val easing: Easing = EaseIn, + val start: Offset = Offset.Zero, + val startIntensity: Float = 0f, + val end: Offset = Offset.Infinite, + val endIntensity: Float = 1f, + ) : HazeProgressive + + companion object { + /** + * A vertical gradient effect. + * + * @param steps - The number of steps in the effect. See [HazeProgressive.steps] for information. + * @param easing - The easing function to use when applying the effect. Defaults to a + * linear easing effect. + * @param startY - Starting x position of the horizontal gradient. Defaults to 0 which + * represents the top of the drawing area. + * @param startIntensity - The intensity of the haze effect at the start, in the range `0f`..`1f`. + * @param endY - Ending x position of the horizontal gradient. Defaults to + * [Float.POSITIVE_INFINITY] which represents the bottom of the drawing area. + * @param endIntensity - The intensity of the haze effect at the end, in the range `0f`..`1f` + */ + fun verticalGradient( + steps: Int = STEPS_AUTO_BALANCED, + easing: Easing = EaseIn, + startY: Float = 0f, + startIntensity: Float = 0f, + endY: Float = Float.POSITIVE_INFINITY, + endIntensity: Float = 1f, + ): LinearGradient = LinearGradient( + steps = steps, + easing = easing, + start = Offset(0f, startY), + startIntensity = startIntensity, + end = Offset(0f, endY), + endIntensity = endIntensity, + ) + + /** + * A horizontal gradient effect. + * + * @param steps - The number of steps in the effect. See [HazeProgressive.steps] for information. + * @param easing - The easing function to use when applying the effect. Defaults to a + * linear easing effect. + * @param startX - Starting x position of the horizontal gradient. Defaults to 0 which + * represents the left of the drawing area + * @param startIntensity - The intensity of the haze effect at the start, in the range `0f`..`1f` + * @param endX - Ending x position of the horizontal gradient. Defaults to + * [Float.POSITIVE_INFINITY] which represents the right of the drawing area. + * @param endIntensity - The intensity of the haze effect at the end, in the range `0f`..`1f` + */ + fun horizontalGradient( + steps: Int = STEPS_AUTO_BALANCED, + easing: Easing = EaseIn, + startX: Float = 0f, + startIntensity: Float = 0f, + endX: Float = Float.POSITIVE_INFINITY, + endIntensity: Float = 1f, + ): LinearGradient = LinearGradient( + steps = steps, + easing = easing, + start = Offset(startX, 0f), + startIntensity = startIntensity, + end = Offset(endX, 0f), + endIntensity = endIntensity, + ) + + /** + * Value which indicates the [steps] value should be automatically computed, + * balancing quality and performance. + */ + const val STEPS_AUTO_BALANCED = -1 + } +} + +private fun lerp(start: Float, stop: Float, fraction: Float): Float { + return start + fraction * (stop - start) +} internal expect fun HazeChildNode.createRenderEffect( - effect: ReusableHazeEffect, - density: Density, + blurRadiusPx: Float, + noiseFactor: Float, + tints: List = emptyList(), + tintAlphaModulate: Float = 1f, + boundsInLayer: Rect, + layerSize: Size, + mask: Brush? = null, ): RenderEffect? internal class ReusableHazeEffect : HazeChildScope { var renderEffect: RenderEffect? = null var renderEffectDirty: Boolean = true var drawParametersDirty: Boolean = true + var progressiveDirty: Boolean = true var positionOnScreen: Offset by mutableStateOf(Offset.Unspecified) @@ -292,6 +530,14 @@ internal class ReusableHazeEffect : HazeChildScope { } } + override var progressive: HazeProgressive? = null + set(value) { + if (value != field) { + progressiveDirty = true + field = value + } + } + override fun applyStyle(style: HazeStyle) { noiseFactor = style.noiseFactor blurRadius = style.blurRadius @@ -305,8 +551,54 @@ internal val ReusableHazeEffect.blurRadiusOrZero: Dp get() = blurRadius.takeOrElse { 0.dp } internal val ReusableHazeEffect.needInvalidation: Boolean - get() = renderEffectDirty || drawParametersDirty + get() = renderEffectDirty || drawParametersDirty || progressiveDirty private fun Size.expand(expansion: Float): Size { return Size(width = width + expansion, height = height + expansion) } + +private fun DrawScope.drawFallbackEffect( + alpha: Float, + blurRadius: Dp, + tints: List, + fallbackTint: HazeTint?, + mask: Brush?, +) { + val tint = fallbackTint + ?.takeIf { it.color.isSpecified } + ?: tints.firstOrNull()?.boostForFallback(blurRadius.takeOrElse { 0.dp }) + + log("HazeChildNode") { "drawEffect. Drawing effect with scrim: tint=$tint, mask=$mask, alpha=$alpha" } + + fun scrim() { + if (tint != null) { + if (mask != null) { + drawRect(brush = mask, colorFilter = ColorFilter.tint(tint.color)) + } else { + drawRect(color = tint.color, blendMode = tint.blendMode) + } + } + } + + if (alpha != 1f) { + val paint = Paint().apply { + this.alpha = alpha + } + drawContext.canvas.withSaveLayer(size.toRect(), paint) { + scrim() + } + } else { + scrim() + } +} + +private fun calculateLength( + start: Offset, + end: Offset, + size: Size, +): Float { + val (startX, startY) = start + val endX = end.x.coerceAtMost(size.width) + val endY = end.y.coerceAtMost(size.height) + return hypot(endX - startX, endY - startY) +} diff --git a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt index 9f25a931..f5541ddf 100644 --- a/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt +++ b/haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeNode.kt @@ -80,14 +80,11 @@ internal class HazeNode( internal expect fun DrawScope.useGraphicLayers(): Boolean -internal fun HazeTint.boostForFallback(blurRadius: Dp): HazeTint = when (this) { - is HazeTint.Color -> { - // For color, we can boost the alpha - val boosted = color.boostAlphaForBlurRadius(blurRadius.takeOrElse { HazeDefaults.blurRadius }) - copy(color = boosted) - } - // For anything else we just use as-is - else -> this +internal fun HazeTint.boostForFallback(blurRadius: Dp): HazeTint { + // For color, we can boost the alpha + val resolved = blurRadius.takeOrElse { HazeDefaults.blurRadius } + val boosted = color.boostAlphaForBlurRadius(resolved) + return copy(color = boosted) } /** diff --git a/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt b/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt index f999e7e2..0291d1fb 100644 --- a/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt +++ b/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/HazeScreenshotTest.kt @@ -30,7 +30,7 @@ class HazeScreenshotTest : ScreenshotTest() { fun creditCard_transparentTint() = runScreenshotTest { setContent { ScreenshotTheme { - CreditCardSample(tint = HazeTint.Color(Color.Transparent)) + CreditCardSample(tint = HazeTint(Color.Transparent)) } } captureRoot() @@ -62,10 +62,34 @@ class HazeScreenshotTest : ScreenshotTest() { captureRoot() } + @Test + fun creditCard_progressive_horiz() = runScreenshotTest { + setContent { + ScreenshotTheme { + CreditCardSample( + progressive = HazeProgressive.horizontalGradient(), + ) + } + } + captureRoot() + } + + @Test + fun creditCard_progressive_vertical() = runScreenshotTest { + setContent { + ScreenshotTheme { + CreditCardSample( + progressive = HazeProgressive.verticalGradient(), + ) + } + } + captureRoot() + } + @Test fun creditCard_childTint() = runScreenshotTest { var tint by mutableStateOf( - HazeTint.Color(Color.Magenta.copy(alpha = 0.5f)), + HazeTint(Color.Magenta.copy(alpha = 0.5f)), ) setContent { @@ -77,11 +101,11 @@ class HazeScreenshotTest : ScreenshotTest() { waitForIdle() captureRoot("magenta") - tint = HazeTint.Color(Color.Yellow.copy(alpha = 0.5f)) + tint = HazeTint(Color.Yellow.copy(alpha = 0.5f)) waitForIdle() captureRoot("yellow") - tint = HazeTint.Color(Color.Red.copy(alpha = 0.5f)) + tint = HazeTint(Color.Red.copy(alpha = 0.5f)) waitForIdle() captureRoot("red") } diff --git a/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt b/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt index 68c4fa4e..74600c85 100644 --- a/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt +++ b/haze/src/screenshotTest/kotlin/dev/chrisbanes/haze/ScreenshotTestContent.kt @@ -26,10 +26,11 @@ import androidx.compose.ui.unit.dp @Composable internal fun CreditCardSample( - tint: HazeTint = HazeTint.Color(Color.White.copy(alpha = 0.1f)), + tint: HazeTint = HazeTint(Color.White.copy(alpha = 0.1f)), shape: RoundedCornerShape = RoundedCornerShape(16.dp), enabled: Boolean = true, mask: Brush? = null, + progressive: HazeProgressive? = null, alpha: Float = 1f, ) { val hazeState = remember { HazeState() } @@ -66,15 +67,14 @@ internal fun CreditCardSample( .then( when { enabled -> { - Modifier.hazeChild( - state = hazeState, - ) { + Modifier.hazeChild(state = hazeState) { backgroundColor = surfaceColor noiseFactor = HazeDefaults.noiseFactor tints = listOfNotNull(tint) blurRadius = 8.dp this.mask = mask this.alpha = alpha + this.progressive = progressive } } else -> Modifier diff --git a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeNode.skiko.kt b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeNode.skiko.kt index 4abc702f..66a1a9f0 100644 --- a/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeNode.skiko.kt +++ b/haze/src/skikoMain/kotlin/dev/chrisbanes/haze/HazeNode.skiko.kt @@ -12,10 +12,7 @@ import androidx.compose.ui.graphics.RenderEffect import androidx.compose.ui.graphics.ShaderBrush 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.graphics.toArgb -import androidx.compose.ui.unit.Density import org.jetbrains.skia.BlendMode import org.jetbrains.skia.ColorFilter import org.jetbrains.skia.FilterTileMode @@ -24,28 +21,32 @@ import org.jetbrains.skia.ImageFilter import org.jetbrains.skia.RuntimeShaderBuilder import org.jetbrains.skia.Shader -internal actual fun HazeChildNode.drawEffect( - drawScope: DrawScope, - effect: ReusableHazeEffect, - graphicsLayer: GraphicsLayer?, -) = with(drawScope) { - drawLayer(requireNotNull(graphicsLayer)) -} - internal actual fun DrawScope.useGraphicLayers(): Boolean = true internal actual fun HazeChildNode.createRenderEffect( - effect: ReusableHazeEffect, - density: Density, + blurRadiusPx: Float, + noiseFactor: Float, + tints: List, + tintAlphaModulate: Float, + boundsInLayer: Rect, + layerSize: Size, + mask: Brush?, ): RenderEffect? { + log("HazeChildNode") { + "createRenderEffect. blurRadiusPx=$blurRadiusPx, " + + "noiseFactor=$noiseFactor, " + + "tints=$tints, " + + "boundsInLayer=$boundsInLayer, " + + "layerSize=$layerSize" + } + val compositeShaderBuilder = RuntimeShaderBuilder(RUNTIME_SHADER).apply { - uniform("noiseFactor", effect.noiseFactor) + uniform("noiseFactor", noiseFactor.coerceIn(0f, 1f)) child("noise", NOISE_SHADER) } + // For CLAMP to work, we need to provide the crop rect - val blurRadiusPx = with(density) { effect.blurRadiusOrZero.toPx() } - val blurFilter = createBlurImageFilter(blurRadiusPx, effect.layerSize.toRect()) - val bounds = Rect(effect.layerOffset, effect.size) + val blurFilter = createBlurImageFilter(blurRadiusPx, layerSize.toRect()) return ImageFilter .makeRuntimeShader( @@ -53,29 +54,35 @@ internal actual fun HazeChildNode.createRenderEffect( shaderNames = arrayOf("content", "blur"), inputs = arrayOf(null, blurFilter), ) - .withTints(effect.tints, bounds) - .withBrush(effect.mask, bounds, BlendMode.DST_IN) + .withTints(tints, tintAlphaModulate) + .withBrush(mask, boundsInLayer, BlendMode.DST_IN) .asComposeRenderEffect() } -private fun ImageFilter.withTints(tints: List, bounds: Rect): ImageFilter { +private fun ImageFilter.withTints(tints: List, alphaModulate: Float): ImageFilter { return tints.fold(this) { acc, tint -> - acc.withTint(tint, bounds) + acc.withTint(tint, alphaModulate) } } -private fun ImageFilter.withTint(tint: HazeTint?, bounds: Rect): ImageFilter = when { - tint is HazeTint.Color && tint.color.alpha >= 0.005f -> { - ImageFilter.makeColorFilter( - f = ColorFilter.makeBlend(tint.color.toArgb(), tint.blendMode.toSkiaBlendMode()), - input = this, - crop = null, - ) - } +private fun ImageFilter.withTint(tint: HazeTint?, alphaModulate: Float): ImageFilter { + if (tint != null) { + val color = tint.color + val modulated = color.copy(alpha = color.alpha * alphaModulate) - tint is HazeTint.Brush -> withBrush(tint.brush, bounds, tint.blendMode.toSkiaBlendMode()) + if (modulated.alpha >= 0.005f) { + return ImageFilter.makeColorFilter( + f = ColorFilter.makeBlend( + color = modulated.toArgb(), + mode = tint.blendMode.toSkiaBlendMode(), + ), + input = this, + crop = null, + ) + } + } - else -> this + return this } private fun ImageFilter.withBrush( @@ -87,19 +94,24 @@ private fun ImageFilter.withBrush( return ImageFilter.makeBlend( blendMode = blendMode, - fg = ImageFilter.makeShader(shader = shader, crop = bounds.toIRect()), + fg = ImageFilter.makeOffset( + dx = bounds.left, + dy = bounds.top, + input = ImageFilter.makeShader(shader = shader, crop = null), + crop = null, + ), bg = this, crop = null, ) } -private fun createBlurImageFilter(blurRadiusPx: Float, bounds: Rect): ImageFilter { +private fun createBlurImageFilter(blurRadiusPx: Float, bounds: Rect? = null): ImageFilter { val sigma = BlurEffect.convertRadiusToSigma(blurRadiusPx) return ImageFilter.makeBlur( sigmaX = sigma, sigmaY = sigma, mode = FilterTileMode.CLAMP, - crop = bounds.toIRect(), + crop = bounds?.toIRect(), ) } diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt index b70e2c76..fa4d058e 100644 --- a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/CardSample.kt @@ -99,7 +99,7 @@ fun CreditCardSample(navigator: Navigator) { .clip(RoundedCornerShape(16.dp)) .hazeChild(state = hazeState) { backgroundColor = Color.Blue - tints = listOf(HazeTint.Color(Color.White.copy(alpha = 0.1f))) + tints = listOf(HazeTint(Color.White.copy(alpha = 0.1f))) blurRadius = 8.dp noiseFactor = HazeDefaults.noiseFactor }, diff --git a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt index f7bdcea9..84ceb59a 100644 --- a/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt +++ b/sample/shared/src/commonMain/kotlin/dev/chrisbanes/haze/sample/ScaffoldSample.kt @@ -4,7 +4,6 @@ package dev.chrisbanes.haze.sample import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.EaseInOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Arrangement @@ -22,12 +21,12 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -36,10 +35,10 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.HazeProgressive import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.haze import dev.chrisbanes.haze.hazeChild @@ -59,7 +58,7 @@ fun ScaffoldSample(navigator: Navigator) { Scaffold( topBar = { - TopAppBar( + LargeTopAppBar( title = { Text(text = "Haze Scaffold sample") }, navigationIcon = { IconButton(onClick = navigator::navigateUp) { @@ -73,7 +72,7 @@ fun ScaffoldSample(navigator: Navigator) { modifier = Modifier .hazeChild(hazeState) { applyStyle(style) - mask = Brush.easedVerticalGradient(easing = EaseInOut) + progressive = HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) } .fillMaxWidth(), )