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 与我联系,进行更深入的讨论。

祝你开心,编程快乐!

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

保护原创,请勿转载!

相关推荐
百***61874 小时前
Spring的构造注入
android·java·spring
Tom4i4 小时前
Kotlin 中的 inline 和 reified 关键字
android·开发语言·kotlin
yi诺千金5 小时前
Android U 自由窗口(浮窗)——启动流程(system_server侧流程)
android
清空mega8 小时前
第11章 网络编程
android·网络
自动化BUG制造器8 小时前
Android UI 线程不一定是主线程
android
无知的前端8 小时前
一文读懂-Jetpack与AndroidX
android·kotlin·android jetpack
河铃旅鹿9 小时前
Android开发-java版:SQLite数据库
android·数据库·笔记·学习·sqlite
旋律逍遥10 小时前
《Framework 开发》3、开发工具及命令行知识装备
android
啦啦91171410 小时前
安卓手机/平板/TV版 Rotation强制横屏显示工具!免ROOT可用!再推荐突破手机限制的3款神器
android·智能手机·电脑