motion-and-haptics
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMotion and haptics
动效与触觉反馈
What fires, when, exactly once, and how it degrades. Feedback is the layer users feel rather than
read — an app whose taps land, whose errors are felt, and whose success is acknowledged reads as
finished; the same app with default Material feedback reads as a prototype.
Scope boundary. Motion tokens — duration roles, curve slots, the no-raw-values gate, and
collapsing to under reduced motion — are owned by
. Painter/ticker mechanics are owned by .
Which surface a message uses — loading, empty, error, snackbar, banner, dialog, Undo, and
screen-reader announcements — is owned by . This skill owns the sensory
layer those decisions ride on: which moments exist, what each one commits to, how haptics are
mapped and fired, and what survives when the animation does not.
resolveMotionDuration.zerodesign-system-structurecustom-canvas-and-gesturesui-states-and-feedback什么反馈、何时触发、只触发一次,以及功能退化时如何降级。反馈是用户通过感知而非阅读获得的层面——点击有落点、错误能感知、成功有确认的应用会显得已经完成;同一个应用若使用 Material 默认反馈,则会显得像原型。
范围边界。 动效 token——时长角色、曲线槽位、禁止使用原始数值的约束,以及在减少动态效果下让 归零为 ——由 负责。Painter/ticker 机制由 负责。某条消息应使用哪种呈现载体——loading、empty、error、snackbar、banner、dialog、Undo 和屏幕阅读器播报——由 负责。本 skill 负责这些决策所承载的感知层:存在哪些交互时刻、每个时刻要确认什么结果、触觉反馈如何映射和触发,以及动画未运行时仍保留什么。
resolveMotionDuration.zerodesign-system-structurecustom-canvas-and-gesturesui-states-and-feedbackNon-negotiable rules
不可妥协的规则
-
Every committing interaction is acknowledged in the same frame it happens. Finger-down shows a press state immediately; the commit's own feedback follows on commit. WHY: a control that waits for an async result before showing anything is indistinguishable from a control that did not register the tap, and the user taps again.
-
Motion decorates state; it is never the state. The end state must be identical, correct, and reachable with all animations disabled. A selection that only exists while its animation runs, or a value that is only legible mid-transition, is a bug that reduced-motion users hit every time.
-
Name your moments and design each one deliberately. Every app has a small catalog — press acknowledgment, rejected input, the commit payoff, success, progress or streak increment, and the signature screen transition. Each moment declares five things: its trigger, what changes, which duration role and curve it spends, its haptic slot, and its reduced-motion fallback. A moment left unspecified is not "minimal" — it is the framework default, chosen by nobody. See.
references/moment-catalog.md -
Every animation declares its reduced-motion behaviour at design time. "It is small, nobody will mind" is not a fallback. The mechanism (→
resolveMotion) belongs toDuration.zero; the decision — cross-fade to the end state, appear already-complete, omit the flourish entirely — is made per moment, here, and written down. Feature code never re-reads the platform flag per animation.design-system-structure -
Haptics are a map, not scattered calls. Aextension maps each event to at most one
HapticThemeverb: selection ≤HapticFeedback, commit ≤selectionClick/lightImpact, rejection ≤mediumImpact, success ≤lightImpact, andmediumImpactreserved for a genuine milestone. AheavyImpactslot is a declared silence, not an omission. WHY: haptics added ad hoc at call sites always drift upward in intensity until the app buzzes.null -
Fire one haptic per committed event, on the commit frame. Never inside an animation callback, never per frame, never once per input in a coalesced gesture — a drag emits at most an engage and a commit. WHY: a haptic per frame is a vibration, and a haptic per coalesced input turns a fast gesture into a rattle.
-
is never the error sound. Escalating intensity for failure punishes the user for a mistake the UI could have prevented. Rejection is acknowledged, not amplified.
heavyImpact -
One in-app toggle gates every haptic, celebrations included. Platforms have their own coarse system-haptics setting; an app that fires haptics needs its own switch too, and it must be a single central gate rather than a flag each call site remembers to check.
-
Every animation is interruptible. A tap during a transition or celebration resolves it to its end state immediately. Never block input for the duration of a flourish, and never make the user wait out a success. WHY: the second time a user sees a celebration they want to be past it.
-
Celebrations have a budget. Bounded element count, bounded time to the next interactive state, originating from the point of action rather than the screen edges, played once, never looped. Failure states get acknowledgment with no shaming motion. WHY: an unbounded celebration is a load screen the user did not ask for.
-
Every repeating or ambient animation declares a stop condition. It stops when its route is not current, when the app is backgrounded, and under reduced motion. Awith no stop condition is a battery bug that no test will catch and no reviewer will see.
repeat() -
Feedback is redundant across channels. Haptics and sound survive reduced motion precisely because they are the remaining channels — so a state change may never be carried by motion alone. (Color-alone is owned by; this is the motion-alone half of the same rule.)
accessibility-as-code -
Sound, if any, is the third channel and never the first. Respect the platform's silent behaviour, keep it short, never require it to understand what happened, and gate it behind the same kind of central toggle as haptics.
-
Transitions follow reading direction; a fixed content surface does not re-mirror. A horizontal push flips under RTL because navigation is directional; a canvas, board, chart, or media surface whose own layout is direction-neutral stays as it is. Direction geometry is owned by.
i18n-rtl-l10n
-
每一次提交型交互都在发生当帧得到确认。 手指按下时立即显示按压状态;提交发生时,随后给出该提交自身的反馈。原因: 如果控件在显示任何反馈之前一直等待异步结果,那么它与根本没有识别到这次点击的控件毫无区别,用户就会再次点击。
-
动效只为状态作装饰;它绝不能成为状态本身。 关闭所有动画后,最终状态仍必须完全相同、正确且可达。如果某个选中状态只在动画运行期间存在,或某个值只有在过渡过程中才能看清,那就是缺陷,而减少动态效果的用户每次都会遇到它。
-
为交互时刻命名,并有意识地设计每一个时刻。 每个应用都有一个小目录——按压确认、输入拒绝、提交完成后的回报、成功、进度推进或连续记录增长,以及标志性屏幕转场。每个时刻都要声明五项内容:其 触发条件、发生的变化、所采用的 时长角色 与曲线、其 触觉槽位,以及其 减少动态效果回退方案。未明确指定的时刻并非“极简”——它只是无人选择的框架默认行为。参见。
references/moment-catalog.md -
每个动画都要在设计阶段声明其减少动态效果行为。 “效果很小,没人会介意”不能算作回退方案。机制(→
resolveMotion)由Duration.zero负责;而具体决策——交叉淡化到最终状态、直接以完成状态出现,或完全省略装饰效果——则在这里按每个时刻作出并记录在案。功能代码绝不能为每个动画重新读取平台标志。design-system-structure -
触觉反馈是一张映射,而不是散落的调用。扩展会将每个事件最多映射到一种
HapticTheme动作:选择 ≤HapticFeedback,提交 ≤selectionClick/lightImpact,拒绝 ≤mediumImpact,成功 ≤lightImpact,而mediumImpact仅用于真正的里程碑。heavyImpact槽位表示明确声明的静默,并非遗漏。 原因: 在调用点临时添加触觉反馈,强度总会不断升级,直到整个应用开始嗡嗡震动。null -
每个已提交事件只触发一次触觉反馈,并在提交帧触发。 绝不能放在动画回调中,绝不能每帧触发,也绝不能对合并输入逐次触发——一次拖动最多产生 engage 和 commit 两个事件。原因: 每帧一次触觉反馈就成了一种持续振动,而对每次合并输入都触发,则会把快速手势变成一连串震动。
-
绝不能作为错误反馈。 失败时提升强度,等于因为用户犯了一个本可由 UI 避免的错误而惩罚用户。拒绝操作应得到确认,而不是被强化。
heavyImpact -
应用内一个开关控制所有触觉反馈,包括庆祝效果。 平台自身有粗粒度的系统触觉设置;需要触发触觉反馈的应用也应有自己的开关,而且它必须是一个中央控制门,而不是让每个调用点各自记得检查的标志。
-
所有动画都可中断。 在转场或庆祝效果期间点击,应立即让其到达最终状态。绝不能为了装饰效果阻塞用户输入,也绝不能让用户等着成功动画结束。原因: 用户第二次看到庆祝效果时,只会希望赶紧让它过去。
-
庆祝效果有预算。 元素数量有上限,距离下一个可交互状态的时间有上限,动画从操作触发点而非屏幕边缘开始,只播放一次,绝不循环。失败状态只提供确认,不使用让用户难堪的动效。原因: 无限度的庆祝效果就是用户根本没要求的加载界面。
-
每个重复或常驻动效都要声明停止条件。 当当前路由不再处于活动状态、应用进入后台或启用减少动态效果时,它必须停止。没有停止条件的是一个耗电缺陷,任何测试都发现不了,任何评审也看不到。
repeat() -
反馈在多个通道上冗余呈现。 触觉和声音在减少动态效果下仍能保留,正是因为它们是剩余的反馈通道——因此状态变化绝不能只靠动效传达。(仅靠颜色传达由负责;这里是同一条规则中仅靠动效传达的部分。)
accessibility-as-code -
声音(如果有)是第三通道,绝不能是首要通道。 尊重平台的静音行为,保持声音简短,绝不能要求用户必须通过声音才能理解发生了什么,并将其置于与触觉反馈同类的中央开关之后。
-
转场遵循阅读方向;固定的内容表面不应被重新镜像。 水平推推动画在 RTL 下会翻转,因为导航具有方向性;而画布、棋盘、图表或媒体表面如果自身布局与方向无关,则保持不变。方向几何由负责。
i18n-rtl-l10n
The moment declaration
交互时刻声明
Write each moment down before implementing it. The table is the specification — an implementation
that cannot fill a column has not finished designing that moment.
| Moment | Trigger | What changes | Duration role | Haptic | Reduced-motion fallback |
|---|---|---|---|---|---|
| Press | pointer down | press transform | shortest | | instant state, no transform |
| Reject | invalid input | bounded shake + message | short, interruptible | | message only, no shake |
| Commit | state committed | the transformation itself | medium | | end state appears instantly |
| Success | task complete | acknowledgment + result | long, skippable | | result appears already complete |
| Increment | counter advances | one emphasis pulse | short | | new value, no pulse |
| Transition | route change | the signature transition | medium | none | cross-fade or none |
Durations here are roles, not numbers — the numbers are tokens owned by .
design-system-structure实现每个交互时刻之前,先把它写下来。这张表就是规范——如果某项实现无法填写完表格中的所有列,就说明该交互时刻的设计尚未完成。
| 交互时刻 | 触发条件 | 发生变化的内容 | 时长角色 | 触觉反馈 | 减少动态效果回退方案 |
|---|---|---|---|---|---|
| Press(按压) | pointer down(指针按下) | press transform(按压变换) | shortest(最短) | | instant state, no transform(即时状态,无变换) |
| Reject(拒绝) | invalid input(无效输入) | bounded shake + message(有界抖动 + 消息) | short, interruptible(短,可中断) | | message only, no shake(仅显示消息,无抖动) |
| Commit(提交) | state committed(状态已提交) | the transformation itself(变换本身) | medium(中等) | | end state appears instantly(最终状态立即出现) |
| Success(成功) | task complete(任务完成) | acknowledgment + result(确认反馈 + 结果) | long, skippable(长,可跳过) | | result appears already complete(结果直接以完成状态出现) |
| Increment(增量) | counter advances(计数器递增) | one emphasis pulse(一次强调脉冲) | short(短) | | new value, no pulse(显示新值,不做脉冲) |
| Transition(转场) | route change(路由变化) | the signature transition(标志性转场) | medium(中等) | none(无) | cross-fade or none(交叉淡化或不转场) |
这里的时长是角色,而不是具体数值——具体数值是由 管理的 token。
design-system-structureFiring a haptic exactly once
恰好触发一次触觉反馈
dart
// The event map lives in the theme; call sites name an event, never an intensity.
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);
}Never call from an listener, an callback, or a
per-item loop — those are the three places a single event becomes a burst.
HapticFeedback.*AnimationControlleronEnddart
// The event map lives in the theme; call sites name an event, never an intensity.
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);
}绝不要从 监听器、 回调或逐项循环中调用 ——正是这三处会让单个事件变成一阵连续触发。
AnimationControlleronEndHapticFeedback.*Anti-patterns
反模式
- A control with no press state — every "did that register?" double-tap starts here.
- State that only exists while an animation runs — turn animations off and the app must still be correct and legible.
- An animation with no designed reduced-motion behaviour, or feature code re-reading the platform flag per animation.
- sprinkled at call sites — map events to slots once; ad-hoc haptics only ever escalate.
HapticFeedback.mediumImpact() - A haptic in an animation listener, an , or per item in a coalesced gesture — one per committed event, on the commit frame.
onEnd - for an error — acknowledge failure, do not punish it.
heavyImpact - An un-skippable celebration, or blocked input during a flourish — a tap resolves it instantly.
- A looping celebration, edge-spawned particles, or an unbounded element count — budget it.
- A "sad" failure animation — the user already knows.
- with no stop condition — it must stop off-route, backgrounded, and under reduced motion.
repeat() - Sound as the only signal, or sound that ignores the platform's silent behaviour.
- A mirrored content surface under RTL — navigation direction flips, a direction-neutral canvas does not.
- 没有按压状态的控件——所有“是不是没响应?”导致的二次点击都由此开始。
- 只在动画运行期间存在的状态——关闭动画后,应用仍必须正确且清晰可读。
- 没有设计减少动态效果行为的动画,或功能代码为每个动画重新读取平台标志。
- 在调用点随意散落 ——集中映射事件与槽位;临时添加的触觉反馈只会不断增强。
HapticFeedback.mediumImpact() - 在动画监听器、或合并手势的逐项处理中触发触觉反馈——每个已提交事件在提交帧只触发一次。
onEnd - 对错误使用 ——确认失败,不要惩罚用户。
heavyImpact - 不可跳过的庆祝效果,或在装饰效果期间阻塞输入——轻触应立即让动画到达最终状态。
- 循环播放的庆祝效果、从屏幕边缘生成的粒子或无上限的元素数量——都要为其设定预算。
- “悲伤”的失败动画——用户已经知道失败了。
- 没有停止条件的 ——离开当前路由、进入后台和启用减少动态效果时都必须停止。
repeat() - 将声音作为唯一信号,或声音无视平台的静音行为。
- 在 RTL 下被镜像的内容表面——导航方向应翻转,但方向中立的画布不应翻转。
Definition of done
完成标准
- Every committing interaction acknowledges within the same frame.
- Every moment touched has a filled-in declaration row: trigger, change, duration role, haptic slot, reduced-motion fallback.
- The end state is identical and reachable with animations disabled; nothing is motion-only.
- Haptics come from the map, are at or below the intensity ceiling for their event, fire once per committed event on the commit frame, and are gated by one central toggle.
HapticTheme - haptic slots are deliberate silences, recorded as such.
null - Every animation is interruptible; a tap resolves it to its end state; input is never blocked.
- Celebrations are bounded, originate from the point of action, play once, and never loop.
- Every repeating animation has an explicit stop condition for off-route, backgrounded, and reduced motion.
- Sound (if any) is redundant, short, silent-mode-aware, and centrally gated.
- Transitions respect reading direction; direction-neutral content surfaces are not re-mirrored.
- 每一次提交型交互都在同一帧内得到确认。
- 本次涉及的每个交互时刻都有一行填写完整的声明:触发条件、变化、时长角色、触觉槽位、减少动态效果回退方案。
- 关闭动画后,最终状态仍然相同且可达;没有任何信息只能通过动效呈现。
- 触觉反馈来自 映射,强度不超过相应事件的强度上限,在提交帧上每个已提交事件只触发一次,并统一受一个中央开关控制。
HapticTheme - 触觉槽位是刻意设置的静默,并已如实记录。
null - 所有动画都可中断;轻触会立即使其到达最终状态;输入绝不会被阻塞。
- 庆祝效果设有边界,从操作触发点开始,只播放一次,绝不循环。
- 每个重复动画在离开当前路由、进入后台和启用减少动态效果时都有明确的停止条件。
- 声音(如果有)采用冗余通道、持续时间短、感知静音模式,并统一受中央开关控制。
- 转场遵循阅读方向;方向中立的内容表面不会被重新镜像。
Related skills
相关 skill
- See for the motion tokens, curve slots, and the
design-system-structurehelper this skill spends — including the three animations Material mounts by default.resolveMotion - See for driving canvas animation from an
custom-canvas-and-gesturesasAnimationControllerand forrepaint:placement.RepaintBoundary - See for the state a moment is feedback about — loading/empty/error rendering, the snackbar/banner/dialog ladder, Undo, and
ui-states-and-feedback.SemanticsService.announce - See for the color-alone rule this skill's motion-alone rule mirrors, and for reading platform accessibility flags.
accessibility-as-code - See for disposal, boundary budgeting, and profile-mode measurement of anything that feels heavy.
flutter-performance - See for directional geometry under RTL.
i18n-rtl-l10n - See for the end-of-build pass where every declared moment is verified on video with reduced motion on and off.
design-review-workflow
- 本 skill 所使用的动效 token、曲线槽位和 helper——包括 Material 默认挂载的三种动画——参见
resolveMotion。design-system-structure - 关于使用 通过
AnimationController驱动 canvas 动画以及repaint:的放置,参见RepaintBoundary。custom-canvas-and-gestures - 关于交互时刻所反馈的状态——loading/empty/error 渲染、snackbar/banner/dialog 层级、Undo 和 ——参见
SemanticsService.announce。ui-states-and-feedback - 本 skill 的“仅靠动效”规则与“仅靠颜色”规则互为镜像;关于后者及读取平台无障碍功能标志,参见 。
accessibility-as-code - 关于资源释放、边界预算,以及对任何感觉沉重的内容进行 profile 模式测量,参见 。
flutter-performance - 关于 RTL 下的方向几何,参见 。
i18n-rtl-l10n - 关于在构建结束阶段通过录屏分别开启和关闭减少动态效果来验证每个已声明的交互时刻,参见 。
design-review-workflow
References
参考资料
- Flutter API — : https://api.flutter.dev/flutter/services/HapticFeedback-class.html
HapticFeedback - Flutter API — : https://api.flutter.dev/flutter/services/SystemSound-class.html
SystemSound - Flutter API — : https://api.flutter.dev/flutter/widgets/MediaQueryData/disableAnimations.html
MediaQueryData.disableAnimations - 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
- Flutter API — : https://api.flutter.dev/flutter/services/HapticFeedback-class.html
HapticFeedback - Flutter API — : https://api.flutter.dev/flutter/services/SystemSound-class.html
SystemSound - Flutter API — : https://api.flutter.dev/flutter/widgets/MediaQueryData/disableAnimations.html
MediaQueryData.disableAnimations - Flutter — 动画概览: https://docs.flutter.dev/ui/animations
- Material 3 — 动效: https://m3.material.io/styles/motion/overview
- Apple HIG — 播放触觉反馈: https://developer.apple.com/design/human-interface-guidelines/playing-haptics
- WCAG 2.2 — 交互动画 (2.3.3): https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html