本文译自「The Art of Shadows in Jetpack Compose」,原文链接medium.com/proandroidd...,由Stefano Natali发布于2025年10月4日。

UI中的阴影发挥着至关重要的作用:它们在视觉上提升 元素,指示着交互性 ,并提供用户操作的即时反馈 。多年来,我们一直依赖于高度 属性,但 Jetpack Compose 现在提供了一套强大的工具,可以对阴影渲染进行精细控制。
Google 最近新增了一个文档页面,其中包含一系列有趣的用例。本文将探索 Compose 中的主要阴影修改器,并深入讲解创建渐变和炫酷特效等高级技巧,从而提升应用的风格。
本文讨论的所有技巧的完整代码示例都可以在我的 GitHub 仓库 Compose Playground 中找到。
阴影修改器(Shadow Modifiers)
添加阴影最简单但自定义程度最低的方法是使用 Modifier.shadow() 。与以往一样,它的行为依赖于基于高度的阴影,模拟来自上方的光源,阴影深度直接取决于你提供的高度值。其主要限制在于阴影始终被裁剪到可合成对象的形状内,并且你无法自定义扩展、颜色色调或偏移等属性。
kotlin
Box(
Modifier
.size(100.dp)
.shadow(
elevation = 10.dp,
shape = RectangleShape
)
.background(Color.White)
)

让我们从一些新功能开始,这些功能赋予我们更强大的力量。使用 dropShadow(),我们可以在内容后面创建自定义阴影。这个修饰符是创建复杂阴影的关键,它允许对内容后面的阴影进行精细控制,使元素看起来像是被抬升了一样。
kotlin
.dropShadow(
shape = RoundedCornerShape(20.dp), shadow = Shadow(
radius = 6.dp,
spread = 2.dp,
color = Color.Black.copy(alpha = 0.3f),
offset = DpOffset(2.dp, 2.dp)
)
)
.background(
color = Color.White, shape = RoundedCornerShape(20.dp)
)

它使用 Shadow 参数,可以精确控制视觉效果。这个参数允许你使用几个关键属性来塑造阴影。 半径 决定了边缘的柔和度和扩散(模糊)程度。扩散 值控制阴影几何形状相对于可组合元素大小的扩展或收缩。最后,偏移 沿 X 轴和 Y 轴 定位阴影,从而确定光源的视觉方向。结合阴影的颜色,这些属性可以实现完全自定义。
💡 顺序很重要:在修改器链中,dropShadow() 修改器必须出现在 background() 修改器之前,因为阴影先绘制,背景绘制在其上方。
innerShadow() 修改器是 dropShadow() 的逆操作,它在可组合元素边界的内部 创建阴影,从而实现元素凹陷或压入表面的视觉效果。与它的对应项一样,它也使用可自定义的Shadow 对象,允许你使用半径 、颜色 、偏移 和扩散来微调效果。
kotlin
.background(
Color.White, shape = RoundedCornerShape(20.dp)
)
.innerShadow(
shape = RoundedCornerShape(20.dp), shadow = Shadow(
radius = 6.dp,
spread = 2.dp,
color = Color.Black.copy(alpha = 0.3f),
offset = DpOffset(2.dp, 2.dp)
)
)

💡再次强调,顺序很重要:****innerShadow()修改器必须 放在background()修改器之后。如果将其放在背景之前,内容将绘制在阴影之上,完全遮盖阴影。
这些修改器的真正强大之处在于它们的自定义和组合。通过叠加innerShadow()和dropShadow(),你可以创建复杂而逼真的视觉效果。我们来看一些高级用例。
高级技巧:组合和自定义阴影
自定义阴影
当你在 dropShadow() 函数中将 Brush 对象而非纯色传递给 Shadow 对象时,自定义的强大功能便显而易见。此功能允许你使用 Brush.sweepGradient 创建渐变效果。此外,无论你使用标准形状还是完全自定义的几何体,阴影始终会贴合可组合对象的形状。
kotlin
.dropShadow(
shape = MaterialShapes.Cookie12Sided.toShape(), shadow = Shadow(
radius = 10.dp,
spread = 6.dp,
brush = Brush.sweepGradient(
listOf(Color.Green, Color.Blue, Color.Yellow, Color.Green)
),
offset = DpOffset(2.dp, 2.dp)
)
)
.background(
color = Color.White,
shape = MaterialShapes.Cookie12Sided.toShape(),
)

新粗犷主义阴影
这种风格完美地展现了 dropShadow() 修改器在实现极致高对比度效果方面的强大功能。要实现新粗犷主义(Neobrutalism)那种粗犷、棱角分明的视觉效果,你必须使用 dropShadow() 函数,并设置模糊度为零,偏移量要明显,通常还要配合粗边框。具体来说,你需要同时设置 radius = 0.dp 和 spread = 0.dp 来消除扩散,然后应用鲜艳的色彩,并设置一个明显的偏移量,从而创建出标志性的锐利轮廓。
kotlin
.dropShadow(
shape = RoundedCornerShape(0.dp),
shadow = Shadow(
radius = 0.dp,
spread = 0.dp,
color = dropShadowColor,
offset = DpOffset(x = 8.dp, 8.dp)
)
)
.border(
8.dp, borderColor
)
.background(
color = Color.White,
shape = RoundedCornerShape(0.dp)
)

具有动效的阴影
阴影并非只是静态的;它们可以制作成动画,从而提供即时的用户反馈。我们将要探索的最后一个高级技巧是创建交互式阴影,使其在特定操作(例如点击或按下)时平滑过渡。这项技术对于模拟物理交互至关重要,它能直观地确认元素的抬升或状态发生了变化。
kotlin
Box(Modifier.fillMaxSize()) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
// Create transition with pressed state
val transition = updateTransition(
targetState = isPressed, label = "button_press_transition"
)
fun <T> buttonPressAnimation() = tween<T>(
durationMillis = 400, easing = Ease
)
// Animate all properties using the transition
val shadowAlpha by transition.animateFloat(
label = "shadow_alpha", transitionSpec = { buttonPressAnimation() }) { pressed ->
if (pressed) 0f else 1f
}
//to animate the color
val colorDropShadow by transition.animateColor(
label = "shadow_color", transitionSpec = { buttonPressAnimation() }) { pressed ->
if (pressed) Color.Transparent else Color.Green.copy(alpha = (0.5f))
}
val innerShadowAlpha by transition.animateFloat(
label = "inner_shadow_alpha", transitionSpec = { buttonPressAnimation() }) { pressed ->
if (!pressed) 0f else 1f
}
Box(
Modifier
.clickable(
interactionSource, indication = null
) {
//...
}
.width(300.dp)
.height(200.dp)
.align(Alignment.Center)
.dropShadow(
shape = RoundedCornerShape(70.dp), shadow = Shadow(
radius = 10.dp,
spread = 0.dp,
color = Color.Green.copy(alpha = (0.5f)),
offset = DpOffset(x = 0.dp, 0.dp),
alpha = shadowAlpha
)
)
// note that the background needs to be defined before defining the inner shadow
.background(
color = Color(0xFFFFFFFF), shape = RoundedCornerShape(70.dp)
)
.innerShadow(
shape = RoundedCornerShape(70.dp), shadow = Shadow(
radius = 8.dp,
spread = 4.dp,
color = Color.Green.copy(alpha = (0.5f)),
alpha = innerShadowAlpha,
offset = DpOffset(x = 0.dp, 0.dp)
)
)
) {
//...
}
}

好的,我们已经了解了如何使用 updateTransition API 来创建流畅的交互反馈。它首先使用 MutableInteractionSource 跟踪按下状态。该状态会驱动一个 400 毫秒的过渡动画 ,同时为两个方向相反的阴影添加动画效果。外部的 dropShadow 在按下时会淡出,而内部的 innerShadow 则会淡入。这种双阴影动画使组件从 抬升 状态平滑过渡到 凹陷 状态,从而在用户交互时提供清晰且动态的反馈。
结论
Compose 全新的阴影 API 标志着对传统 elevation 属性局限性的重大突破。它将核心概念拆分为 dropShadow() 和 innerShadow() ,并赋予我们对 Shadow 对象属性的完全控制权。
无论你是打造简单的渐变效果、模拟物理深度,还是实现新粗野主义的高对比度视觉冲击,这种全新的自定义程度都意味着你的 UI 终于可以充分展现你设计的艺术愿景。
不妨尝试这些修饰符,为你的 Jetpack Compose 应用注入全新的创意维度!
如果你觉得这篇文章有趣,欢迎关注我,获取更多关于 Android 开发和 Jetpack Compose 的精彩内容。我会定期发布相关主题的文章。欢迎随时分享你的评论,或通过 Bluesky 或 LinkedIn 与我联系,进行更深入的讨论。
祝你开心,编程快乐!
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!