Administrator
发布于 2026-08-26 / 14 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 92 篇:Material 3 Expressive 设计语言:从 Material You 到情感化设计

前言

Material You 出来那年,"取色器"成了设计圈最热的功能——壁纸一变,整个 App 跟着变。两年过去,Google 在 Android 16 同期推出了 Material 3 Expressive,把 Material You 的"温柔"推到了"表情包"级别:

更大的字号、更长的圆角、更大的可点击区域、更夸张的弹性动画。

很多工程师觉得这跟咱没关系,但你看一眼 Jetpack Compose 1.8 起新增的 ExpressiveTheme API 就知道——这次跟咱们写代码的人绑得很死。

一、为什么我们需要新设计语言

Material You 解决了"千人千面"的颜色问题,但没有解决两个真正的痛点:

  1. 可发现性差:FAB、菜单、按钮大量使用时,用户根本分不清主次。

  2. 节奏感:Material You 的动画还是"该有就有",没有"故意停顿"让用户感觉到层级。

Expressive 的解决思路:用 物理弹性(spring-based motion)和 留白压迫(spacing scale v2)强迫设计师重新审视信息密度。

二、字号与间距:写一次就够

新规范里最大的变化是 Type Scale v3:

Token

旧值

新值

用法

displayLarge

57sp / line 64

64sp / line 72

仅用在 Splash、Splash→首页切换

headlineLarge

32sp / line 40

36sp / line 44

页面顶部主标题

titleLarge

22sp / line 28

24sp / line 32

卡片标题、Sheet 头部

bodyLarge

16sp / line 24

16sp / line 26

正文不变

labelLarge

14sp / line 20

14sp / line 22

按钮文字不变

关键:headline 这一档全线加大 4sp,是大多数 App 页面立刻改的项。

间距也改了,引入 4dp grid 的 1.5× 同步规则:

val spacing = ExpressiveSpacing(
  compactPadding = 8.dp,
  defaultPadding = 16.dp,
  dramaticPadding = 32.dp
)

dramaticPadding 用在引导页、空状态、错误页——故意让元素之间拉开距离,让用户有"看清再做决定"的时间。

三、弹性动画:ExpressiveTheme 的核心

Expressive 把 Material motion spec 整合进 Compose API:

@Composable
fun ExpressiveButton(text: String, onClick: () -> Unit) {
  val interaction = remember { MutableInteractionSource() }
  val pressed by interaction.collectIsPressedAsState()
  val scale by animateFloatAsState(
    if (pressed) 0.96f else 1f,
    animationSpec = spring(
      dampingRatio = Spring.DampingRatioMediumBouncy,
      stiffness = Spring.StiffnessMedium
    ), label = "scale"
  )
  Button(
    modifier = Modifier.scale(scale),
    onClick = onClick,
    interactionSource = interaction
  ) {
    Text(text)
  }
}

spring API 取代了大部分 tween 用例。视觉上的差别:以前按钮按下是"立即缩小",现在像皮球一样"按下去弹一下"。

四、可发现性:把 FAB 换成 Expressive Cluster

Material You 时代 FAB 是"一坨悬浮按钮",Expressive 推 Expressive Cluster(中文名"动作簇")——把多个相关动作放进一个可展开区域:

@Composable
fun PrimaryCluster(onSend: () -> Unit, onAttach: () -> Unit) {
  var expanded by remember { mutableStateOf(false) }
  ExpressiveCluster(
    mainAction = ClusterAction("发送", Icons.Default.Send, onSend),
    secondaryActions = listOf(
      ClusterAction("附件", Icons.Default.AttachFile, onAttach),
      ClusterAction("草稿", Icons.Save, {})
    ),
    expanded = expanded,
    onExpandChange = { expanded = it }
  )
}

用户长按主按钮,主按钮轻微退后,副按钮绕主按钮 60° 扇形展开。比起"悬浮按钮 + 弹菜单",可见动作从 1 个变成 3 个,且无需进入二级页面。

五、Expressiveness 不只是 UI:触感反馈与音效

新规范要求:

  • 每个按钮按下都得有 HapticFeedback:长按中等强度、短按轻强度、错误反馈重冲击。

  • 关键操作有 Sonic Branding:从购物车"加入"到付款"成功"使用一组专属音效。

Compose 里直接调:

val haptic = LocalHapticFeedback.current
val view = LocalView.current
Button(onClick = {
  haptic.performHapticFeedback(HapticFeedbackType.LongPress)
  view.playSoundEffect(SoundEffectConstants.CLICK)
}) { Text("下一步") }

但不要每个按钮都加——Expressive 规范里写得很清楚:50% 的页面元素"沉默"才能让另外 50% 的元素"被听见"。

六、在 Compose 里接入 Material 3 Expressive

implementation("androidx.compose.material3:material3:1.8.0")
implementation("androidx.compose.material3:material3-window-size-class:1.8.0")

// 完整主题
@Composable
fun AppTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colorScheme = expressiveColorScheme(dynamicColor = true),
    typography = expressiveTypography(),
    shapes = expressiveShapes(),
    motion = expressiveMotionScheme()
  ) {
    content()
  }
}

expressiveMotionScheme() 是新增的——把 spring 的 dampingRatio / stiffness 抽出来作为 token,跨页面复用,避免每个项目自己定义一套。

七、落地建议

  1. 先改字号:headline 普遍加 4sp,是风险最低、收益最快的动作。

  2. 再改间距:空状态、引导页用 dramaticPadding,让用户感受到"喘气"。

  3. 弹性动画:仅在 FAB、主按钮、Switch 上启用,其它地方保守使用。

  4. 音频与触感:先在旗舰机和 Pixel 上做 A/B 测试,老机型 fallback 到点击震动即可。

小结

Expressive 不是"换个皮肤"——它要求工程和设计重新谈一遍节奏。下一篇文章我们换赛道:聊聊可折叠设备的工程化适配,跟 Material 3 Expressive 是一对固定搭配。

参考阅读

  • Material Design 3 Expressive(m3.material.io)

  • Compose Material 3 1.8 升级指引

  • 「物理弹性」与 Compose spring API


评论