If you didn't know already, there's a defect with Android's material shadows,
the ones that came with Material Design and its concepts of surfaces, lighting,
and elevation. Also, if you didn't know, Compose utilizes many of the same
graphics APIs as the View framework, including those responsible for said
shadows, so it has the same glitch that Views do, at least for now.
That's a FloatingActionButton, an ExtendedFloatingActionButton, and a Card
with translucent backgrounds, showing the defect.
For reasons I won't get into here,* I don't believe that there is any proper fix for this – i.e., I don't think that the platform offers any method or configuration by which to clip out or otherwise remove that artifact – so we're left with workarounds. Additionally, a main requirement is that the shadows appear exactly as the platform ones normally would, so any method that draws shadows with other techniques, like a uniform gradient or blur or whatnot, are not acceptable.
Given that, can we create a robust, generally applicable solution in Compose?
I personally landed on an overall approach of disabling the original shadow and drawing a clipped replica in its place. (I realize that simply punching a hole through it is not how shadows work realistically, but that seems to be the predominately expected effect.) I've shared an example of the Compose version of this in an answer below, but the primary motivation for this question was to check for better ideas before this is put into a library.
I'm sure that there are technical details in my example that can be improved,
but I'm mainly curious about fundamentally different approaches or suggestions.
I'm not interested in, for instance, somehow using drawBehind() or Canvas
instead to do essentially the same thing, or refactoring parameters just to slot
the content in, etc. I'm thinking more along the lines of:
Can you devise some other (more performant) way to trim that artifact without creating and clipping a separate shadow object? With
Views, about the only way I found was to draw theViewtwice, with the content clipped in one draw and the shadow disabled in the other. I eventually decided against that, though, given the overhead.Can this be extracted to a
Modifierand extension, similar to the*GraphicsLayerModifiers andshadow()/graphicsLayer()? I've not yet fully wrapped my head around all of Compose's concepts and capabilities, but I don't think so.Is there any other way to make this generally applicable, without requiring additional wiring? The shadow object in my example depends on three optional parameters with defaults from the target composable, and I can't think of any way to get at those, apart from wrapping the target with another composable.
* Those reasons are outlined in my question here.
Clarifications:
It is necessary to preserve the
Composable's translucency/transparency, so we can still see what's behind it. Setting an opaque background color will not work here. The artifact needs to be fixed, not just covered over.If an opaque color would work for your particular setup – e.g, if the underlying content is a single solid color – you can simply use the
Color#compositeOver()function to figure the opaque color that would result from your translucent one blending with the color behind it.This question is asking specifically about material shadows, the ones that are cast by
Views/RenderNodes, and that cooperate with the on-screen draw routine and its two-source lighting model.android.graphics.Paint's shadow layer is a separate mechanism that does not interact with that model, and therefore is not generating material shadows. It's just drawing a static, uniform gradient, which is noted above as being unacceptable here because it looks completely different than the material shadows.If
Paint's shadow layer would be suitable for your particular setup, there are many examples already available on-site, like those on this question. There's no need to repeat them here.


Modifier.dropShadow()in the same manner, I've a couple quick examples in this gist. They useModifier.composed()so they're not as optimized as they could be, but they're probably fine for most general use cases. I may eventually put togetherModifier.Nodeversions, but that should probably be a whole new post.