Jetpack Compose中的阴影艺术

本文译自「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.dpspread = 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 的精彩内容。我会定期发布相关主题的文章。欢迎随时分享你的评论,或通过 BlueskyLinkedIn 与我联系,进行更深入的讨论。

祝你开心,编程快乐!

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
阿巴斯甜10 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker11 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952712 小时前
Andorid Google 登录接入文档
android
黄林晴13 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android