关于 motion-and-haptics
面向 Flutter 应用开发者的动效与触觉反馈设计技能,规定交互时刻、动画的减少动态效果方案及触觉映射。重点是让状态在动画关闭时仍然成立,并确保反馈及时且每次提交只触发一次。
主要功能
- 01交互时刻声明规范
- 02减少动态效果的降级方案
- 03集中式触觉事件映射
- 04每次提交触发一次触觉
- 05可中断且有界的庆祝动画
- 06重复动画停止条件
适用场景
- 01设计按钮按下与提交反馈
- 02为拒绝输入和成功状态配置触觉
- 03适配减少动态效果设置
- 04约束庆祝动画和循环动画
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill motion-and-haptics安装后在 Claude Code 中输入/motion-and-haptics确认已加载。
附带文件references/moment-catalog.md
详细介绍
动效与触觉反馈
何时触发、具体怎么触发,以及如何降级。反馈是用户感受到而非读到的层面——点按有响应、错误能被感知、成功得到确认的应用,会让人觉得已经打磨完成;同一个应用若使用默认的 Material 反馈,则会显得像个原型。
范围边界。 动效令牌——时长角色、曲线槽位、禁止使用原始值的规则,以及在减少动态效果时将 resolveMotion 折叠为 Duration.zero——归 design-system-structure 管辖。绘制器/计时器机制归 custom-canvas-and-gestures 管辖。消息使用哪种界面——加载、空状态、错误、Snackbar、横幅、对话框、撤销和屏幕阅读器播报——归 ui-states-and-feedback 管辖。本技能负责这些决策所依托的感官层:有哪些时机、每个时机要实现什么、如何映射和触发触觉反馈,以及动画不可用时保留什么。
不容妥协的规则
-
每次会提交的交互都必须在发生的同一帧得到确认。 手指按下时立即显示按压状态;提交时再给出该提交对应的反馈。原因:如果控件要等异步结果返回后才显示任何反馈,用户就无法判断控件是否已收到点按,于是会再次点按。
-
动效装饰状态,而不是状态本身。 禁用所有动画时,最终状态必须保持一致、正确且可达。选择状态只有在动画运行时才存在,或数值只有在过渡期间才看得清,都是减少动态效果的用户每次都会遇到的错误。
-
为各个时机命名,并有意识地逐一设计。 每个应用都有一份简短目录——按压确认、输入被拒绝、提交后的回馈、成功、进度或连续记录递增,以及标志性的屏幕过渡。每个时机都要明确五项内容:其触发条件、变化内容、使用的时长角色和曲线、其触觉反馈槽位,以及其减少动态效果时的回退方案。未指定的时机并不代表“极简”——它只是由无人选择的框架默认行为。参见
references/moment-catalog.md。 -
设计时就要明确每个动画在减少动态效果时的行为。 “动画很小,没人会介意”不算回退方案。机制(
resolveMotion→Duration.zero)归design-system-structure管辖;而每个时机的具体决策——交叉淡化至最终状态、直接显示已完成状态,或完全省略点缀动效——在此处逐一制定并记录。功能代码不会为每个动画重新读取平台标志。 -
触觉反馈应有统一映射,而非散落各处的调用。
HapticTheme扩展会将每个事件映射到至多一个HapticFeedback动词:选择 ≤selectionClick,提交 ≤lightImpact/mediumImpact,拒绝 ≤lightImpact,成功 ≤mediumImpact,而heavyImpact仅用于真正的里程碑。null槽位代表明确静默,而非遗漏。原因:在调用点随意添加的触觉反馈,其强度总会逐渐升高,直到整个应用都在嗡嗡作响。 -
每个已提交事件仅触发一次触觉反馈,并在提交帧触发。 绝不在动画回调中触发,不逐帧触发,也不针对合并手势中的每次输入分别触发——拖动最多只在开始和提交时触发反馈。原因:逐帧触发会变成震动;对合并输入逐个触发,会让快速手势变成一阵乱震。
-
绝不用
heavyImpact表示错误。 用更强的反馈来惩罚失败,会让用户为 UI 本可以避免的错误受罚。应确认用户操作被拒绝,而不是放大反馈。 -
应用内的一个开关应控制所有触觉反馈,包括庆祝效果。 平台通常有自己的系统级触觉设置;需要触发触觉反馈的应用也必须提供自己的开关,而且该开关必须是统一的中央门控,而不是让每个调用点都记得检查的标志。
-
每个动画都可中断。 在过渡或庆祝动画期间轻点,应立即将其结束到最终状态。切勿在炫技动画期间阻塞输入,也绝不要让用户等待庆祝动画结束。原因:用户第二次看到庆祝动画时,只想让它快点结束。
-
庆祝动画必须有预算。 限定元素数量,限定进入下一个可交互状态前的时长;动画从操作发生的位置开始,而非从屏幕边缘开始;只播放一次,绝不循环。失败状态只需确认,不要带有羞辱意味的动作。原因:没有上限的庆祝动画,就像用户并未要求的加载画面。
-
每个重复或环境动画都必须声明停止条件。 当其所在路由不再是当前路由、应用进入后台或启用减少动态效果时,动画都应停止。没有停止条件的
repeat()是一个电池隐患,既不会被测试发现,也不会被代码审查者看到。 -
通过多个渠道提供反馈。 启用减少动态效果时,触觉反馈和声音仍然有效,正因为它们是剩余的反馈渠道——因此,绝不能只靠动态效果传达状态变化。(仅靠颜色的问题由
accessibility-as-code负责;仅靠动态效果则是同一规则的另一部分。) -
如果提供声音,它只能作为第三种反馈渠道,绝不能是第一种。 遵循平台的静音行为,保持简短,绝不能要求用户依靠声音理解发生了什么,并通过与触觉反馈相同类型的中央开关控制声音。
-
过渡效果应遵循阅读方向;固定内容表面不应重新镜像。 水平推入效果在 RTL 下应翻转,因为导航具有方向性;而画布、棋盘、图表或媒体表面的布局本身与方向无关,因此应保持原样。方向几何由
i18n-rtl-l10n负责。
声明每个时刻
在实现之前,先记录每个时刻。此表就是规范——如果某个实现无法填写其中一列,就说明该时刻的设计还没有完成。
| 时刻 | 触发条件 | 变化内容 | 时长作用 | 触觉反馈 | 减少动态效果时的回退方式 |
|---|---|---|---|---|---|
| 按下 | 指针按下 | 按下变换 | 最短 | selectionClick | 立即变更状态,不做变换 |
| 拒绝 | 输入无效 | 有限幅度的抖动 + 消息 | 短暂,可中断 | lightImpact | 仅显示消息,不抖动 |
| 提交 | 状态已提交 | 变换本身 | 中等 | lightImpact | 立即显示最终状态 |
| 成功 | 任务完成 | 确认反馈 + 结果 | 较长,可跳过 | mediumImpact | 直接显示已完成的结果 |
| 增量 | 计数器递增 | 单次强调脉冲 | 短暂 | selectionClick 或静音 | 显示新数值,不做脉冲 |
| 过渡 | 路由变化 | 标志性过渡效果 | 中等 | 无 | 淡入淡出或无效果 |
此处的时长是作用类别,不是具体数值——具体数值由 design-system-structure 中的令牌负责。
确保触觉反馈恰好触发一次
// The event map lives in the theme; call sites name an event, never an intensity.
@immutable
class HapticTheme extends ThemeExtension<HapticTheme> {
const HapticTheme({this.select, this.commit, this.reject, this.success});
/// A null slot is a DECLARED silence — the design said no haptic here.
final Future<void> Function()? select;
final Future<void> Function()? commit;
final Future<void> Function()? reject;
final Future<void> Function()? success;
// copyWith / lerp omitted — see design-system-structure for the ThemeExtension contract.
}
// One central gate; no call site remembers to check the setting.
void fire(WidgetRef ref, Future<void> Function()? slot) {
if (!ref.read(hapticsEnabledProvider)) return;
slot?.call();
}
// At the commit — once, on the frame the state actually changed (rules 6, 8).
void onCommit(BuildContext context, WidgetRef ref, EditIntent intent) {
ref.read(itemsProvider.notifier).apply(intent); // state first
fire(ref, Theme.of(context).extension<HapticTheme>()!.commit);
}
绝不要在 AnimationController 监听器、onEnd 回调或逐项循环中调用 HapticFeedback.*——这三处都可能让单个事件变成一连串反馈。
反模式
- 没有按下状态的控件——每次「刚才点到了吗?」的双击都由此而来。
- 只在动画运行期间存在的状态——关闭动画后,应用仍必须正确、清晰易读。
- 没有为减少动态效果设计行为的动画,或功能代码在每个动画中重复读取平台标志。
- 在调用处散落
HapticFeedback.mediumImpact()——一次性将事件映射到槽位;临时添加的触觉反馈只会不断升级。 - 在动画监听器、
onEnd中,或在合并手势的每个项目上触发触觉反馈——每个已提交事件只触发一次,并且在提交帧触发。 - 用
heavyImpact表示错误——确认失败即可,不要惩罚用户。 - 无法跳过的庆祝动画,或在动画效果期间阻止输入——轻点即可立即结束动画。
- 循环播放的庆祝动画、从屏幕边缘生成的粒子,或数量不受限制的元素——为其设定预算。
- 「悲伤」的失败动画——用户已经知道失败了。
- 没有停止条件的
repeat()——离开路由、应用转入后台以及启用减少动态效果时都必须停止。 - 将声音作为唯一信号,或忽略平台静音行为的声音。
- RTL 下镜像内容画布——导航方向会翻转,但方向中性的画布不会。
完成定义
- 每次会提交状态的交互都在同一帧内得到确认。
- 每个涉及的时刻都有填写完整的声明行:触发条件、变化、时长作用、触觉槽位、减少动态效果时的回退行为。
- 禁用动画后,最终状态保持一致且仍可到达;任何内容都不能只靠动态效果呈现。
- 触觉反馈来自
HapticTheme映射,强度不超过对应事件的上限,每个已提交事件只在提交帧触发一次,并由一个中央开关控制。 -
null触觉槽位均为有意设置的静默,并已记录。 - 每个动画都可中断;轻点即可将其结束到最终状态;绝不阻止输入。
- 庆祝动画有明确的范围限制,从操作位置开始,只播放一次,绝不循环。
- 每个重复动画在离开路由、应用转入后台以及启用减少动态效果时,都有明确的停止条件。
- 声音(如果有)是冗余信号、时长简短、尊重静音模式,并由中央开关控制。
- 转场遵循阅读方向;方向中性的内容画布不会再次镜像。
相关技能
- 关于本技能使用的动态效果令牌、曲线槽位和
resolveMotion辅助函数(包括 Material 默认挂载的三个动画),请参阅design-system-structure。 - 关于使用
AnimationController通过repaint:驱动画布动画以及放置RepaintBoundary,请参阅custom-canvas-and-gestures。 - 关于某个时刻所反馈的状态——加载、空状态和错误状态的呈现、snackbar/banner/dialog 层级、撤销以及
SemanticsService.announce,请参阅ui-states-and-feedback。 - 关于本技能的「动态效果不能作为唯一信号」规则所对应的「颜色不能作为唯一信号」规则,以及读取平台无障碍标志,请参阅
accessibility-as-code。 - 关于释放资源、边界预算,以及使用 profile 模式测量任何感觉卡顿的内容,请参阅
flutter-performance。 - 关于 RTL 下的方向几何,请参阅
i18n-rtl-l10n。 - 关于构建结束时的检查流程——在启用和关闭减少动态效果的情况下,通过视频验证每个已声明的时刻,请参阅
design-review-workflow。
References
- Flutter API —
HapticFeedback: https://api.flutter.dev/flutter/services/HapticFeedback-class.html - Flutter API —
SystemSound: https://api.flutter.dev/flutter/services/SystemSound-class.html - Flutter API —
MediaQueryData.disableAnimations: https://api.flutter.dev/flutter/widgets/MediaQueryData/disableAnimations.html - Flutter — animations overview: https://docs.flutter.dev/ui/animations
- Material 3 — motion: https://m3.material.io/styles/motion/overview
- Apple HIG — playing haptics: https://developer.apple.com/design/human-interface-guidelines/playing-haptics
- WCAG 2.2 — Animation from Interactions (2.3.3): https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html