diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index d61d054..1a24650 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,9 +5,6 @@ - - diff --git a/backdrop/src/main/java/com/kyant/backdrop/BackdropEffectScope.kt b/backdrop/src/main/java/com/kyant/backdrop/BackdropEffectScope.kt index ae1e758..518f42b 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/BackdropEffectScope.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/BackdropEffectScope.kt @@ -20,23 +20,21 @@ interface BackdropEffectScope : Density { var renderEffect: RenderEffect? - val cornerRadiusArray: FloatArray + val cornerRadii: FloatArray get() { val shape = shape - require(shape is CornerBasedShape) { "Lens effects only support CornerBasedShape" } + require(shape is CornerBasedShape) { "Only CornerBasedShape is supported." } val isLtr = layoutDirection == LayoutDirection.Ltr - val cornerRadius = - floatArrayOf( - if (isLtr) shape.topStart.toPx(size, this) - else shape.topEnd.toPx(size, this), - if (isLtr) shape.topEnd.toPx(size, this) - else shape.topStart.toPx(size, this), - if (isLtr) shape.bottomEnd.toPx(size, this) - else shape.bottomStart.toPx(size, this), - if (isLtr) shape.bottomStart.toPx(size, this) - else shape.bottomEnd.toPx(size, this) - ) - return cornerRadius + return floatArrayOf( + if (isLtr) shape.topStart.toPx(size, this) + else shape.topEnd.toPx(size, this), + if (isLtr) shape.topEnd.toPx(size, this) + else shape.topStart.toPx(size, this), + if (isLtr) shape.bottomEnd.toPx(size, this) + else shape.bottomStart.toPx(size, this), + if (isLtr) shape.bottomStart.toPx(size, this) + else shape.bottomEnd.toPx(size, this) + ) } @RequiresApi(Build.VERSION_CODES.TIRAMISU) diff --git a/backdrop/src/main/java/com/kyant/backdrop/LayerBackdrop.kt b/backdrop/src/main/java/com/kyant/backdrop/LayerBackdrop.kt index 8c3bf46..7f5972b 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/LayerBackdrop.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/LayerBackdrop.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.positionOnScreen +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.GlobalPositionAwareModifierNode import androidx.compose.ui.node.ModifierNodeElement @@ -48,7 +48,7 @@ class LayerBackdrop( var backdropPosition: Offset by mutableStateOf(Offset.Zero) override fun DrawScope.drawBackdrop(coordinates: LayoutCoordinates) { - val position = coordinates.positionOnScreen() - backdropPosition + val position = coordinates.positionInWindow() - backdropPosition translate(-position.x, -position.y) { if (backgroundColor != null && backgroundColor.isSpecified) { drawContext.canvas.nativeCanvas.drawColor(backgroundColor.toArgb()) @@ -106,7 +106,7 @@ private class LayerBackdropNode( override fun onGloballyPositioned(coordinates: LayoutCoordinates) { if (coordinates.isAttached) { - backdrop.backdropPosition = coordinates.positionOnScreen() + backdrop.backdropPosition = coordinates.positionInWindow() } } diff --git a/backdrop/src/main/java/com/kyant/backdrop/Shaders.kt b/backdrop/src/main/java/com/kyant/backdrop/Shaders.kt index 67a5ff3..2e45783 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/Shaders.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/Shaders.kt @@ -4,18 +4,18 @@ import org.intellij.lang.annotations.Language @Language("AGSL") private const val RoundedRectSDF = """ -float radiusAt(float2 coord, float4 radius) { +float radiusAt(float2 coord, float4 radii) { if (coord.x >= 0.0) { - if (coord.y <= 0.0) return radius.y; - else return radius.z; + if (coord.y <= 0.0) return radii.y; + else return radii.z; } else { - if (coord.y <= 0.0) return radius.x; - else return radius.w; + if (coord.y <= 0.0) return radii.x; + else return radii.w; } } -float sdRoundedRectangle(float2 coord, float2 halfSize, float4 radius) { - float r = radiusAt(coord, radius); +float sdRoundedRectangle(float2 coord, float2 halfSize, float4 radii) { + float r = radiusAt(coord, radii); float2 innerHalfSize = halfSize - float2(r); float2 cornerCoord = abs(coord) - innerHalfSize; @@ -24,8 +24,8 @@ float sdRoundedRectangle(float2 coord, float2 halfSize, float4 radius) { return outside + inside; } -float2 gradSdRoundedRectangle(float2 coord, float2 halfSize, float4 radius) { - float r = radiusAt(coord, radius); +float2 gradSdRoundedRectangle(float2 coord, float2 halfSize, float4 radii) { + float r = radiusAt(coord, radii); float2 innerHalfSize = halfSize - float2(r); float2 cornerCoord = abs(coord) - innerHalfSize; @@ -41,7 +41,7 @@ internal const val RefractionShaderString = """ uniform shader image; uniform float2 size; -uniform float4 cornerRadius; +uniform float4 cornerRadii; uniform float refractionHeight; uniform float refractionAmount; uniform float depthEffect; @@ -55,14 +55,14 @@ float circleMap(float x) { half4 main(float2 coord) { float2 halfSize = size * 0.5; float2 centeredCoord = coord - halfSize; - float sd = sdRoundedRectangle(centeredCoord, halfSize, cornerRadius); + float sd = sdRoundedRectangle(centeredCoord, halfSize, cornerRadii); if (-sd >= refractionHeight) { return image.eval(coord); } sd = min(sd, 0.0); float4 maxGradRadius = float4(min(halfSize.x, halfSize.y)); - float4 gradRadius = min(cornerRadius * 1.5, maxGradRadius); + float4 gradRadius = min(cornerRadii * 1.5, maxGradRadius); float2 normal = gradSdRoundedRectangle(centeredCoord, halfSize, gradRadius); float refractedDistance = circleMap(1.0 - -sd / refractionHeight) * refractionAmount; @@ -77,7 +77,7 @@ internal const val DispersionShaderString = """ uniform shader image; uniform float2 size; -uniform float4 cornerRadius; +uniform float4 cornerRadii; uniform float dispersionHeight; uniform float dispersionAmount; @@ -90,7 +90,7 @@ float circleMap(float x) { half4 main(float2 coord) { float2 halfSize = size * 0.5; float2 centeredCoord = coord - halfSize; - float sd = sdRoundedRectangle(centeredCoord, halfSize, cornerRadius); + float sd = sdRoundedRectangle(centeredCoord, halfSize, cornerRadii); if (-sd >= dispersionHeight) { return image.eval(coord); } @@ -103,7 +103,7 @@ half4 main(float2 coord) { } float4 maxGradRadius = float4(min(halfSize.x, halfSize.y)); - float4 gradRadius = min(cornerRadius * 1.5, maxGradRadius); + float4 gradRadius = min(cornerRadii * 1.5, maxGradRadius); float2 normal = gradSdRoundedRectangle(centeredCoord, halfSize, gradRadius); float2 tangent = float2(normal.y, -normal.x); diff --git a/backdrop/src/main/java/com/kyant/backdrop/effects/Lens.kt b/backdrop/src/main/java/com/kyant/backdrop/effects/Lens.kt index f093751..76f46b9 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/effects/Lens.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/effects/Lens.kt @@ -16,7 +16,7 @@ fun BackdropEffectScope.refraction( val shader = obtainRuntimeShader("Refraction", RefractionShaderString).apply { setFloatUniform("size", size.width, size.height) - setFloatUniform("cornerRadius", cornerRadiusArray) + setFloatUniform("cornerRadii", cornerRadii) setFloatUniform("refractionHeight", height) setFloatUniform("refractionAmount", -amount) setFloatUniform("depthEffect", if (hasDepthEffect) 1f else 0f) @@ -35,7 +35,7 @@ fun BackdropEffectScope.dispersion( val shader = obtainRuntimeShader("Dispersion", DispersionShaderString).apply { setFloatUniform("size", size.width, size.height) - setFloatUniform("cornerRadius", cornerRadiusArray) + setFloatUniform("cornerRadii", cornerRadii) setFloatUniform("dispersionHeight", height) setFloatUniform("dispersionAmount", amount) } @@ -53,18 +53,18 @@ fun BackdropEffectScope.refractionWithDispersion( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return if (height <= 0f || amount <= 0f) return - val cornerRadiusArray = cornerRadiusArray + val cornerRadii = cornerRadii val refractionShader = obtainRuntimeShader("Refraction", RefractionShaderString).apply { setFloatUniform("size", size.width, size.height) - setFloatUniform("cornerRadius", cornerRadiusArray) + setFloatUniform("cornerRadii", cornerRadii) setFloatUniform("refractionHeight", height) setFloatUniform("refractionAmount", -amount) setFloatUniform("depthEffect", if (hasDepthEffect) 1f else 0f) } val dispersionShader = obtainRuntimeShader("Dispersion", DispersionShaderString).apply { setFloatUniform("size", size.width, size.height) - setFloatUniform("cornerRadius", cornerRadiusArray) + setFloatUniform("cornerRadii", cornerRadii) setFloatUniform("dispersionHeight", height * dispersionIntensity) setFloatUniform("dispersionAmount", amount * dispersionIntensity) } diff --git a/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightModifier.kt b/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightModifier.kt index b551b8e..1ac4dd8 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightModifier.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightModifier.kt @@ -1,16 +1,22 @@ package com.kyant.backdrop.highlight +import android.graphics.Paint +import android.graphics.RectF +import android.os.Build import androidx.compose.ui.draw.CacheDrawModifierNode import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.GraphicsLayerScope -import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.asAndroidPath +import androidx.compose.ui.graphics.asComposePaint import androidx.compose.ui.graphics.asComposeRenderEffect -import androidx.compose.ui.graphics.drawOutline -import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.isUnspecified 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.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope @@ -71,6 +77,10 @@ internal class HighlightNode( override val shouldAutoInvalidate: Boolean = false private var graphicsLayer: GraphicsLayer? = null + private val paint = + Paint().apply { + style = Paint.Style.STROKE + } val drawNode = delegate(CacheDrawModifierNode { val highlight = highlight() @@ -82,27 +92,43 @@ internal class HighlightNode( } val size = size - val widthPx = highlight.width.toPx() - val strokeWidth = ceil(widthPx.fastCoerceAtMost(size.minDimension / 2f)) + val strokeWidth = ceil(highlight.width.toPx().fastCoerceAtMost(size.minDimension / 2f)) val borderSize = Size(size.width - strokeWidth, size.height - strokeWidth) val outline = shapeProvider.shape.createOutline(borderSize, layoutDirection, this) - graphicsLayer.blendMode = highlight.blendMode + paint.color = highlight.color.toArgb() + paint.strokeWidth = strokeWidth + paint.asComposePaint().blendMode = highlight.blendMode + graphicsLayer.blendMode = highlight.blendMode graphicsLayer.record { translate(strokeWidth / 2f, strokeWidth / 2f) { - drawOutline( - outline = outline, - brush = SolidColor(highlight.color), - style = Stroke(strokeWidth) - ) + val canvas = drawContext.canvas.nativeCanvas + + when (outline) { + is Outline.Rectangle -> { + val rect = RectF(0f, 0f, size.width, size.height) + canvas.drawRect(rect, paint) + } + + is Outline.Rounded -> { + val path = Path().apply { addRoundRect(outline.roundRect) }.asAndroidPath() + canvas.drawPath(path, paint) + } + + is Outline.Generic -> { + val path = outline.path.asAndroidPath() + canvas.drawPath(path, paint) + } + } } } onDrawWithContent { - val borderRenderEffect = highlight.style().createRenderEffect(size, strokeWidth) - graphicsLayer.renderEffect = borderRenderEffect?.asComposeRenderEffect() - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + graphicsLayer.renderEffect = + highlight.style().createRenderEffect(size, strokeWidth)?.asComposeRenderEffect() + } drawContent() drawLayer(graphicsLayer) } diff --git a/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightStyle.kt b/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightStyle.kt index a036f9f..8bad05a 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightStyle.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/highlight/HighlightStyle.kt @@ -5,6 +5,7 @@ import android.graphics.RuntimeShader import android.graphics.Shader import android.os.Build import androidx.annotation.FloatRange +import androidx.annotation.RequiresApi import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.geometry.Size @@ -14,6 +15,7 @@ import kotlin.math.PI @Immutable interface HighlightStyle { + @RequiresApi(Build.VERSION_CODES.S) fun createRenderEffect(size: Size, width: Float): RenderEffect? { return null } @@ -24,18 +26,15 @@ interface HighlightStyle { @Immutable data object Soft : HighlightStyle { + @RequiresApi(Build.VERSION_CODES.S) override fun createRenderEffect(size: Size, width: Float): RenderEffect? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val blurRadiusPx = width / 2f - if (blurRadiusPx > 0f) { - RenderEffect.createBlurEffect( - blurRadiusPx, - blurRadiusPx, - Shader.TileMode.DECAL - ) - } else { - null - } + val blurRadiusPx = width / 2f + return if (blurRadiusPx > 0f) { + RenderEffect.createBlurEffect( + blurRadiusPx, + blurRadiusPx, + Shader.TileMode.DECAL + ) } else { null } @@ -48,20 +47,21 @@ interface HighlightStyle { @param:FloatRange(from = 0.0) val falloff: Float = 1f ) : HighlightStyle { + @RequiresApi(Build.VERSION_CODES.S) override fun createRenderEffect(size: Size, width: Float): RenderEffect? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val blurRadiusPx = width / 2f - val blurRenderEffect = - if (blurRadiusPx > 0f) { - RenderEffect.createBlurEffect( - blurRadiusPx, - blurRadiusPx, - Shader.TileMode.DECAL - ) - } else { - null - } + val blurRadiusPx = width / 2f + val blurRenderEffect = + if (blurRadiusPx > 0f) { + RenderEffect.createBlurEffect( + blurRadiusPx, + blurRadiusPx, + Shader.TileMode.DECAL + ) + } else { + null + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val highlightRenderEffect = RenderEffect.createRuntimeShaderEffect( RuntimeShader(DynamicHighlightStyleShaderString).apply { @@ -73,15 +73,12 @@ interface HighlightStyle { ) if (blurRenderEffect != null) { - RenderEffect.createChainEffect( - highlightRenderEffect, - blurRenderEffect - ) + RenderEffect.createChainEffect(highlightRenderEffect, blurRenderEffect) } else { highlightRenderEffect } } else { - null + blurRenderEffect } } } diff --git a/backdrop/src/main/java/com/kyant/backdrop/shadow/Shadow.kt b/backdrop/src/main/java/com/kyant/backdrop/shadow/Shadow.kt index 447cc91..814949f 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/shadow/Shadow.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/shadow/Shadow.kt @@ -13,6 +13,5 @@ data class Shadow( val elevation: Dp = 24f.dp, val color: Color = Color.Black.copy(alpha = 0.15f), val offset: DpOffset = DpOffset(0f.dp, 4f.dp), - val alpha: Float = 1f, val blendMode: BlendMode = DrawScope.DefaultBlendMode ) diff --git a/backdrop/src/main/java/com/kyant/backdrop/shadow/ShadowModifier.kt b/backdrop/src/main/java/com/kyant/backdrop/shadow/ShadowModifier.kt index 1dae613..09f958f 100644 --- a/backdrop/src/main/java/com/kyant/backdrop/shadow/ShadowModifier.kt +++ b/backdrop/src/main/java/com/kyant/backdrop/shadow/ShadowModifier.kt @@ -1,16 +1,16 @@ package com.kyant.backdrop.shadow -import android.graphics.drawable.ShapeDrawable -import android.graphics.drawable.shapes.PathShape +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.os.Build import androidx.compose.ui.draw.CacheDrawModifierNode import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asAndroidPath import androidx.compose.ui.graphics.asComposePaint -import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.isUnspecified import androidx.compose.ui.graphics.layer.CompositingStrategy @@ -23,7 +23,6 @@ import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.requireGraphicsContext import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.util.fastCoerceIn import com.kyant.backdrop.BackdropShapeProvider import kotlin.math.ceil @@ -73,7 +72,15 @@ internal class ShadowNode( override val shouldAutoInvalidate: Boolean = false private var graphicsLayer: GraphicsLayer? = null - private val shapeDrawable = ShapeDrawable() + private val shadowPaint = Paint() + private val maskPaint = + Paint().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + blendMode = android.graphics.BlendMode.DST_OUT + } else { + xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + } + } val drawNode = delegate(CacheDrawModifierNode { val shadow = shadow() @@ -83,32 +90,11 @@ internal class ShadowNode( val size = size val outline = shapeProvider.shape.createOutline(size, layoutDirection, this) - val path = when (outline) { - is Outline.Rectangle -> Path().apply { addRect(outline.rect) } - is Outline.Rounded -> Path().apply { addRoundRect(outline.roundRect) } - is Outline.Generic -> outline.path - } - val elevation = shadow.elevation.toPx() val offset = Offset(shadow.offset.x.toPx(), shadow.offset.y.toPx()) - shapeDrawable.apply { - this.shape = - PathShape( - path.asAndroidPath(), - size.width, - size.height - ) - setBounds(0, 0, ceil(size.width).toInt(), ceil(size.height).toInt()) - paint.setShadowLayer( - elevation, - offset.x, - offset.y, - shadow.color.toArgb() - ) - paint.alpha = (shadow.alpha.fastCoerceIn(0f, 1f) * 255f + 0.5f).toInt() - paint.asComposePaint().blendMode = shadow.blendMode - } + shadowPaint.setShadowLayer(elevation, offset.x, offset.y, shadow.color.toArgb()) + shadowPaint.asComposePaint().blendMode = shadow.blendMode graphicsLayer?.record( density = this, @@ -118,14 +104,27 @@ internal class ShadowNode( ceil(size.height + elevation * 2).toInt() ) ) { - translate(elevation - offset.x, elevation - offset.y) { - shapeDrawable.draw(drawContext.canvas.nativeCanvas) + val canvas = drawContext.canvas.nativeCanvas + canvas.translate(elevation - offset.x, elevation - offset.y) - drawOutline( - outline = outline, - color = Color.Black, - blendMode = BlendMode.DstOut - ) + when (outline) { + is Outline.Rectangle -> { + val rect = RectF(0f, 0f, size.width, size.height) + canvas.drawRect(rect, shadowPaint) + canvas.drawRect(rect, maskPaint) + } + + is Outline.Rounded -> { + val path = Path().apply { addRoundRect(outline.roundRect) }.asAndroidPath() + canvas.drawPath(path, shadowPaint) + canvas.drawPath(path, maskPaint) + } + + is Outline.Generic -> { + val path = outline.path.asAndroidPath() + canvas.drawPath(path, shadowPaint) + canvas.drawPath(path, maskPaint) + } } } diff --git a/catalog/src/main/java/com/kyant/backdrop/catalog/destinations/DialogContent.kt b/catalog/src/main/java/com/kyant/backdrop/catalog/destinations/DialogContent.kt index 1f00e02..5c51148 100644 --- a/catalog/src/main/java/com/kyant/backdrop/catalog/destinations/DialogContent.kt +++ b/catalog/src/main/java/com/kyant/backdrop/catalog/destinations/DialogContent.kt @@ -38,6 +38,7 @@ import com.kyant.backdrop.effects.blur import com.kyant.backdrop.effects.colorFilter import com.kyant.backdrop.effects.refraction import com.kyant.backdrop.highlight.Highlight +import com.kyant.backdrop.highlight.HighlightStyle import com.kyant.backdrop.rememberLayerBackdrop import com.kyant.capsule.ContinuousCapsule import com.kyant.capsule.ContinuousRoundedRectangle @@ -77,11 +78,7 @@ fun DialogContent() { .drawBackdrop( backdrop, { ContinuousRoundedRectangle(48f.dp) }, - highlight = { - Highlight( - style = { com.kyant.backdrop.highlight.HighlightStyle.Soft } - ) - }, + highlight = { Highlight { HighlightStyle.Soft } }, onDrawSurface = { drawRect(containerColor) } ) { colorFilter(