Skip to content

Commit

Permalink
Progressive blur (#346)
Browse files Browse the repository at this point in the history
Fixes #344
  • Loading branch information
chrisbanes authored Oct 10, 2024
1 parent 1643357 commit 7b7b171
Show file tree
Hide file tree
Showing 21 changed files with 539 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
)
Expand Down
65 changes: 45 additions & 20 deletions haze/api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ 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<dev.chrisbanes.haze.HazeTint> getTints();
method public void setAlpha(float);
method public void setBackgroundColor(long);
method public void setBlurRadius(float);
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<? extends dev.chrisbanes.haze.HazeTint>);
method public void setProgressive(dev.chrisbanes.haze.HazeProgressive?);
method public void setTints(java.util.List<dev.chrisbanes.haze.HazeTint>);
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<dev.chrisbanes.haze.HazeTint> tints;
}

Expand All @@ -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();
Expand All @@ -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<? extends dev.chrisbanes.haze.HazeTint> tints, optional float blurRadius, optional float noiseFactor, optional dev.chrisbanes.haze.HazeTint? fallbackTint);
ctor public HazeStyle(optional long backgroundColor, optional java.util.List<dev.chrisbanes.haze.HazeTint> tints, optional float blurRadius, optional float noiseFactor, optional dev.chrisbanes.haze.HazeTint? fallbackTint);
method public long component1-0d7_KjU();
method public java.util.List<dev.chrisbanes.haze.HazeTint> 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<? extends dev.chrisbanes.haze.HazeTint> tints, float blurRadius, float noiseFactor, dev.chrisbanes.haze.HazeTint? fallbackTint);
method public dev.chrisbanes.haze.HazeStyle copy-cq6XJ1M(long backgroundColor, java.util.List<dev.chrisbanes.haze.HazeTint> 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();
Expand All @@ -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;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 39 additions & 79 deletions haze/src/androidMain/kotlin/dev/chrisbanes/haze/HazeNode.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<HazeTint>,
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<Int, Bitmap>(3)
Expand Down Expand Up @@ -165,28 +119,34 @@ private fun Brush.toShader(size: Size): Shader? = when (this) {
@RequiresApi(31)
private fun AndroidRenderEffect.withTints(
tints: List<HazeTint>,
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
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
19 changes: 6 additions & 13 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/Haze.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 7b7b171

Please sign in to comment.