mcpField
M
motion-and-haptics

motion-and-haptics

github.com/zakariaf设计与 UI63160 次安装5 周前更新
代码仓库

关于 motion-and-haptics

面向 Flutter 应用开发者的动效与触觉反馈设计技能,规定交互时刻、动画的减少动态效果方案及触觉映射。重点是让状态在动画关闭时仍然成立,并确保反馈及时且每次提交只触发一次。

主要功能

  1. 01交互时刻声明规范
  2. 02减少动态效果的降级方案
  3. 03集中式触觉事件映射
  4. 04每次提交触发一次触觉
  5. 05可中断且有界的庆祝动画
  6. 06重复动画停止条件

适用场景

  1. 01设计按钮按下与提交反馈
  2. 02为拒绝输入和成功状态配置触觉
  3. 03适配减少动态效果设置
  4. 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 管辖。本技能负责这些决策所依托的感官层:有哪些时机、每个时机要实现什么、如何映射和触发触觉反馈,以及动画不可用时保留什么。

不容妥协的规则

  1. 每次会提交的交互都必须在发生的同一帧得到确认。 手指按下时立即显示按压状态;提交时再给出该提交对应的反馈。原因:如果控件要等异步结果返回后才显示任何反馈,用户就无法判断控件是否已收到点按,于是会再次点按。

  2. 动效装饰状态,而不是状态本身。 禁用所有动画时,最终状态必须保持一致、正确且可达。选择状态只有在动画运行时才存在,或数值只有在过渡期间才看得清,都是减少动态效果的用户每次都会遇到的错误。

  3. 为各个时机命名,并有意识地逐一设计。 每个应用都有一份简短目录——按压确认、输入被拒绝、提交后的回馈、成功、进度或连续记录递增,以及标志性的屏幕过渡。每个时机都要明确五项内容:其触发条件、变化内容、使用的时长角色和曲线、其触觉反馈槽位,以及其减少动态效果时的回退方案。未指定的时机并不代表“极简”——它只是由无人选择的框架默认行为。参见 references/moment-catalog.md。

  4. 设计时就要明确每个动画在减少动态效果时的行为。 “动画很小,没人会介意”不算回退方案。机制(resolveMotion → Duration.zero)归 design-system-structure 管辖;而每个时机的具体决策——交叉淡化至最终状态、直接显示已完成状态,或完全省略点缀动效——在此处逐一制定并记录。功能代码不会为每个动画重新读取平台标志。

  5. 触觉反馈应有统一映射,而非散落各处的调用。 HapticTheme 扩展会将每个事件映射到至多一个 HapticFeedback 动词:选择 ≤ selectionClick,提交 ≤ lightImpact/mediumImpact,拒绝 ≤ lightImpact,成功 ≤ mediumImpact,而 heavyImpact 仅用于真正的里程碑。null 槽位代表明确静默,而非遗漏。原因:在调用点随意添加的触觉反馈,其强度总会逐渐升高,直到整个应用都在嗡嗡作响。

  6. 每个已提交事件仅触发一次触觉反馈,并在提交帧触发。 绝不在动画回调中触发,不逐帧触发,也不针对合并手势中的每次输入分别触发——拖动最多只在开始和提交时触发反馈。原因:逐帧触发会变成震动;对合并输入逐个触发,会让快速手势变成一阵乱震。

  7. 绝不用 heavyImpact 表示错误。 用更强的反馈来惩罚失败,会让用户为 UI 本可以避免的错误受罚。应确认用户操作被拒绝,而不是放大反馈。

  8. 应用内的一个开关应控制所有触觉反馈,包括庆祝效果。 平台通常有自己的系统级触觉设置;需要触发触觉反馈的应用也必须提供自己的开关,而且该开关必须是统一的中央门控,而不是让每个调用点都记得检查的标志。

  9. 每个动画都可中断。 在过渡或庆祝动画期间轻点,应立即将其结束到最终状态。切勿在炫技动画期间阻塞输入,也绝不要让用户等待庆祝动画结束。原因:用户第二次看到庆祝动画时,只想让它快点结束。

  10. 庆祝动画必须有预算。 限定元素数量,限定进入下一个可交互状态前的时长;动画从操作发生的位置开始,而非从屏幕边缘开始;只播放一次,绝不循环。失败状态只需确认,不要带有羞辱意味的动作。原因:没有上限的庆祝动画,就像用户并未要求的加载画面。

  11. 每个重复或环境动画都必须声明停止条件。 当其所在路由不再是当前路由、应用进入后台或启用减少动态效果时,动画都应停止。没有停止条件的 repeat() 是一个电池隐患,既不会被测试发现,也不会被代码审查者看到。

  12. 通过多个渠道提供反馈。 启用减少动态效果时,触觉反馈和声音仍然有效,正因为它们是剩余的反馈渠道——因此,绝不能只靠动态效果传达状态变化。(仅靠颜色的问题由 accessibility-as-code 负责;仅靠动态效果则是同一规则的另一部分。)

  13. 如果提供声音,它只能作为第三种反馈渠道,绝不能是第一种。 遵循平台的静音行为,保持简短,绝不能要求用户依靠声音理解发生了什么,并通过与触觉反馈相同类型的中央开关控制声音。

  14. 过渡效果应遵循阅读方向;固定内容表面不应重新镜像。 水平推入效果在 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

评论

支持 Markdown