
在 Compose 1.9.0 中,新增了一套阴影 API,让我们可以更轻松地创建外阴影和内阴影。
本文将详细探究其工作方式,并展示几个实用的示例。
初尝
kotlin
Box(
modifier = Modifier
.size(160.dp)
.dropShadow(shape = RectangleShape) {
this.radius = 20f
}
.background("#fb2c36".color)
)
Box(
modifier = Modifier
.size(160.dp)
.background("#fb2c36".color)
.innerShadow(shape = RectangleShape) {
this.radius = 20f
}
)
要创建阴影,我们可以使用两个 modifier:dropShadow 和 innerShadow。
还是那句话,
modifier的调用顺序非常关键。通常需要先调用dropShadow,再调用background;而innerShadow则应该放在background之后,否则它会被背景色遮盖。
这两个 modifier 都通过 shape 和 block 两个参数来定义外观。shape 可以直接使用默认提供的形状,比如 RectangleShape、CircleShape 等,也可以传入自定义形状。
block 参数会提供一个 ShadowScope,让我们可以调整阴影的若干属性。
dropShadow提供的是DropShadowScope,而innerShadow提供的是InnerShadowScope。
我们先来看一下上面的效果:

题外话
为什么 dropShadow 不叫 outerShadow?与 innerShadow 相比,叫 outerShadow 岂不是更合理?
这里的 drop 不是「掉落、滴落」的意思,而是沿用了设计与图像软件里一个非常常见的固定说法:drop shadow(投影)。
在 Photoshop、Figma、CSS(filter: drop-shadow(...))等语境里,drop shadow 通常指给元素加一层偏在轮廓外侧、可带偏移和模糊的阴影。

dropShadow 就是在用同一套业界习惯命名,表示「向外投射在背后的那块阴影」,以便和 innerShadow(内阴影) 成对区分。
下面就逐个来看这几个属性,并对比它们对两类阴影分别会产生什么影响。
radius
这是一个 Float 值,用来控制阴影的模糊程度。
kotlin
this.radius = 50f

spread
它会改变阴影的初始大小。数值越大,阴影的覆盖面积也就越大。
kotlin
this.radius = 0f
this.spread = 20f

这里为了更好地区分 spread 和 radius 的效果,我将 radius 设为 0f,这样阴影就没有模糊效果了,呈现为一个纯色的实心阴影,以便直观展示 spread 的扩张作用。
这两有什么区别
看起来,这两个属性都会让阴影"变大",但它们的作用原理和视觉效果有着本质的区别。
简单来说:
spread(扩张/蔓延)改变的是阴影的"实体大小"。radius(模糊半径)改变的是阴影的"边缘柔和度",类似一种晕染。
offset
它可以让阴影相对源对象偏移若干像素。
kotlin
this.offset = Offset(10f, 20f)

alpha
它会改变阴影的不透明度。
kotlin
this.alpha = .5f

color
阴影默认是黑色,不过我们可以把它改成任意颜色:
kotlin
this.color = Color.Green

例如,我们可以将其改成绿色的阴影。
brush
除了纯色之外,还可以用它为阴影设置渐变。如果想让阴影强度呈现渐变变化,这会非常实用。
kotlin
this.brush = Brush.verticalGradient(
colors = listOf(Transparent, Color.Red)
)

blendMode
不同的混合模式会根据背景改变阴影的最终表现。比如在现实世界里,我们几乎看不到纯黑的阴影;阴影通常会根据其落在的表面,呈现为更深一层的颜色。
在应用里,我们也许会尝试通过降低透明度来模拟这种效果,但结果往往不够逼真。
举个例子,如果阴影落在一块高饱和颜色上,我们可以把混合模式设为 Overlay,这样阴影就会自然地呈现为下方底色更深、更暗的版本。
kotlin
this.blendMode = BlendMode.Overlay

两个重载函数
kotlin
fun Modifier.innerShadow(shape: Shape, shadow: Shadow): Modifier =
this then SimpleInnerShadowElement(shape, shadow)
@Stable
fun Modifier.innerShadow(shape: Shape, block: InnerShadowScope.() -> Unit): Modifier =
this then BlockInnerShadowElement(shape, block)
需要注意的是,除了传 block 参数外,也可以直接传入一个 Shadow 对象。
它包含同样的配置项,只不过 radius、spread 和 offset 采用的是 dp 而不是像素。这种方式更适合静态阴影;而在动画场景下,使用 block 能够避免在重组时发生大量状态读取,性能通常会更好。
实战一下
在这个 API 出现之前,你可能经常遇到这种情况:Figma 设计稿里给出了一套非常具体的阴影参数,但尝试使用旧版 shadow modifier 后,发现无论怎么调整 elevation,都达不到想要的效果。
现在有了新 modifier 提供的这些选项,我们终于可以把阴影精细地调到目标状态。
既然已经知道怎么创建阴影了,接下来就看看它在应用中有哪些实际用法。
快速发光
别忘了,阴影属性之一就是颜色。也就是说,这套 API 远不止能用来做阴影。只要给它一个足够明亮的颜色,就能营造出发光效果。再把外阴影和内阴影叠加起来,就能让它真正有"向外发散"的感觉。
kotlin
Box(
modifier = Modifier
.size(220.dp)
.dropShadow(shape = glowShape) {
this.radius = 60f
this.color = Color(0xFFEF4444)
this.brush = Brush.verticalGradient(
colors = listOf(Color(0xFF4ADE80), Color(0xFF38BDF8)),
)
}
.border(
width = 1.dp,
shape = glowShape,
brush = Brush.verticalGradient(
colors = listOf(Color(0xFFFEF08A), Color(0xFF38BDF8)),
),
)
.background(color = Zinc950, shape = glowShape)
.innerShadow(shape = glowShape) {
this.radius = 90f
this.color = Color(0xFFDC2626)
this.brush = Brush.verticalGradient(
colors = listOf(Color(0xFF4ADE80), Color(0xFF38BDF8)),
)
this.alpha = 0.4f
}
)
效果如下:

竟然有一种 Apple 的感觉。
拟物
现在,我们可以创建一个更加逼真的拟物效果了。
kotlin
Box(
modifier = Modifier
.size(220.dp)
.dropShadow(shape = neoShape) {
this.radius = 50f
brush = Brush.verticalGradient(
colors = listOf(Color.White, Color.White, Zinc950.copy(alpha = 0.2f)),
)
}
.border(
width = 2.dp,
shape = neoShape,
brush = Brush.verticalGradient(
colors = listOf(Color.White, Zinc950.copy(alpha = 0.3f)),
),
)
.background(color = Zinc300, shape = neoShape)
.innerShadow(shape = neoShape) {
this.radius = 90f
brush = Brush.verticalGradient(
colors = listOf(Color.White, Zinc950.copy(alpha = 0.2f)),
)
}
) {
Text("Apple", color = Color.White, fontSize = 48.sp, modifier = Modifier.align(Alignment.Center))
}

抱歉没忍住,真的太像了!
一点想法
Compose 1.9.0 引入的 dropShadow 和 innerShadow 彻底补齐了以前 modifier 在精细化 UI 表现上的短板。它不仅解决了以往开发者需要通过多层 Box 叠加、甚至自定义 DrawScope 才能实现复杂投影的问题,还在动画性能上提供了 block 方式的专属优化。
不论是新拟态、发光效果,还是满足设计师极其苛刻的阴影参数,这套新 API 都能优雅胜任。
不过,虽然 Compose 提供了如此强大的 API,但最终阴影效果到底"由谁来做"依然是一个需要团队内部商讨的问题。
在实际协作中,有的团队倾向于让设计师直接切图导出阴影,有的团队则完全交由开发通过代码实现。由研发用代码实现的好处显而易见:速度快、包体积小(不需要引入多余的图片资源),且更容易做动态交互和适配。
但代码实现的坏处在于,一旦视觉效果不对或者出现问题,就必须由开发介入去修改和重新编译。而如果采用切图,往往只需要设计师调整后,开发替换一下资源即可,责任边界更清晰。因此,如何在"极佳的运行时灵活性"与"更低的设计还原成本"之间取得平衡,仍需结合项目的实际情况来做取舍。