I'm using a graphics layer and off-screen composit...
# compose
r
I'm using a graphics layer and off-screen compositing to draw using blend modes (i.e. bitmap masking)
Copy code
.graphicsLayer {
  compositingStrategy = CompositingStrategy.Offscreen
}
However, when I set the graphics layer to CompositingStrategy.Offscreen, my drawing is clipped to the composable's rect. Even when clip = false is set on graphicsLayer. Does anyone know how use blend modes without this clipping?
r
If you need alpha blend modes like DstOut you can't
r
I do. In my case, I'm making a shadow that doesn't draw behind a given shape (so the shape can be semi-transparent). The clipping that's occurring ends up clipping the shadow. Is there a better approach for achieving this?
r
You'll need to make the layer big enough to contain the shadow. There are other ways to do this but they involve bitmaps and/or brushes
👍 1
💯 1
r
Ah, thanks for confirming! Any chance you could point me in the direction of what I'll need to look into?
r
Well it depends on how you draw your shadow, but you can — with Android APIs — create a
ComposeShader
that combines two shaders with a blend mode. You can then render your mask into a
Bitmap
and use that bitmap to mask the second shader (which for your shadow would be another
Bitmap
)
You’ll still have to create bitmaps appropriately sized though
c
@rob42 any chance you have an example of the final output you're trying to get to?
r
This is a compose multiplatform project, so it looks like that won't be available to me unfortunately! @Colton Idle here's the kind of effect -- a shadow that doesn't render inside the shape.
On iOS/AppKit I'd drop down to CoreGraphics and draw into the context with different blend modes to perform the cutout effect. I thought in compose, the equivalent would be to use drawBehind, but that's leading to the clipping problem
r
right because this effect needs an alpha channel to work
and the window is opaque
which is why you need a graphics layer
Android has an API to do this (unclipped saveLayer) but it’s expensive and now deprecated
j
Would the easiest solution be to make the desired translucent shape horizontally and vertically padded to make room for the desired shadow?
r
That's what I ended up doing. For future reference, here's how I implemented it:
Copy code
@Composable
fun Modifier.cutoutShadow(
    color: Color = Color.Black,
    shape: Shape,
    radius: Dp = 0.dp,
    offsetX: Dp = 0.dp,
    offsetY: Dp = 0.dp,
): Modifier {
    // Creates a container big enough to hold the shadow centered within it
    val padding = PaddingValues(
        horizontal = radius + abs(offsetX.value).dp,
        vertical = radius + abs(offsetY.value).dp
    )

    return outset(padding)
        .graphicsLayer {
            compositingStrategy = CompositingStrategy.Offscreen
        }
        .drawWithCache {
            // The canvas bounds were enlarged above (to make room for the shadow).
            // This effectively does the reverse to get back the desired size of the shape.
            val shapeSize = Size(
                width = size.width - (padding.calculateLeftPadding(layoutDirection) + padding.calculateRightPadding(layoutDirection)).toPx(),
                height = size.height - (padding.calculateTopPadding() + padding.calculateBottomPadding()).toPx()
            )

            // Offsets from top left corner of enlarged canvas
            val shapeOffset = Offset(
                x = padding.calculateLeftPadding(layoutDirection).toPx(),
                y = padding.calculateTopPadding().toPx()
            )
            val shadowOffset = Offset(
                x = shapeOffset.x + offsetX.toPx(),
                y = shapeOffset.y + offsetY.toPx(),
            )

            val outline = shape.createOutline(size = shapeSize, layoutDirection = layoutDirection, density = this)

            val paint = Paint().apply {
                val nativePaint = asFrameworkPaint()
                if (radius != 0.dp) {
                    nativePaint.maskFilter = MaskFilter.makeBlur(FilterBlurMode.NORMAL, radius.toPx() / 2, true)
                }
                nativePaint.color = color.toArgb()
            }

            onDrawBehind {
                drawIntoCanvas {
                    translate(left = shadowOffset.x, top = shadowOffset.y) {
                        it.drawOutline(outline = outline, paint = paint)
                    }
                }

                // Erase the shape from the drawn shadow
                translate(left = shapeOffset.x, top = shapeOffset.y) {
                    drawOutline(outline = outline, color = Color.Black, blendMode = BlendMode.Clear)
                }
            }
        }
        .padding(padding)
}

fun Modifier.outset(values: PaddingValues) = layout { measurable, constraints ->
    val wExtra = values.calculateLeftPadding(layoutDirection) + values.calculateRightPadding(layoutDirection)
    val hExtra = values.calculateTopPadding() + values.calculateBottomPadding()

    val placeable =  measurable.measure(constraints.offset(
        horizontal = (-values.calculateLeftPadding(layoutDirection)).roundToPx(),
        vertical = (-values.calculateTopPadding()).roundToPx()
    ))

    layout(
        width = placeable.width - wExtra.roundToPx(),
        height = placeable.height - hExtra.roundToPx()
    ) {
        placeable.place(
            x = -values.calculateLeftPadding(layoutDirection).roundToPx(),
            y = -values.calculateTopPadding().roundToPx(),
        )
    }
}

fun Modifier.outset(all: Dp) = outset(values = PaddingValues(all))
fun Modifier.outset(horizontal: Dp, vertical: Dp) =
    outset(values = PaddingValues(horizontal = horizontal, vertical = vertical))
fun Modifier.outset(start: Dp, top: Dp, end: Dp, bottom: Dp) =
    outset(values = PaddingValues(start = start, top = top, end = end, bottom = bottom))
👀 1
w
i publish a cmp library to solve this. https://github.com/adamglin0/compose-shadow
244 Views