【用案例学Material 3 Expressive】第1课:从一封邮件开始,感受安卓设计新语言的表现力

引语

"美的东西更好用。" ------ 唐·诺曼《情感化设计》
这句看似简单的话,道出了 Material 3 Expressive 的核心信念:好的设计不只是让人"能用",更要让人"想用"。这一课,我们从一封邮件开始,看看 Material 3 Expressive 如何在秩序之上,注入情感与生命力。

你有没有过这样的经历?

打开Android Studio,新建一个项目,然后面对一堆XML布局文件和主题配置发呆。颜色要手动定义,按钮样式要自己写drawable,深色模式要单独维护一套资源文件,更别提不同屏幕尺寸的适配了......那种感觉,就像用螺丝刀组装宜家家具------能装好,但过程实在谈不上愉快。

然后Material Design 3来了。它带着一个有点浪漫的名字------"Material You",试图回答一个朴素的问题:为什么你的手机界面不能像你的穿搭一样,有自己的风格?

而到了2025年,Google在I/O大会上正式发布了Material 3 Expressive------这是Material Design有史以来最大的一次更新,建立在此前Material You的基础上,进一步强化了情感化设计和表现力。Google为此开展了46场全球研究,测试数百种设计变体,收集了18000名参与者的反馈,结果显示表现力强的界面在趣味性、创造力、活力和友好度上全面优于传统设计。

这一课,我们不从抽象的设计原则讲起,而是从一封邮件开始。Google官方提供了一个非常经典的Material 3示例应用------Reply。它是一个邮件客户端,包含邮件列表页和详情页,麻雀虽小五脏俱全。我们通过它来认识Material 3 Expressive最核心的几个子系统:颜色、排版、形状、动效、组件和高度。而且,这一课里出现的每一行示例代码,我都会加上逐行详细注解,让你不仅知道"怎么写",更知道"为什么这么写"。

准备好了吗?让我们从一封邮件开始。

一、先看目标:Reply邮件应用长什么样

在动手写代码之前,我们先在脑子里"跑"一遍这个应用。

打开Reply,首页是邮件列表。每一封邮件是一张卡片,卡片里有发件人头像(圆形,带首字母)、姓名、邮件主题和正文预览。整体感觉是清爽、有层次,但又不花哨。

点进一封邮件,进入详情页。顶部是发件人和主题,中间是正文,底部有一个醒目的"回复"按钮。右上角还有一个悬浮操作按钮(FAB),用来写新邮件。

底部导航栏可以在"收件箱"和"已加星标"之间切换。切换时,图标的填充状态会变化,文字颜色也会跟着变。

而在Material 3 Expressive的加持下,这个应用会变得更"有生命"------按钮按下时会弹跳,卡片切换时会流动,导航栏选中时会形状变形。这些都不是花哨的装饰,而是有目的的情感表达。

这个应用不大,但颜色系统、排版系统、形状系统、动效系统、按钮、卡片、导航栏、图标、高度,全都用上了。我们这一课的目标,就是把这些东西一个一个拆开,再组装回去。

二、依赖:只加一行,但要知道为什么

在build.gradle.kts文件中,你需要加入Material 3的依赖。代码只有一行,但我们把每个部分都解释清楚:

kotlin 复制代码
implementation("androidx.compose.material3:material3:$material3_version") // 引入 Material 3 组件库;冒号前是组坐标,冒号后是模块名,$material3_version 是版本变量,方便统一升级

这一行代码看起来简单,但它背后是一整套设计系统。加上它,你就获得了MaterialTheme、ColorScheme、Typography、Shapes,以及Button、Card、NavigationBar等所有Material 3组件。从Material 3 1.4.0版本开始,Expressive的新组件(如ButtonGroup、FAB Menu、LoadingIndicator等)也已包含在内。有些API被标记为实验性,使用它们时需要添加@ExperimentalMaterial3ExpressiveApi注解。这不是坏事,只是说明Google还在打磨细节。

三、Material 3 Expressive:从"一致"到"共情"

在深入代码之前,我们需要先理解Material 3 Expressive和它之前版本的根本区别。

Material Design 2的设计哲学可以用一个词概括:一致性。Google希望所有安卓应用看起来像一家人,统一的颜色、统一的组件、统一的交互模式。这在当时是合理的------安卓生态的碎片化问题太严重了,需要一套强力的规范来收拢。

Material 3带来了"Material You"理念,追求**"每个应用看起来都像你"**。动态颜色让界面适应用户的壁纸,个性化主题成为可能。

而Material 3 Expressive,则把目标又推进了一步:让界面不仅像你,还能打动你。正如Google所说,"人们越来越把设备视为自身的延伸。表现力强的UI能够产生情感影响,通过唤起更强烈的感觉或情绪来培养这种连接"。

这个理念落地为几个核心方向:

情感化设计------界面不只是功能性的,它应该让人产生情感共鸣。Expressive的动效系统使用弹簧物理(stiffness、damping、velocity)取代传统的贝塞尔曲线,让交互感觉"活"起来。

动态优先------Google的研究表明,遵循Material 3 Expressive指南设计的产品,用户定位正确屏幕元素的速度快了33%,任务完成速度快了20%。

表现力组件------15个新增或更新的组件带来了更多的尺寸、形状、功能和视觉强调选项。

扩展的形状和排版------35种新形状选项,加上Emphasized排版样式,让界面可以表达更丰富的情感状态。

在Material 3 Expressive中,动效和形状的变革尤为突出。接下来,我们先用两章的篇幅把颜色和排版的基础打好,然后用两章的篇幅深入这两个核心变革。

四、颜色系统:不是选几个好看的颜色那么简单

4.1 五组色调,十三个亮度

Material 3的颜色系统是整套设计语言中最复杂的部分,也是最值得花时间理解的部分。

传统做法是:选一个品牌色,选一个辅助色,选一个强调色,然后手动确保它们在浅色和深色模式下都有足够的对比度。这个过程既费时又容易出错。

Material 3的做法完全不同。它的起点是五种关键颜色 :Primary(主色)、Secondary(辅助色)、Tertiary(三级色)、Neutral(中性色)和Neutral Variant(中性变体色)。每一种颜色都会生成一个包含13个色调的调色板,从0(纯黑)到100(纯白),中间以10为步长分布。

这意味着什么?意味着你不需要手动为"按钮在浅色模式下的背景色"和"按钮在深色模式下的背景色"分别选颜色。系统会根据色调值自动计算:浅色主题下,Primary容器使用色调90,文字使用色调10;深色主题下则反过来。色调系统保证了对比度和可读性的一致性。

在Material 3 Expressive中,动态颜色主题进一步扩展,支持更丰富的个性化调色。M3 Expressive还引入了三个标准化的对比度级别------标准、中等和高,用户可以根据自己的可读性需求进行调整,系统会动态重新计算所有颜色角色的色调值,确保对比度始终合规。

4.2 颜色角色:每个颜色都有"职位"

色调调色板只是原料,真正让颜色系统运转起来的是颜色角色的概念。Material 3为每种关键颜色定义了一系列角色,最重要的包括:

primary / onPrimary------主色及其上的文字颜色。用于最突出的元素,比如FAB和主要按钮。

primaryContainer / onPrimaryContainer------主色的容器变体,用于需要较低强调度的场景,比如Filled Tonal按钮。

surface / onSurface------表面颜色及其上的文字颜色。这是应用中最常见的背景色。

surfaceVariant / onSurfaceVariant------表面变体,用于需要微妙区分的场景,比如卡片背景和输入框背景。

error / onError------错误状态的颜色。

这种"角色"思维的重要性在于:当你使用MaterialTheme.colorScheme.primary时,你拿到的不是一个固定的颜色值,而是一个语义化的颜色引用 。如果用户切换了动态颜色,或者你更换了品牌配色方案,这个引用会自动指向新的正确颜色。你的UI代码完全不需要改动。

4.3 动手实践:定制Reply的颜色

让我们在Reply应用中实际操作一下。在ui/theme/Color.kt中,我们定义应用的品牌颜色:

kotlin 复制代码
val Blue40 = Color(0xFF1A73E8) // 定义品牌主色:一种偏亮的蓝色,40 表示浅色主题常用色调,0xFF 表示不透明
val Blue80 = Color(0xFF8AB4F8) // 定义品牌主色的浅色变体:更淡的蓝色,80 表示深色主题或容器色常用色调
val BlueGrey40 = Color(0xFF5F6368) // 定义中性偏蓝的灰色:用于次要文字或表面变体,40 表示浅色主题常用
val BlueGrey80 = Color(0xFFBDC1C6) // 定义中性偏蓝的浅灰:用于深色主题或低强调背景,80 表示深色主题常用

然后在Theme.kt中构建浅色颜色方案:

kotlin 复制代码
private val LightColorScheme = lightColorScheme( // 调用 Material 3 的浅色方案构建函数,生成一套语义化颜色
    primary = Blue40, // 主色:用于 FAB、主要按钮、选中状态等最高强调元素
    onPrimary = Color.White, // 主色上的文字/图标颜色:白色保证在 Blue40 上有足够对比度
    primaryContainer = Blue80, // 主色容器:用于 Filled Tonal 按钮、头像背景等较低强调场景
    onPrimaryContainer = Color(0xFF001D36), // 主色容器上的文字/图标:深蓝黑,保证在 Blue80 上清晰可读
    surface = Color(0xFFF8F9FA), // 表面色:应用最常见的背景色,接近白色但略带灰
    onSurface = Color(0xFF1F1F1F), // 表面上的文字/图标:深灰黑,保证在浅色表面上有高对比度
    surfaceVariant = Color(0xFFE8EAED), // 表面变体:用于卡片、输入框等需要与背景微妙区分的区域
    onSurfaceVariant = Color(0xFF444746), // 表面变体上的文字/图标:中深灰,保证在 surfaceVariant 上可读
) // 浅色方案构建结束,赋给 LightColorScheme

现在,任何使用MaterialTheme.colorScheme.surface的组件都会自动获得Color(0xFFF8F9FA)作为背景色,使用onSurface的文字则会得到Color(0xFF1F1F1F)。

4.4 动态颜色:一行代码的魔法

如果你觉得手动定义颜色方案还是太麻烦,Material 3提供了一个更激进的选项------动态颜色。从Android 12开始,系统可以从用户的壁纸中提取颜色,自动生成一套完整的配色方案。

在Compose中启用动态颜色只需要一个条件判断:

kotlin 复制代码
val colorScheme = when { // 声明一个不可变变量 colorScheme,用 when 表达式根据条件选择方案
    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { // 条件:启用动态颜色,且系统版本是 Android 12(API 31)及以上
        val context = LocalContext.current // 获取当前 Compose 环境里的 Context,用于读取系统壁纸颜色
        if (darkTheme) dynamicDarkColorScheme(context) // 如果是深色主题,调用动态深色方案,从壁纸提取颜色
        else dynamicLightColorScheme(context) // 否则调用动态浅色方案,同样从壁纸提取颜色
    } // 动态颜色分支结束
    darkTheme -> DarkColorScheme // 如果不满足动态颜色但需要深色主题,使用手写深色方案
    else -> LightColorScheme // 其他情况使用手写浅色方案
} // when 表达式结束,colorScheme 得到最终颜色方案

dynamicLightColorScheme和dynamicDarkColorScheme会读取系统壁纸的色调信息,生成一套在视觉上和谐、在对比度上合规的配色。这意味着用户换了一张壁纸,你的应用界面就焕然一新------而你没有写任何额外的代码。

当然,动态颜色并不总是适合所有应用。如果你的品牌有严格的视觉规范,或者应用类型对颜色准确性有高要求(比如金融、医疗),手动定义颜色方案仍然是更好的选择。Material 3的设计允许你在这两种模式之间灵活切换。

4.5 在组件里使用颜色

颜色定义好了,接下来就是在界面里使用。下面这段代码展示了一个邮件卡片如何使用主题颜色:

kotlin 复制代码
Card( // 创建一个 Material 3 卡片,作为邮件列表项的容器
    colors = CardDefaults.cardColors( // 指定卡片颜色配置,不硬编码颜色
        containerColor = MaterialTheme.colorScheme.surfaceVariant // 卡片背景使用表面变体色,与页面背景形成微妙层次
    ) // 颜色配置结束
) { // 卡片内容 lambda 开始
    Text( // 创建文本组件,显示邮件主题
        text = email.subject, // 文本内容来自邮件对象的 subject 属性
        style = MaterialTheme.typography.titleLarge, // 文字样式使用主题中的 titleLarge,保证全局一致
        color = MaterialTheme.colorScheme.onSurfaceVariant // 文字颜色使用表面变体上的颜色,保证对比度
    ) // Text 组件结束
} // Card 组件结束

注意,这里没有出现任何Color(0xFF...)。所有颜色都来自MaterialTheme.colorScheme。这就是Material 3的颜色角色系统在实战中的样子:你描述"这是什么角色",而不是"这是什么颜色"。

五、排版系统:五种样式,够用就好

5.1 从十五到五的减法

Material Design 2的排版系统定义了15种文本样式,从Display Large到Overline,开发者需要记住每一种样式的用途和适用场景。Material 3做了一个大胆的简化:虽然仍然保留了15种样式的定义,但官方建议在大多数应用中只使用五种:

  • headlineSmall------页面级标题,比如邮件详情页的主题行
  • titleLarge------列表项的标题,比如邮件列表中的邮件主题
  • bodyLarge------正文内容
  • bodyMedium------次要正文内容
  • labelMedium------标签和辅助信息,比如发件人姓名和时间戳

在Reply的Codelab中,这五种样式被精确地应用到了邮件列表和详情页的各个元素上。比如邮件列表项中,发件人姓名用labelMedium,邮件主题用titleLarge,正文预览用bodyLarge。

5.2 Emphasized排版:Expressive的新武器

Material 3 Expressive在原有排版系统的基础上,引入了Emphasized排版样式。这不是简单地加大加粗,而是一套专门用于表达情感状态和视觉焦点的排版方案。

Emphasized排版样式的特点是更大、更粗、更有存在感。Google的指南指出,这些样式应该用于"强调重要操作(如'开始录音')或关键信息(如未读消息)"。在Reply应用中,我们可以把邮件主题的排版升级为Emphasized样式,让它在列表中更有冲击力。

更强大的是,Expressive的排版系统支持可变字体。你可以动态调整字重、字宽等属性,让文字在不同场景下表达不同的情感------从柔和的阅读体验到强烈的视觉冲击。

5.3 在Compose中定义排版

在ui/theme/Type.kt中定义应用排版:

kotlin 复制代码
val AppTypography = Typography( // 创建 Material 3 的排版对象,覆盖默认字体样式
    titleLarge = TextStyle( // 定义 titleLarge:用于列表项标题、邮件主题等
        fontWeight = FontWeight.SemiBold, // 字重设为半粗体,让标题比正文更醒目
        fontSize = 18.sp, // 字号 18sp,适合手机屏幕上的重要标题
        lineHeight = 32.sp, // 行高 32sp,比字号大很多,避免长标题拥挤
        letterSpacing = 0.sp // 字间距为 0,保持默认紧凑感
    ), // titleLarge 定义结束
    bodyLarge = TextStyle( // 定义 bodyLarge:用于正文、邮件预览等
        fontWeight = FontWeight.Normal, // 字重正常,适合大段阅读
        fontSize = 16.sp, // 字号 16sp,移动端正文的舒适大小
        lineHeight = 24.sp, // 行高 24sp,约为字号的 1.5 倍,提升可读性
        letterSpacing = 0.5.sp // 字间距 0.5sp,让正文略微呼吸
    ), // bodyLarge 定义结束
    labelMedium = TextStyle( // 定义 labelMedium:用于发件人、时间戳等辅助信息
        fontWeight = FontWeight.Medium, // 字重中等,比正文稍强但弱于标题
        fontSize = 12.sp, // 字号 12sp,适合次要标签
        lineHeight = 16.sp, // 行高 16sp,保证小字号也不拥挤
        letterSpacing = 0.5.sp // 字间距 0.5sp,提升小字清晰度
    ) // labelMedium 定义结束
) // AppTypography 创建结束

注意titleLarge中我们把fontWeight设为SemiBold,lineHeight设为32.sp。这是Reply Codelab中的一个关键调整------邮件主题需要比普通标题更醒目,同时更大的行高让长主题在列表中不会显得拥挤。

5.4 在组件中使用排版

然后在组件中使用:

kotlin 复制代码
Text( // 创建文本组件,显示发件人姓名
    text = email.sender.firstName, // 文本内容来自发件人的名字
    style = MaterialTheme.typography.labelMedium, // 使用主题中的 labelMedium 样式
    color = MaterialTheme.colorScheme.onSurfaceVariant // 使用表面变体上的颜色
) // Text 组件结束

这里的关键是MaterialTheme.typography.labelMedium------它不是硬编码的样式,而是从主题中读取的。如果将来你把整个应用的标签字体换成另一种风格,只需要修改Type.kt一个地方。

5.5 字体的可访问性

Material 3的排版系统内置了对可访问性的支持。每种文本样式都定义了推荐的最小字号和行高,确保在不同屏幕密度和显示设置下文字仍然清晰可读。当你自定义字体时,建议遵循这些基准值,不要为了"好看"而牺牲可读性。

六、动效系统:让界面"活"起来

动效是Material 3 Expressive最核心的变革。它不是加一些花哨的动画效果,而是重新定义了界面如何回应用户的每一次触碰。Google将这一变革称为"运动物理系统"(Motion Physics System)------用弹簧物理取代了传统的贝塞尔缓动曲线。

6.1 从贝塞尔曲线到弹簧物理

传统的动画使用时间/缓动曲线来控制------你指定动画持续多少毫秒,用什么样的缓动函数(比如ease-in-out)。这种方式精确可控,但感觉机械,就像一台机器在按部就班地执行指令。

Material 3 Expressive引入了运动物理系统,使用弹簧物理来驱动动画。这不是简单的"加个弹跳效果",而是一套完整的物理模型。它基于三个核心参数:

Stiffness(刚度) ------弹簧有多"硬"。刚度越高,动画完成得越快。想象一根很紧的弹簧和一根很松的弹簧,松手后回弹的速度完全不同。

Damping(阻尼) ------弹跳消退的速度。阻尼为1时完全没有弹跳,阻尼越低弹跳越明显。这就像在弹簧上加了不同程度的缓冲器。

Initial Velocity(初速度) ------动画开始时的初始速度。如果你快速滑动一个元素,它会带着这个速度继续运动,而不是突然改变速度。

Material 3 Expressive官方给出了默认的弹簧参数:空间弹簧(用于位置、尺寸变化)的阻尼为0.8,刚度为380;效果弹簧(用于颜色、透明度变化)的阻尼为1.0,刚度为1600。效果弹簧阻尼为1.0意味着完全没有弹跳------因为颜色的变化不应该有回弹感。

6.2 空间弹簧与效果弹簧

Expressive的运动系统区分了两种弹簧类型:

Spatial Springs(空间弹簧) ------用于位置、尺寸、旋转等空间属性的变化。它们模拟物体在真实空间中的运动方式,让动画感觉清晰、可预测。

Effects Springs(效果弹簧) ------用于颜色、透明度等效果属性的变化。它们确保这些变化平滑自然,不会产生突兀的视觉跳跃。

这种区分很重要。如果一个按钮按下时,位置变化带有弹跳(空间弹簧)但颜色变化没有弹跳(效果弹簧),整个交互就会感觉自然。反之,如果颜色也在"弹跳",那就会显得怪异。

6.3 在Compose中使用弹簧动画

kotlin 复制代码
val scale by animateFloatAsState( // 创建一个动画状态,跟踪缩放值
    targetValue = if (isPressed) 0.95f else 1f, // 按下时缩小到 0.95,松开时恢复
    animationSpec = spring( // 使用弹簧动画规格
        dampingRatio = Spring.DampingRatioMediumBouncy, // 阻尼比:中等弹跳,有明显的回弹效果
        stiffness = Spring.StiffnessMedium // 刚度:中等,动画完成速度适中
    ), // 弹簧规格结束
    label = "buttonScale" // 动画标签,方便调试
) // 动画状态结束

Box( // 创建可缩放容器
    modifier = Modifier // 修饰符开始
        .scale(scale) // 应用缩放动画值
        .pointerInput(Unit) { // 添加指针输入处理
            detectTapGestures( // 检测点击手势
                onPress = { // 按下时
                    isPressed = true // 设为按下状态,触发缩小动画
                    awaitRelease() // 等待释放
                    isPressed = false // 恢复状态,触发回弹动画
                } // 按下回调结束
            ) // 手势检测结束
        } // 指针输入结束
) { // Box 内容开始
    // 按钮内容
} // Box 内容结束

这段代码展示了Expressive风格的核心------按钮在被按下时会缩小,松开时带着弹簧般的感觉弹回 。这不是简单的线性动画,而是通过spring函数控制的物理模拟,dampingRatio和stiffness决定了回弹的手感。

6.4 触觉反馈与动效的配合

Material 3 Expressive强调动效与触觉反馈(Haptic Feedback) 的配合。当用户关闭一个通知时,不仅会看到平滑的分离动画,还会感受到恰到好处的震动。这种多感官的反馈让交互更加立体和满足。

在Compose中,你可以通过LocalHapticFeedback来实现:

kotlin 复制代码
val haptic = LocalHapticFeedback.current // 获取当前环境的触觉反馈实例

// 在合适的时机触发
haptic.performHapticFeedback(HapticFeedbackType.LongPress) // 执行长按触觉反馈,配合动画增强感知

七、形状系统:35种形状的视觉语言

如果说动效是Expressive的"灵魂",那么形状就是它的"骨架"。Material 3 Expressive将形状系统从6种基础形状扩展到了35种形状,这是一个质的飞跃。

7.1 从基础形状到扩展形状库

在Material 3中,基础形状系统包括:

extraSmall(4dp)------用于小尺寸组件,如Chip和小的输入框

small(8dp)------用于中等尺寸组件,如Card和Dialog

medium(12dp)------用于较大的容器

large(16dp)------用于更大的表面

extraLarge(28dp)------用于模态底部弹窗等

full------完全圆形,用于FAB和IconButton

7.2 Expressive形状库:从装饰到动态

这些新形状不只是更多的圆角选项。它们包括了装饰性形状------比如特殊的角形、不规则多边形,可以用在图像裁剪、头像、加载指示器等元素上。

更重要的是,Expressive引入了形状变形(Shape Morphing) ------一个形状可以平滑过渡到另一个形状。比如一个方形按钮被按下时可以变成圆形,一个卡片展开时可以改变轮廓形状。形状变形使用Expressive运动方案(带有弹跳感)作为默认行为,也可以切换到标准运动方案。

在Reply应用中,我们可以让邮件卡片的头像在选中时从圆形变形为水滴形,或者让FAB在展开时从圆形变为圆角方形。这些形状变化不只是视觉装饰------它们是状态的视觉信号,帮助用户理解界面当前的状态。

7.3 在Compose中定义形状

kotlin 复制代码
val AppShapes = Shapes( // 创建 Material 3 形状对象,统一管理圆角
    extraSmall = RoundedCornerShape(4.dp), // 超小圆角:用于 Chip、小输入框等精致元素
    small = RoundedCornerShape(8.dp), // 小圆角:用于小卡片、小按钮等
    medium = RoundedCornerShape(12.dp), // 中圆角:用于普通卡片、对话框等
    large = RoundedCornerShape(16.dp), // 大圆角:用于大表面、底部弹窗等
    extraLarge = RoundedCornerShape(28.dp) // 超大圆角:用于模态底部弹窗等强调容器
) // AppShapes 创建结束

使用起来同样简单:

kotlin 复制代码
Card( // 创建卡片组件
    shape = MaterialTheme.shapes.medium, // 卡片形状使用主题中的 medium 圆角,保持全局一致
    colors = CardDefaults.cardColors( // 指定卡片颜色配置
        containerColor = MaterialTheme.colorScheme.surfaceVariant // 背景使用表面变体色
    ) // 颜色配置结束
) { // 卡片内容开始
    // 卡片内容
} // 卡片内容结束

八、组件实战:从按钮到导航栏

8.1 五种按钮,五种强调级别

Material 3定义了五种按钮样式,它们的区别不在于外观,而在于强调级别:

Filled Button------最高强调。用于最终确认操作,比如"发送""保存""确认"。

Filled Tonal Button------中高强调。用于次要的确认操作,视觉上比Filled柔和。

Elevated Button------中强调。带轻微阴影,用于需要在视觉上"浮起"的场景。

Outlined Button------中低强调。用于次要操作,比如"取消""稍后"。

Text Button------最低强调。用于最不重要的操作,比如对话框中的"了解更多"。

在Reply中,邮件详情页底部的"回复"按钮使用了FilledButton,因为这是页面最重要的操作:

kotlin 复制代码
FilledButton( // 创建填充按钮,最高强调级别
    onClick = { /* 回复邮件 */ }, // 点击时执行回复逻辑,这里用注释占位
    modifier = Modifier.fillMaxWidth() // 修饰符:让按钮宽度填满父容器
) { // 按钮内容 lambda 开始
    Icon( // 创建图标组件
        imageVector = Icons.Default.Reply, // 使用默认的回复图标
        contentDescription = null // 内容描述设为 null,因为旁边有文字说明,避免重复朗读
    ) // 图标组件结束
    Spacer(Modifier.width(8.dp)) // 创建间距,宽度 8dp,把图标和文字分开
    Text("回复") // 创建文字"回复",告诉用户按钮功能
} // 按钮内容结束

注意按钮同时包含了图标和文字------这是Material 3推荐的做法,图标帮助快速识别,文字提供明确含义。

8.2 Expressive新组件:Button Group

Material 3 Expressive引入了全新的Button Group组件------一组相关按钮水平排列,按下时会进行形状变形。这在Reply应用中非常有用------比如在邮件操作区域放置"回复""转发""标记"三个按钮组:

kotlin 复制代码
ButtonGroup( // 创建 Material 3 Expressive 按钮组
    modifier = Modifier.padding(16.dp) // 外边距 16dp
) { // 按钮组内容开始
    FilledTonalButton( // 第一个按钮:回复
        onClick = { /* 回复邮件 */ }, // 点击回调
        shape = ButtonGroupDefaults.connectedLeadingButtonShape // 使用按钮组连接形状------左侧圆角
    ) { // 按钮内容开始
        Icon(Icons.Default.Reply, contentDescription = null) // 回复图标
        Spacer(Modifier.width(8.dp)) // 图标和文字间距
        Text("回复") // 按钮文字
    } // 按钮内容结束
    FilledTonalButton( // 第二个按钮:转发
        onClick = { /* 转发邮件 */ }, // 点击回调
        shape = ButtonGroupDefaults.connectedMiddleButtonShape // 使用按钮组连接形状------中间方形
    ) { // 按钮内容开始
        Icon(Icons.Default.Forward, contentDescription = null) // 转发图标
        Spacer(Modifier.width(8.dp)) // 图标和文字间距
        Text("转发") // 按钮文字
    } // 按钮内容结束
    FilledTonalButton( // 第三个按钮:标记
        onClick = { /* 标记邮件 */ }, // 点击回调
        shape = ButtonGroupDefaults.connectedTrailingButtonShape // 使用按钮组连接形状------右侧圆角
    ) { // 按钮内容开始
        Icon(Icons.Default.Star, contentDescription = null) // 星标图标
        Spacer(Modifier.width(8.dp)) // 图标和文字间距
        Text("标记") // 按钮文字
    } // 按钮内容结束
} // 按钮组结束

Button Group不只是把按钮排在一起,它还内置了形状变形动画------当用户触摸组内某个按钮时,该按钮的形状会动态变化,提供即时的视觉反馈。

Material 3 Expressive用FAB Menu取代了之前的Speed Dial和堆叠的Mini FAB。一个FAB菜单可以包含2到6个相关操作,展开时带有弹簧动画和触觉反馈。

在Reply中,写邮件FAB可以展开为"新邮件""回复全部""草稿箱"等多个操作:

kotlin 复制代码
FloatingActionButtonMenu( // 创建 FAB 菜单,替代传统 Speed Dial
    expanded = expanded, // 当前展开状态
    onExpandedChange = { expanded = it }, // 展开状态变化回调
    fab = { // 主 FAB 定义
        FloatingActionButton( // 创建主浮动操作按钮
            onClick = { expanded = !expanded }, // 点击切换展开状态
            shape = MaterialTheme.shapes.extraLarge, // Expressive 大圆角形状
            containerColor = MaterialTheme.colorScheme.primaryContainer, // 主色容器背景
            contentColor = MaterialTheme.colorScheme.onPrimaryContainer // 主色容器上的颜色
        ) { // FAB 内容
            Icon( // 主图标
                imageVector = if (expanded) Icons.Default.Close else Icons.Default.Edit, // 展开时显示关闭,收起时显示编辑
                contentDescription = if (expanded) "关闭菜单" else "写邮件" // 无障碍描述
            ) // 图标结束
        } // FAB 结束
    } // 主 FAB 定义结束
) { // FAB 菜单内容开始
    FloatingActionButtonMenuItem( // 菜单项:新邮件
        onClick = { expanded = false }, // 点击后收起菜单
        icon = { Icon(Icons.Default.Email, contentDescription = null) }, // 菜单项图标
        text = { Text("新邮件") } // 菜单项文字
    ) // 菜单项结束
    FloatingActionButtonMenuItem( // 菜单项:回复全部
        onClick = { expanded = false }, // 点击后收起菜单
        icon = { Icon(Icons.Default.ReplyAll, contentDescription = null) }, // 菜单项图标
        text = { Text("回复全部") } // 菜单项文字
    ) // 菜单项结束
} // FAB 菜单结束

8.4 卡片:内容的容器

卡片是Material 3中最常用的容器组件。Reply的邮件列表就是由一系列Card组成的:

kotlin 复制代码
Card( // 创建邮件卡片
    onClick = { /* 打开邮件详情 */ }, // 点击卡片时打开详情页
    modifier = Modifier.fillMaxWidth(), // 让卡片宽度填满父容器
    colors = CardDefaults.cardColors( // 指定卡片颜色
        containerColor = MaterialTheme.colorScheme.surfaceVariant // 背景使用表面变体色
    ) // 颜色配置结束
) { // 卡片内容开始
    Row( // 创建横向布局,把头像和文字排成一行
        modifier = Modifier.padding(16.dp), // 内边距 16dp,让内容不贴边
        verticalAlignment = Alignment.CenterVertically // 垂直居中对齐子元素
    ) { // Row 内容开始
        Box( // 创建盒子,作为头像容器
            modifier = Modifier // 修饰符开始
                .size(40.dp) // 设置尺寸为 40dp
                .background( // 设置背景
                    color = MaterialTheme.colorScheme.primaryContainer, // 背景色使用主色容器
                    shape = CircleShape // 形状为圆形
                ), // 背景设置结束
            contentAlignment = Alignment.Center // 内容居中对齐
        ) { // Box 内容开始
            Text( // 创建文本,显示发件人名字首字母
                text = email.sender.firstName.first().toString(), // 取名字第一个字符并转成字符串
                style = MaterialTheme.typography.titleLarge, // 使用 titleLarge 样式
                color = MaterialTheme.colorScheme.onPrimaryContainer // 使用主色容器上的颜色
            ) // Text 结束
        } // Box 结束
        Spacer(Modifier.width(16.dp)) // 创建 16dp 宽的间距,隔开头像和文字
        Column(modifier = Modifier.weight(1f)) { // 创建纵向布局,并占满剩余宽度
            Text( // 显示发件人姓名
                text = email.sender.firstName, // 文本为发件人名字
                style = MaterialTheme.typography.labelMedium, // 使用 labelMedium 样式
                color = MaterialTheme.colorScheme.onSurfaceVariant // 使用表面变体上的颜色
            ) // Text 结束
            Text( // 显示邮件主题
                text = email.subject, // 文本为邮件主题
                style = MaterialTheme.typography.titleLarge, // 使用 titleLarge 样式
                modifier = Modifier.padding(vertical = 4.dp) // 上下各加 4dp 内边距
            ) // Text 结束
            Text( // 显示邮件正文预览
                text = email.body, // 文本为邮件正文
                maxLines = 2, // 最多显示两行
                overflow = TextOverflow.Ellipsis, // 超出部分用省略号表示
                style = MaterialTheme.typography.bodyLarge, // 使用 bodyLarge 样式
                color = MaterialTheme.colorScheme.onSurfaceVariant // 使用表面变体上的颜色
            ) // Text 结束
        } // Column 结束
    } // Row 结束
} // Card 结束

这段代码展示了Material 3的典型用法:所有颜色都来自MaterialTheme.colorScheme,所有文字样式都来自MaterialTheme.typography。没有硬编码的颜色值,没有手动定义的文字大小。这意味着当你切换主题或启用动态颜色时,整个卡片的外观会自动适配。

8.5 底部导航栏

在手机等紧凑设备上,底部导航栏是最常见的导航模式。Material 3中对应的组件是NavigationBar和NavigationBarItem:

kotlin 复制代码
NavigationBar( // 创建 Material 3 底部导航栏
    containerColor = MaterialTheme.colorScheme.surface // 导航栏背景使用表面色
) { // 导航栏内容开始
    NavigationBarItem( // 创建第一个导航项
        selected = selectedItem == 0, // 当 selectedItem 为 0 时表示选中
        onClick = { selectedItem = 0 }, // 点击后把选中项设为 0
        icon = { Icon(Icons.Default.Home, contentDescription = "收件箱") }, // 图标为首页,描述为收件箱
        label = { Text("收件箱") } // 标签文字为收件箱
    ) // 第一个导航项结束
    NavigationBarItem( // 创建第二个导航项
        selected = selectedItem == 1, // 当 selectedItem 为 1 时表示选中
        onClick = { selectedItem = 1 }, // 点击后把选中项设为 1
        icon = { Icon(Icons.Default.Star, contentDescription = "已加星标") }, // 图标为星标,描述为已加星标
        label = { Text("已加星标") } // 标签文字为已加星标
    ) // 第二个导航项结束
} // 导航栏内容结束

注意从Material 2迁移过来的开发者:BottomNavigation和BottomNavigationItem已经被NavigationBar和NavigationBarItem取代了。这不是简单的改名------新组件的视觉语言、交互反馈和可访问性支持都有显著提升。

九、高度(Elevation):不只是阴影

在Material 2中,高度主要通过阴影来表达------一个组件在Z轴上的位置越高,阴影越大。Material 3改变了这种做法,转而使用色调叠加来表示高度。

什么是色调叠加?简单说,当一个组件"浮起"时,它的背景色不是变得更暗或更亮,而是在表面色上叠加一层主色调。高度越高,叠加的主色调越明显。

在Compose中,Surface组件通过tonalElevation参数来实现这个效果:

kotlin 复制代码
Surface( // 创建表面容器,用于承载内容并表达层级
    tonalElevation = 5.dp, // 色调高度 5dp,表面会叠加约 5% 主色调,表达轻微浮起
    color = MaterialTheme.colorScheme.surface // 表面颜色使用主题表面色
) { // 表面内容开始
    // 内容
} // 表面内容结束

5dp的色调高度意味着表面色会叠加约5%的主色调。这个变化很微妙,但在视觉上创造了一种层次感------高层的表面看起来"更接近"用户,也就更应该被注意到。

Material 3定义了六个高度级别:

  • Level 0:0dp------贴在背景上
  • Level 1:1dp------轻微抬起
  • Level 2:3dp------卡片级别
  • Level 3:6dp------FAB级别
  • Level 4:8dp------导航栏级别
  • Level 5:12dp------模态弹窗级别

这些级别不是强制性的,但它们为不同组件提供了合理的默认值。你可以根据实际需要调整。

十、图标与加载:更多Expressive组件

10.1 从Material Icons到Material Symbols

Material 3引入了一套全新的图标系统------Material Symbols 。它取代了之前的Material Icons,提供了三种风格:Outlined (描边)、Rounded (圆角)和Sharp(锐角)。

更重要的是,Material Symbols是可变字体 ,这意味着你可以动态调整四个属性:Weight (字重)、Fill (填充)、Grade (等级)和Optical Size(视觉尺寸)。其中Fill属性特别有用------你可以在选中/未选中状态之间平滑过渡图标的填充程度。

10.2 Loading Indicator:取代传统的圆形进度条

Material 3 Expressive引入了一个新的Loading Indicator 组件,专门用于替换不定量的圆形进度指示器。它的设计目标是处理5秒以内的等待 ------比如页面加载、下拉刷新等场景。与传统的旋转圆圈不同,Loading Indicator使用波浪形状,Google指出"波浪形状可以让较长的流程感觉不那么静态",并支持成功和错误状态的切换,整个过渡都有弹簧动画的加持。

kotlin 复制代码
LoadingIndicator( // 创建 Material 3 Expressive 加载指示器
    modifier = Modifier.size(48.dp), // 设置尺寸 48dp
    containerColor = MaterialTheme.colorScheme.primary, // 容器颜色使用主色
    indicatorColor = MaterialTheme.colorScheme.onPrimary // 指示器颜色使用主色上的颜色
) // 加载指示器结束

对于超过5秒的等待,仍然使用传统的线性或圆形进度指示器,但它们也获得了更新------变得更粗,并且采用波浪形而非平面形状。

10.3 Split Button:一分为二的智能按钮

Split Button是另一个Expressive新组件,它将一个主要操作和一个相关的次要操作合并到一个视觉容器中。左侧是主操作按钮(带图标和/或文字),右侧是一个动画箭头,点击后展开更多选项。

在Reply中,"发送"按钮可以是Split Button------点击左侧直接发送,点击右侧箭头则展开"定时发送""保存草稿"等选项。

10.4 在Compose中使用图标

对于需要Fill状态切换的场景(比如星标图标):

kotlin 复制代码
Icon( // 创建图标组件
    imageVector = if (isStarred) Icons.Filled.Star else Icons.Outlined.Star, // 根据是否星标选择填充或描边星形图标
    contentDescription = if (isStarred) "取消星标" else "添加星标", // 根据状态提供不同的无障碍描述
    tint = if (isStarred) MaterialTheme.colorScheme.primary // 如果已星标,使用主色
           else MaterialTheme.colorScheme.onSurfaceVariant // 否则使用表面变体上的颜色
) // 图标组件结束

注意tint参数------图标的颜色应该始终来自MaterialTheme.colorScheme,而不是硬编码。这样图标才能随着主题变化自动调整。

十一、把它们组装起来

现在我们已经覆盖了Material 3 Expressive的核心概念和组件。让我们回到Reply应用,看看这些元素如何协同工作。

应用的整体主题由MaterialTheme包裹:

kotlin 复制代码
@Composable // 声明这是一个 Compose 可组合函数
fun ReplyTheme( // 定义主题函数 ReplyTheme
    darkTheme: Boolean = isSystemInDarkTheme(), // 是否深色主题,默认跟随系统
    dynamicColor: Boolean = true, // 是否启用动态颜色,默认开启
    content: @Composable () -> Unit // 要包裹的界面内容
) { // 函数体开始
    val colorScheme = when { // 计算颜色方案
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { // 开启动态颜色且 Android 12+
            val context = LocalContext.current // 获取当前 Context
            if (darkTheme) dynamicDarkColorScheme(context) // 深色动态方案
            else dynamicLightColorScheme(context) // 浅色动态方案
        } // 分支结束
        darkTheme -> DarkColorScheme // 手写深色方案
        else -> LightColorScheme // 手写浅色方案
    } // when 结束
    MaterialTheme( // 应用 Material 3 主题
        colorScheme = colorScheme, // 注入颜色方案
        typography = AppTypography, // 注入排版方案
        shapes = AppShapes, // 注入形状方案
        content = content // 注入内容
    ) // MaterialTheme 结束
} // ReplyTheme 结束

然后在MainActivity中使用:

kotlin 复制代码
class MainActivity : ComponentActivity() { // 定义主 Activity,继承 ComponentActivity
    override fun onCreate(savedInstanceState: Bundle?) { // 重写 onCreate 生命周期方法
        super.onCreate(savedInstanceState) // 调用父类实现,完成系统初始化
        setContent { // 设置 Compose 内容
            ReplyTheme { // 用 ReplyTheme 包裹整个应用界面
                ReplyApp() // 调用 ReplyApp 可组合函数,展示邮件应用
            } // ReplyTheme 结束
        } // setContent 结束
    } // onCreate 结束
} // MainActivity 结束

ReplyApp中包含了邮件列表页和详情页的导航。列表页使用Card展示邮件,详情页展示完整的邮件内容和回复按钮。底部导航栏在列表页和"已加星标"页之间切换。FAB展开为FAB Menu,提供多个邮件操作。

当你运行这个应用时,你会注意到几件事:

颜色的层次感是自然形成的------你不需要手动调整每个元素的颜色,它们之间的关系由角色系统自动维护。

排版的可读性在浅色和深色模式下都很好------色调系统保证了文字和背景之间的对比度。

圆角的一致性贯穿始终------按钮、卡片、对话框都遵循同一套形状规则。

而在Expressive的加持下,每一次交互都带有弹簧般的生命力------按钮按下时微微缩小、松开时带着俏皮的回弹;导航栏切换时图标形状平滑变形;FAB展开时菜单项像弹簧一样依次弹出。这些微小的动效让应用从"能用"变成了"好用且有乐趣"。

如果你在支持动态颜色的设备上运行,换一张壁纸,整个应用的配色会随之改变,而你的代码没有做任何处理。

十二、本课要点回顾

这一课我们从Reply应用的案例出发,认识了Material 3 Expressive的几个核心领域,也感受了它的设计风格:

设计理念------Material 3 Expressive是Material 3的扩展包,以Material You为基础,专注于情感化设计和表现力。Google的研究表明,遵循Expressive指南的设计能让用户任务完成速度提高20%,定位正确元素的速度提高33%。

颜色系统------五组色调调色板、十三个亮度级别、语义化的颜色角色。动态颜色进一步降低了定制门槛,让应用"像用户"。Expressive还增加了三档对比度控制级别。

排版系统------简化到五种核心样式,新增Emphasized排版用于表达情感状态和视觉焦点,支持可变字体动态调整。

形状系统------从6种基础形状扩展到35种,配合形状变形动画,为界面提供更丰富的视觉表达。

动效系统------这是Expressive最核心的变革。弹簧物理取代传统缓动曲线,stiffness、damping、velocity三个参数控制动画手感,空间弹簧和效果弹簧分工协作,配合触觉反馈创造多感官体验。

组件生态------新增ButtonGroup、FAB Menu、LoadingIndicator、Split Button等15个组件,所有组件共享主题的颜色、排版和形状,并带有Expressive动效。

高度系统------用色调叠加替代阴影来表达层级,让浮起效果在不同背景下都能保持视觉一致性。

下一课我们将深入Material 3 Expressive的动效细节,看看弹簧物理系统如何精确控制动画手感,以及如何为不同场景选择合适的运动模式。但在此之前,建议你先在Android Studio中运行Reply示例,亲手改一改颜色、字体、圆角和动效参数,感受一下Material 3 Expressive的主题系统是如何把零散的样式参数收拢成一个统一的体验的。

设计系统的价值不在于它提供了多少组件,而在于它让好的默认值成为常态。你不需要成为设计专家就能做出好看的应用------这就是Material 3 Expressive想帮你实现的事。

十三、练习题:把知识变成手感

练习1:自定义一套颜色方案,并用到邮件卡片上

题目 :在 Color.kt 中定义品牌色,在 Theme.kt 中构建浅色方案,然后写一个 EmailCard,要求卡片背景用 surfaceVariant,发件人姓名用 onSurfaceVariant,头像背景用 primaryContainer,首字母用 onPrimaryContainer。

参考答案代码:

kotlin 复制代码
// Color.kt
val ReplyBlue = Color(0xFF1A73E8) // 品牌主色:偏亮的蓝色
val ReplyBlueContainer = Color(0xFFD3E3FD) // 主色容器:浅蓝,用于头像背景
val ReplyOnBlueContainer = Color(0xFF041E49) // 主色容器上的文字:深蓝黑,保证对比度
val ReplySurface = Color(0xFFF8F9FA) // 表面色:接近白色的页面背景
val ReplySurfaceVariant = Color(0xFFE8EAED) // 表面变体:卡片背景,与页面背景微妙区分
val ReplyOnSurface = Color(0xFF1F1F1F) // 表面上的文字:深灰黑
val ReplyOnSurfaceVariant = Color(0xFF444746) // 表面变体上的文字:中深灰

// Theme.kt
private val LightColorScheme = lightColorScheme( // 构建浅色颜色方案
    primary = ReplyBlue, // 主色:用于 FAB、主要按钮、选中状态
    onPrimary = Color.White, // 主色上的文字/图标:白色
    primaryContainer = ReplyBlueContainer, // 主色容器:用于头像、Filled Tonal 按钮
    onPrimaryContainer = ReplyOnBlueContainer, // 主色容器上的文字/图标:深蓝黑
    surface = ReplySurface, // 表面色:页面背景
    onSurface = ReplyOnSurface, // 表面上的文字/图标:深灰黑
    surfaceVariant = ReplySurfaceVariant, // 表面变体:卡片、输入框背景
    onSurfaceVariant = ReplyOnSurfaceVariant // 表面变体上的文字/图标:中深灰
)

@Composable
fun ReplyTheme(content: @Composable () -> Unit) { // 定义应用主题
    MaterialTheme( // 应用 Material 3 主题
        colorScheme = LightColorScheme, // 注入颜色方案
        typography = AppTypography, // 注入排版方案
        shapes = AppShapes, // 注入形状方案
        content = content // 注入界面内容
    )
}

// EmailCard.kt
@Composable
fun EmailCard(email: Email) { // 邮件卡片组件
    Card( // 创建 Material 3 卡片
        modifier = Modifier.fillMaxWidth(), // 宽度填满父容器
        colors = CardDefaults.cardColors( // 配置卡片颜色
            containerColor = MaterialTheme.colorScheme.surfaceVariant // 背景用表面变体色
        )
    ) {
        Row( // 横向排列头像和文字
            modifier = Modifier.padding(16.dp), // 内边距 16dp
            verticalAlignment = Alignment.CenterVertically // 垂直居中
        ) {
            Box( // 头像容器
                modifier = Modifier
                    .size(40.dp) // 尺寸 40dp
                    .background( // 设置背景
                        color = MaterialTheme.colorScheme.primaryContainer, // 主色容器
                        shape = CircleShape // 圆形
                    ),
                contentAlignment = Alignment.Center // 内容居中
            ) {
                Text( // 显示发件人名字首字母
                    text = email.sender.firstName.first().toString(), // 取第一个字符
                    style = MaterialTheme.typography.titleLarge, // 标题样式
                    color = MaterialTheme.colorScheme.onPrimaryContainer // 主色容器上的颜色
                )
            }
            Spacer(Modifier.width(16.dp)) // 头像和文字之间留 16dp
            Column(modifier = Modifier.weight(1f)) { // 文字列占满剩余宽度
                Text( // 发件人姓名
                    text = email.sender.firstName,
                    style = MaterialTheme.typography.labelMedium, // 辅助标签样式
                    color = MaterialTheme.colorScheme.onSurfaceVariant // 表面变体上的文字色
                )
                Text( // 邮件主题
                    text = email.subject,
                    style = MaterialTheme.typography.titleLarge, // 列表标题样式
                    modifier = Modifier.padding(vertical = 4.dp) // 上下留 4dp
                )
                Text( // 正文预览
                    text = email.body,
                    maxLines = 2, // 最多两行
                    overflow = TextOverflow.Ellipsis, // 超出省略
                    style = MaterialTheme.typography.bodyLarge, // 正文样式
                    color = MaterialTheme.colorScheme.onSurfaceVariant // 表面变体上的文字色
                )
            }
        }
    }
}

解读 :这道题训练的是"颜色角色"思维。你没有在组件里写任何 Color(0xFF...),而是全部通过 MaterialTheme.colorScheme 获取。好处是:以后切换深色模式或动态颜色,EmailCard 一行都不用改。头像用 primaryContainer 而不是 primary,是因为头像不需要最高强调,容器色更柔和。


练习2:用弹簧动画让按钮"活"起来

题目 :创建一个按钮,按下时缩小到0.95倍,松开时用弹簧动画弹回1倍。使用 Spring.DampingRatioMediumBouncy 和 Spring.StiffnessMedium。

参考答案代码:

kotlin 复制代码
var isPressed by remember { mutableStateOf(false) } // 保存按钮是否被按下

val scale by animateFloatAsState( // 创建缩放动画状态
    targetValue = if (isPressed) 0.95f else 1f, // 按下时缩小到 0.95,松开时恢复
    animationSpec = spring( // 使用弹簧动画规格
        dampingRatio = Spring.DampingRatioMediumBouncy, // 中等弹跳阻尼,回弹明显但不过分
        stiffness = Spring.StiffnessMedium // 中等刚度,动画速度适中
    ), // 弹簧规格结束
    label = "buttonScale" // 动画标签
) // 动画状态结束

Box( // 创建可缩放容器
    modifier = Modifier // 修饰符开始
        .scale(scale) // 应用缩放动画
        .pointerInput(Unit) { // 添加手势处理
            detectTapGestures( // 检测点击手势
                onPress = { // 按下回调
                    isPressed = true // 设为按下状态
                    awaitRelease() // 等待释放
                    isPressed = false // 恢复状态
                } // 按下回调结束
            ) // 手势检测结束
        } // 手势处理结束
) { // Box 内容
    FilledButton(onClick = { /* 操作 */ }) { // 填充按钮
        Text("按下我") // 按钮文字
    } // 按钮结束
} // Box 结束

解读 :这是Material 3 Expressive动效系统的入门练习。spring函数是核心------dampingRatio控制弹跳程度,stiffness控制动画速度。DampingRatioMediumBouncy会让按钮在松开时有一个明显的回弹效果,这就是Expressive所追求的"让交互感觉有生命力"。


练习3:用形状变形实现头像切换

题目 :创建一个头像,默认是圆形,点击后平滑变形为圆角方形。使用 animateFloatAsState 控制圆角变化。

参考答案代码:

kotlin 复制代码
var isSelected by remember { mutableStateOf(false) } // 保存选中状态

val cornerPercent by animateFloatAsState( // 创建圆角动画
    targetValue = if (isSelected) 0.3f else 0.5f, // 选中时圆角比例 0.3(圆角方形),未选中 0.5(圆形)
    animationSpec = spring( // 弹簧动画规格
        dampingRatio = Spring.DampingRatioLowBouncy, // 低弹跳,变形更柔和
        stiffness = Spring.StiffnessLow // 低刚度,变形速度较慢更优雅
    ), // 弹簧规格结束
    label = "cornerMorph" // 动画标签
) // 动画结束

Box( // 创建头像容器
    modifier = Modifier // 修饰符开始
        .size(48.dp) // 尺寸 48dp
        .clip(RoundedCornerShape(percent = (cornerPercent * 100).toInt())) // 用百分比控制圆角形状变形
        .background(MaterialTheme.colorScheme.primaryContainer) // 主色容器背景
        .clickable { isSelected = !isSelected }, // 点击切换选中状态
    contentAlignment = Alignment.Center // 内容居中
) { // Box 内容
    Text( // 头像文字
        text = "A", // 示例首字母
        style = MaterialTheme.typography.titleLarge, // 标题样式
        color = MaterialTheme.colorScheme.onPrimaryContainer // 主色容器上的颜色
    ) // 文字结束
} // Box 结束

解读 :形状变形是Material 3 Expressive的标志性特征之一。通过动画化圆角百分比,头像可以在圆形和方形之间平滑过渡。Spring.DampingRatioLowBouncy让变形更柔和,RoundedCornerShape(percent)是Compose中实现形状变形的简洁方式。Expressive的扩展形状库提供了更多形状选项,但即使只用基础的圆角变化,也能创造出很好的变形效果。


练习4:用Button Group组织操作按钮

题目 :使用 ButtonGroup 创建一组"回复""转发""标记"三个按钮,分别使用连接形状的 leading、middle、trailing 样式。

参考答案代码:

kotlin 复制代码
ButtonGroup( // 创建 Material 3 Expressive 按钮组
    modifier = Modifier // 修饰符开始
        .fillMaxWidth() // 宽度填满
        .padding(horizontal = 16.dp) // 水平内边距 16dp
) { // 按钮组内容
    FilledTonalButton( // 第一个按钮:回复
        onClick = { /* 回复邮件 */ }, // 点击回调
        shape = ButtonGroupDefaults.connectedLeadingButtonShape // 左侧按钮形状------左圆右方
    ) { // 按钮内容
        Icon(Icons.Default.Reply, contentDescription = null) // 回复图标
        Spacer(Modifier.width(8.dp)) // 图标和文字间距
        Text("回复") // 按钮文字
    } // 按钮结束
    FilledTonalButton( // 第二个按钮:转发
        onClick = { /* 转发邮件 */ }, // 点击回调
        shape = ButtonGroupDefaults.connectedMiddleButtonShape // 中间按钮形状------方形
    ) { // 按钮内容
        Icon(Icons.Default.Forward, contentDescription = null) // 转发图标
        Spacer(Modifier.width(8.dp)) // 图标和文字间距
        Text("转发") // 按钮文字
    } // 按钮结束
    FilledTonalButton( // 第三个按钮:标记
        onClick = { /* 标记邮件 */ }, // 点击回调
        shape = ButtonGroupDefaults.connectedTrailingButtonShape // 右侧按钮形状------右圆左方
    ) { // 按钮内容
        Icon(Icons.Default.Star, contentDescription = null) // 星标图标
        Spacer(Modifier.width(8.dp)) // 图标和文字间距
        Text("标记") // 按钮文字
    } // 按钮结束
} // 按钮组结束

解读 :ButtonGroup 是Material 3 Expressive引入的全新组件。它不是简单地把按钮排在一起,而是通过 connectedLeadingButtonShape、connectedMiddleButtonShape、connectedTrailingButtonShape 让三个按钮形成一个视觉上连贯的整体。当用户触摸组内按钮时,会触发形状变形动画,带来即时的视觉反馈。这是Expressive"表现力"理念的完美体现------功能不变,但感觉完全不同。


练习5:底部导航栏选中时切换图标填充

题目 :实现一个底部导航栏,两个项:"收件箱"和"已加星标"。选中时图标用 Filled,未选中时用 Outlined。用 remember 保存当前选中项。

参考答案代码:

kotlin 复制代码
var selectedItem by remember { mutableIntStateOf(0) } // 保存当前选中项:0 收件箱,1 星标

NavigationBar( // Material 3 底部导航栏
    containerColor = MaterialTheme.colorScheme.surface // 背景用表面色
) {
    NavigationBarItem( // 第一个导航项:收件箱
        selected = selectedItem == 0, // 当前选中 0 时高亮
        onClick = { selectedItem = 0 }, // 点击后选中 0
        icon = {
            Icon(
                imageVector = if (selectedItem == 0) Icons.Filled.Home else Icons.Outlined.Home, // 选中填充,未选中描边
                contentDescription = "收件箱" // 无障碍描述
            )
        },
        label = { Text("收件箱") } // 标签文字
    )

    NavigationBarItem( // 第二个导航项:已加星标
        selected = selectedItem == 1, // 当前选中 1 时高亮
        onClick = { selectedItem = 1 }, // 点击后选中 1
        icon = {
            Icon(
                imageVector = if (selectedItem == 1) Icons.Filled.Star else Icons.Outlined.Star, // 选中填充,未选中描边
                contentDescription = "已加星标" // 无障碍描述
            )
        },
        label = { Text("已加星标") } // 标签文字
    )
}

解读 :Material 3 的导航项通过 selected 自动处理颜色、指示器和高亮。图标在 Filled 和 Outlined 之间切换,是一种低成本但很有效的状态反馈。在Material 3 Expressive中,这个切换还会伴随着弹簧动画和形状变形,让状态变化更加生动。remember { mutableIntStateOf(0) } 保存选中状态,配置变化时如果希望保留,可以换成 rememberSaveable。


练习6:动态颜色优先,手动方案回退

题目 :写一个 ReplyTheme,要求 Android 12 及以上且开启动态颜色时使用动态方案,否则深色模式用手写深色方案,浅色模式用手写浅色方案。

参考答案代码:

kotlin 复制代码
@Composable
fun ReplyTheme( // 应用主题
    darkTheme: Boolean = isSystemInDarkTheme(), // 是否深色主题,默认跟随系统
    dynamicColor: Boolean = true, // 是否启用动态颜色,默认开启
    content: @Composable () -> Unit // 要包裹的界面内容
) {
    val colorScheme = when { // 根据条件选择颜色方案
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { // 开启动态颜色且 Android 12+
            val context = LocalContext.current // 获取当前 Context
            if (darkTheme) dynamicDarkColorScheme(context) // 动态深色方案
            else dynamicLightColorScheme(context) // 动态浅色方案
        }
        darkTheme -> DarkColorScheme // 手写深色方案
        else -> LightColorScheme // 手写浅色方案
    }

    MaterialTheme( // 应用 Material 3 主题
        colorScheme = colorScheme, // 注入颜色方案
        typography = AppTypography, // 注入排版方案
        shapes = AppShapes, // 注入形状方案
        content = content // 注入内容
    )
}

解读 :这是 Material 3 主题配置的标准写法。动态颜色让应用"像用户",手动方案保证低版本和品牌一致性。when 的顺序很重要:先判断动态颜色,再判断深色模式,最后回退到浅色。这样逻辑清晰,也不会出现"明明开了动态颜色却走了手动方案"的问题。


练习7:用 tonalElevation 表达表面浮起

题目 :创建一个 Surface,设置 tonalElevation = 5.dp,背景用 surface,里面放一段文字。再写一个 tonalElevation = 0.dp 的版本,对比效果。

参考答案代码:

kotlin 复制代码
Surface( // 创建表面容器
    tonalElevation = 5.dp, // 色调高度 5dp:表面会叠加约 5% 主色调,表达轻微浮起
    color = MaterialTheme.colorScheme.surface, // 表面颜色用主题表面色
    shape = MaterialTheme.shapes.medium, // 中等圆角
    modifier = Modifier.fillMaxWidth() // 宽度填满
) {
    Text( // 表面内的文字
        text = "这条表面浮起了 5dp", // 文字内容
        modifier = Modifier.padding(16.dp), // 内边距 16dp
        style = MaterialTheme.typography.bodyLarge // 正文样式
    )
}

Surface( // 对比:不浮起的表面
    tonalElevation = 0.dp, // 色调高度 0dp:贴在背景上
    color = MaterialTheme.colorScheme.surface, // 同样的表面色
    shape = MaterialTheme.shapes.medium, // 同样的圆角
    modifier = Modifier.fillMaxWidth() // 宽度填满
) {
    Text( // 表面内的文字
        text = "这条表面贴在背景上", // 文字内容
        modifier = Modifier.padding(16.dp), // 内边距 16dp
        style = MaterialTheme.typography.bodyLarge // 正文样式
    )
}

解读 :Material 3 用"色调叠加"代替 Material 2 的阴影来表达高度。tonalElevation = 5.dp 不会让表面变黑,而是叠加一层很淡的主色调,看起来"离用户更近"。这种效果在不同背景、深色模式下都更稳定,不会像阴影那样在深色背景上显得脏。


做完这七道题,你应该已经亲手摸过 Material 3 Expressive 的颜色、排版、形状、动效、按钮、导航栏、动态主题和高度系统了。下一课我们会深入更复杂的组件组合和动效定制,但只要你把这七道题里的 "角色思维" 、 "主题思维" 和 "弹簧思维" 记住,后面的组件都只是同一套逻辑的延伸。

相关推荐
sunoo-2291 小时前
【嵌入式 ARM 学习第十二天】SPI 裸机驱动 + I2C 触摸屏驱动 + 三大串行协议深度对比
arm开发·笔记·vscode·学习·imx6ull
墨心@2 小时前
第 4 章《工具》学习总结
学习·自然语言处理·prompt·agent·harness
AOI小白新手上路2 小时前
4-1_地址空间_RISC与CISC_学习笔记
笔记·学习
Vcaker2 小时前
Linux学习36-k8s集群升级及kube-vip高可用
linux·运维·学习
probex_2 小时前
学习笔记:知识图谱落地实践
笔记·学习·知识图谱
AOI小白新手上路2 小时前
4-3_ARM汇编_学习笔记
汇编·arm开发·学习
kkkkkkkkkk_Z2 小时前
新手自学嵌入式 | 学习日记:ARM架构初探与基础概念梳理
arm开发·学习·架构
鬓戈3 小时前
开源中文输入法技术调研与自建方案
学习·开源
生如夏花爱学习229663 小时前
贴心使用的语音生活小助手
android