在 Compose 中添加阴影

阴影可在视觉上提升界面,向用户指示互动性,并针对用户操作提供即时反馈。Compose 提供了多种将阴影纳入应用的方法:

  • Modifier.shadow():在符合 Material Design 指南的可组合项后面创建基于高度的阴影。
  • Modifier.dropShadow():创建可自定义的阴影,该阴影显示在可组合项后面,使其看起来具有立体感。
  • Modifier.innerShadow():在可组合项的边框内创建阴影,使其看起来像压入其后面的表面。

Modifier.shadow() 适用于创建基本阴影,而 dropShadowinnerShadow 修饰符可提供更精细的阴影渲染控制和精度。

本页介绍了如何实现这些修饰符,包括如何在用户互动时为阴影添加动画效果,以及如何链接 innerShadow()dropShadow() 修饰符来创建渐变阴影拟态阴影等。

创建基本阴影

Modifier.shadow() 会创建一个遵循 Material Design 准则的基本阴影,模拟来自上方的光源。阴影深度基于 elevation 值,投射的阴影会剪裁为可组合函数的形状。

@Composable
fun ElevationBasedShadow() {
    Box(
        modifier = Modifier.aspectRatio(1f).fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Box(
            Modifier
                .size(100.dp, 100.dp)
                .shadow(10.dp, RectangleShape)
                .background(Color.White)
        )
    }
}

白色矩形周围投射的灰色阴影。
图 1. 使用 Modifier.shadow 创建的基于高度的阴影。

实现阴影

使用 dropShadow() 修饰符可在内容后面绘制精确的阴影,使元素看起来具有立体感。

您可以通过其 Shadow 参数控制以下关键方面:

  • radius:定义模糊效果的柔和度和扩散度。
  • color:定义色调的颜色。
  • offset:沿 x 轴和 y 轴定位阴影的几何图形。
  • spread:控制阴影几何图形的扩展或收缩。

此外,shape 参数定义了阴影的整体形状。它可以采用 androidx.compose.foundation.shape 软件包中的任何几何图形,以及 Material Expressive 形状

如需实现基本的阴影效果,请在可组合项链上添加 dropShadow() 修饰符,并提供半径、颜色和扩散。请注意,显示在阴影顶部的 purpleColor 背景是在 dropShadow() 修饰符之后绘制的:

@Composable
fun SimpleDropShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(300.dp)
                .dropShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 6.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 4.dp, 4.dp)
                    )
                )
                .align(Alignment.Center)
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
        ) {
            Text(
                "Drop Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

代码要点

  • dropShadow() 修饰符应用于内部 Box。影子具有以下特征:
    • 圆角矩形形状 (RoundedCornerShape(20.dp))
    • 模糊半径为 10.dp,使边缘柔和而模糊
    • 扩展半径为 6.dp,可扩大阴影的大小,使其大于投射阴影的盒子
    • Alpha 值为 0.5f,使阴影呈半透明状态
  • 定义阴影后,.应用了 background() 调节系数。
    • Box填充了白色。
    • 背景会剪裁为与阴影相同的圆角矩形形状。

结果

白色矩形周围投射的灰色阴影。
图 2. 围绕形状绘制的阴影。

实现内阴影

如需创建与 dropShadow 相反的效果,请使用 Modifier.innerShadow(),该函数会营造出元素凹陷或按压到下方表面的错觉。

创建内阴影时,顺序非常重要。内阴影绘制在内容顶部,因此您通常应执行以下操作:

  1. 绘制背景内容。
  2. 应用 innerShadow() 修饰符可创建凹面外观。

如果 innerShadow() 放置在背景之前,则背景会绘制在阴影之上,从而完全隐藏阴影。

以下示例展示了 innerShadow()RoundedCornerShape 上的应用:

@Composable
fun SimpleInnerShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
                .innerShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 2.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 6.dp, 7.dp)
                    )
                )

        ) {
            Text(
                "Inner Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

白色矩形内的灰色内阴影。
图 3. Modifier.innerShadow() 在圆角矩形上的应用。

在用户互动时为阴影添加动画效果

如需让阴影响应用户互动,您可以将阴影属性与 Compose 的动画 API 集成。例如,当用户按下按钮时,阴影可以发生变化,从而提供即时视觉反馈。

以下代码通过阴影创建“按压”效果(表面被向下推入屏幕的错觉):

@Composable
fun AnimatedColoredShadows() {
    SnippetsTheme {
        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 = EaseInOut
            )

            // Animate all properties using the transition
            val shadowAlpha by transition.animateFloat(
                label = "shadow_alpha",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) 0f else 1f
            }
            // ...

            val blueDropShadow by transition.animateColor(
                label = "shadow_color",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) Color.Transparent else blueDropShadowColor
            }

            // ...

            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 = blueDropShadow,
                            offset = DpOffset(x = 0.dp, -(2).dp),
                            alpha = shadowAlpha
                        )
                    )
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = darkBlueDropShadow,
                            offset = DpOffset(x = 2.dp, 6.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 = innerShadowColor2,
                            offset = DpOffset(x = 4.dp, 0.dp)
                        )
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 20.dp,
                            spread = 4.dp,
                            color = innerShadowColor1,
                            offset = DpOffset(x = 4.dp, 0.dp),
                            alpha = innerShadowAlpha
                        )
                    )

            ) {
                Text(
                    "Animated Shadows",
                    // ...
                )
            }
        }
    }
}

代码要点

  • 使用 transition.animateColortransition.animateFloat 声明参数在按下时要实现的动画的起始状态和结束状态。
  • 使用 updateTransition 并为其提供所选的 targetState (targetState = isPressed),以验证所有动画是否同步。每当 isPressed 发生变化时,过渡对象都会自动管理所有子属性从当前值到新目标值的动画。
  • 定义 buttonPressAnimation 规范,用于控制过渡的时间和缓动效果。它指定了一个时长为 400 毫秒且具有 EaseInOut 曲线的 tween(中间阶段的简称),这意味着动画开始时速度较慢,中间阶段速度加快,结束时速度减慢。
  • 定义一个 Box,其中包含一系列修饰符函数,这些函数会应用所有动画属性来创建视觉元素,包括以下内容:
    • .clickable():一种使 Box 可交互的修饰符。
    • .dropShadow():首先应用两个外部阴影。它们的颜色和 alpha 属性与动画值(blueDropShadow 等)相关联,并创建初始凸起外观。
    • .innerShadow():在背景上绘制两个内阴影。 这些属性与其他动画值(innerShadowColor1 等)相关联,并创建缩进外观。

结果

图 4.在用户按压时添加动画效果的阴影。

创建渐变阴影

阴影不限于纯色。阴影 API 接受 Brush,可用于创建渐变阴影。

Box(
    modifier = Modifier
        .width(240.dp)
        .height(200.dp)
        .dropShadow(
            shape = RoundedCornerShape(70.dp),
            shadow = Shadow(
                radius = 10.dp,
                spread = animatedSpread.dp,
                brush = Brush.sweepGradient(
                    colors
                ),
                offset = DpOffset(x = 0.dp, y = 0.dp),
                alpha = animatedAlpha
            )
        )
        .clip(RoundedCornerShape(70.dp))
        .background(Color(0xEDFFFFFF)),
    contentAlignment = Alignment.Center
) {
    Text(
        text = breathingText,
        color = Color.Black,
        style = MaterialTheme.typography.bodyLarge
    )
}

代码要点

  • dropShadow() 会在盒子后面添加阴影。
  • brush = Brush.sweepGradient(colors) 使用在预定义 colors 列表中循环的渐变来为阴影着色,从而创建彩虹般的效果。

结果

您可以使用画笔作为阴影来创建具有“呼吸”动画效果的渐变 dropShadow()

图 5. 带动画效果的渐变阴影。

合并阴影

您可以组合并分层使用 dropShadow()innerShadow() 修饰符,以创建各种效果。以下部分将介绍如何使用此技巧生成拟态、新粗犷主义和逼真阴影。

创建拟态阴影

拟态阴影的特点是外观柔和,可自然地从背景中浮现出来。如需创建拟态阴影,请执行以下操作:

  1. 使用与背景颜色相同的元素。
  2. 应用两个浅色的相对阴影:一个角为浅色阴影,另一个对角为深色阴影。

以下代码段叠加了两个 dropShadow() 修饰符,以创建拟态效果:

@Composable
fun NeumorphicRaisedButton(
    shape: RoundedCornerShape = RoundedCornerShape(30.dp)
) {
    val bgColor = Color(0xFFe0e0e0)
    val lightShadow = Color(0xFFFFFFFF)
    val darkShadow = Color(0xFFb1b1b1)
    val upperOffset = -10.dp
    val lowerOffset = 10.dp
    val radius = 15.dp
    val spread = 0.dp
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(bgColor)
            .wrapContentSize(Alignment.Center)
            .size(240.dp)
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = lightShadow,
                    spread = spread,
                    offset = DpOffset(upperOffset, upperOffset)
                ),
            )
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = darkShadow,
                    spread = spread,
                    offset = DpOffset(lowerOffset, lowerOffset)
                ),

            )
            .background(bgColor, shape)
    )
}

白色背景上具有拟态效果的白色矩形。
图 6. 一种拟态阴影效果。

创建新粗犷主义风格的阴影

新粗犷主义风格采用高对比度、块状布局、鲜艳色彩和粗边框。如需创建此效果,请使用模糊度为零且偏移量明显的 dropShadow(),如以下代码段所示:

@Composable
fun NeoBrutalShadows() {
    SnippetsTheme {
        val dropShadowColor = Color(0xFF007AFF)
        val borderColor = Color(0xFFFF2D55)
        Box(Modifier.fillMaxSize()) {
            Box(
                Modifier
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .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)
                    )
            ) {
                Text(
                    "Neobrutal Shadows",
                    modifier = Modifier.align(Alignment.Center),
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

黄色背景上,一个白色矩形周围有红色边框,并带有蓝色阴影。
图 7. 一种新粗犷主义风格的阴影效果。

创建逼真的阴影

逼真的阴影可模拟现实世界中的阴影,它们看起来像是被主光源照亮,从而产生直射阴影和更漫射的阴影。您可以堆叠多个具有不同属性的 dropShadow()innerShadow() 实例,以重现逼真的阴影效果,如以下代码段所示:

@Composable
fun RealisticShadows() {
    Box(Modifier.fillMaxSize()) {
        val dropShadowColor1 = Color(0xB3000000)
        val dropShadowColor2 = Color(0x66000000)

        val innerShadowColor1 = Color(0xCC000000)
        val innerShadowColor2 = Color(0xFF050505)
        val innerShadowColor3 = Color(0x40FFFFFF)
        val innerShadowColor4 = Color(0x1A050505)
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 40.dp,
                        spread = 0.dp,
                        color = dropShadowColor1,
                        offset = DpOffset(x = 2.dp, 8.dp)
                    )
                )
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 0.dp,
                        color = dropShadowColor2,
                        offset = DpOffset(x = 0.dp, 4.dp)
                    )
                )
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.Black,
                    shape = RoundedCornerShape(100.dp)
                )
// //
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 3.dp,
                        color = innerShadowColor1,
                        offset = DpOffset(x = 6.dp, 6.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 1.dp,
                        color = Color.White,
                        offset = DpOffset(x = 5.dp, 5.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 5.dp,
                        color = innerShadowColor2,
                        offset = DpOffset(x = (-3).dp, (-12).dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 10.dp,
                        color = innerShadowColor3,
                        offset = DpOffset(x = 0.dp, 0.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 9.dp,
                        color = innerShadowColor4,
                        offset = DpOffset(x = 1.dp, 1.dp)
                    )
                )

        ) {
            Text(
                "Realistic Shadows",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 24.sp,
                color = Color.White
            )
        }
    }
}

代码要点

  • 应用两个具有不同属性的链式 dropShadow() 修饰符,然后应用一个 background 修饰符。
  • 通过链接 innerShadow() 修饰符,在组件边缘打造金属边框效果。

结果

上述代码段会生成以下内容:

黑色圆角形状周围的白色逼真阴影。
图 8. 逼真的阴影效果。