我再也不用求设计做阴影了 — Compose 阴影

在 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
        }
)

要创建阴影,我们可以使用两个 modifierdropShadowinnerShadow

还是那句话,modifier 的调用顺序非常关键。通常需要先调用 dropShadow,再调用 background;而 innerShadow 则应该放在 background 之后,否则它会被背景色遮盖。

这两个 modifier 都通过 shapeblock 两个参数来定义外观。shape 可以直接使用默认提供的形状,比如 RectangleShapeCircleShape 等,也可以传入自定义形状。

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 

这里为了更好地区分 spreadradius 的效果,我将 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 对象。

它包含同样的配置项,只不过 radiusspreadoffset 采用的是 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 引入的 dropShadowinnerShadow 彻底补齐了以前 modifier 在精细化 UI 表现上的短板。它不仅解决了以往开发者需要通过多层 Box 叠加、甚至自定义 DrawScope 才能实现复杂投影的问题,还在动画性能上提供了 block 方式的专属优化。

不论是新拟态、发光效果,还是满足设计师极其苛刻的阴影参数,这套新 API 都能优雅胜任。

不过,虽然 Compose 提供了如此强大的 API,但最终阴影效果到底"由谁来做"依然是一个需要团队内部商讨的问题。

在实际协作中,有的团队倾向于让设计师直接切图导出阴影,有的团队则完全交由开发通过代码实现。由研发用代码实现的好处显而易见:速度快、包体积小(不需要引入多余的图片资源),且更容易做动态交互和适配。

但代码实现的坏处在于,一旦视觉效果不对或者出现问题,就必须由开发介入去修改和重新编译。而如果采用切图,往往只需要设计师调整后,开发替换一下资源即可,责任边界更清晰。因此,如何在"极佳的运行时灵活性"与"更低的设计还原成本"之间取得平衡,仍需结合项目的实际情况来做取舍。

相关推荐
Digitally2 小时前
6 种简单方法:在 Mac 电脑与安卓手机之间传输文件
android
鹏程十八少2 小时前
3. 2026金三银四 Android 背完这 23 道题,Android 线程面试横着走
android·面试·前端框架
冬奇Lab12 小时前
Android 开发要变天了:Google 专为 Agent 重建工具链,Token 减少 70%、速度提升 3 倍
android·人工智能·ai编程
imuliuliang15 小时前
存储过程(SQL)
android·数据库·sql
AgCl2316 小时前
MYSQL-6-函数与约束-3/17
android·数据库·mysql
zzb158017 小时前
Fragment 生命周期深度图解:从 onAttach 到 onDetach 完整流程(面试必备)
android·java·面试·安卓
众少成多积小致巨17 小时前
Android 源码查看笔记
android·源码
angerdream17 小时前
Android手把手编写儿童手机远程监控App之前台服务
android
敲代码的瓦龙19 小时前
Android?Activity!!!
android