我们将在这个本地示例中使用FloatingActionButton(),因为它支持我们需要考虑的几乎所有选项,但这应该适用于任何 Composable。为方便起见,如果您想尝试除 FloatingActionButton() 之外的其他方法,我已经使用此解决方案包装了几个更常见的并将它们组装在 this GitHub gist 中。
我们希望我们的包装器 Composable 充当替代品,因此它的参数列表和默认值完全从 FloatingActionButton() 复制:
@Composable
fun ClippedShadowFloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit
) {
Layout(
{
ClippedShadow(
elevation = elevation.elevation(interactionSource).value,
shape = shape,
modifier = modifier
)
FloatingActionButton(
onClick = onClick,
modifier = modifier,
interactionSource = interactionSource,
shape = shape,
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
content = content
)
},
modifier
) { measurables, constraints ->
require(measurables.size == 2)
val shadow = measurables[0]
val target = measurables[1]
val targetPlaceable = target.measure(constraints)
val width = targetPlaceable.width
val height = targetPlaceable.height
val shadowPlaceable = shadow.measure(Constraints.fixed(width, height))
layout(width, height) {
shadowPlaceable.place(0, 0)
targetPlaceable.place(0, 0)
}
}
}
我们基本上将 FloatingActionButton() 和我们的副本影子 Composable 包装在为设置优化的 Layout() 中。除了elevation 之外,大多数参数都原封不动地传递给包装的FloatingActionButton(),我们将其归零以禁用固有阴影。相反,我们将适当的原始高程值指向我们的ClippedShadow(),这里使用FloatingActionButtonElevation 和InteractionSource 参数计算。像Card() 这样更简单的可组合项将在Dp 中具有可以直接传递的无状态海拔值。
ClippedShadow() 本身是另一个自定义的Layout(),但没有内容:
@Composable
fun ClippedShadow(elevation: Dp, shape: Shape, modifier: Modifier = Modifier) {
Layout(
modifier
.drawWithCache {
// Naive cache setup similar to foundation's Background.
val path = Path()
var lastSize: Size? = null
fun updatePathIfNeeded() {
if (size != lastSize) {
path.reset()
path.addOutline(
shape.createOutline(size, layoutDirection, this)
)
lastSize = size
}
}
onDrawWithContent {
updatePathIfNeeded()
clipPath(path, ClipOp.Difference) {
this@onDrawWithContent.drawContent()
}
}
}
.shadow(elevation, shape)
) { _, constraints ->
layout(constraints.minWidth, constraints.minHeight) {}
}
}
我们只需要它的影子和Canvas 访问,我们通过两个简单的Modifier 扩展来获得。 drawWithCache() 让我们保留一个简单的 Path 缓存,用于剪辑和恢复整个内容绘制,shadow() 是不言自明的。将此 Composable 分层放置在目标后面,其自身的阴影被禁用,我们得到了想要的效果:
与问题一样,前三个是Card()、FloatingActionButton() 和ExtendedFloatingActionButton(),但包含在我们的修复中。为了证明InteractionSource/elevation 重定向按预期工作,this brief gif 并排显示两个具有完全透明背景的FloatingActionButton()s;右边的那个应用了我们的修复。
对于上面修复图像中的第四个示例,我们使用了单独的 ClippedShadow(),只是为了说明它也可以单独工作:
ClippedShadow(
elevation = 10.dp,
shape = RoundedCornerShape(10.dp),
modifier = Modifier.size(FabSize)
)
就像常规的可组合项一样,它应该适用于任何对当前 API 级别有效的Shape。任意凸形Paths 适用于所有相关版本,从 API 级别 29 (Q) 开始,凹形也可以。