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 フィードバックではプロトタイプのように感じられます。
スコープの境界。 モーションのトークン — 時間の役割、カーブのスロット、生の値を許可しないルール、そして reduced motion の場合に resolveMotion が Duration.zero に集約される仕組み — は design-system-structure の担当です。ペインターやティッカーの仕組みは custom-canvas-and-gestures の担当です。メッセージをどのサーフェスで表示するか — ローディング、空の状態、エラー、スナックバー、バナー、ダイアログ、元に戻す操作、スクリーンリーダーへの通知 — は ui-states-and-feedback の担当です。このスキルが扱うのは、これらの判断を支える感覚的なレイヤーです。どのような瞬間があるのか、それぞれが何を確定するのか、触覚フィードバックをどう割り当てて発火させるのか、そしてアニメーションがなくても何が残るのかを定めます。
譲れないルール
-
確定を伴うすべての操作は、操作が起きたのと同じフレームで応答します。 指が触れた時点で、すぐに押下状態を表示します。確定時には、その確定に対応するフィードバックを続けて表示します。理由:非同期の結果が返るまで何も表示しないコントロールは、タップを受け付けなかったコントロールと見分けがつかず、ユーザーがもう一度タップしてしまいます。
-
モーションは状態を装飾するものであり、状態そのものではありません。 すべてのアニメーションを無効にしても、最終状態は同一で、正しく、到達可能でなければなりません。アニメーションの実行中にしか存在しない選択状態や、遷移中にしか判読できない値は、reduced motion を使うユーザーが毎回直面する不具合です。
-
瞬間に名前を付け、それぞれを意図的に設計します。 どのアプリにも、小規模なカタログがあります。押下の応答、入力の拒否、確定時の手応え、成功、進捗や連続達成数の増加、そして特徴的な画面遷移です。それぞれの瞬間について、トリガー、変化するもの、使用する時間の役割とカーブ、触覚フィードバックのスロット、そしてreduced motion 時のフォールバックの5つを定義します。指定されていない瞬間は「最小限」なのではなく、誰も選んでいないフレームワークのデフォルトです。
references/moment-catalog.mdを参照してください。 -
すべてのアニメーションについて、reduced motion 時の動作を設計時に定義します。 「小さな動きだから、誰も気にしない」はフォールバックではありません。仕組み(
resolveMotion→Duration.zero)はdesign-system-structureの担当ですが、クロスフェードで最終状態に移る、完了した状態で表示する、演出を完全に省くといった判断は、ここで瞬間ごとに行い、記録します。機能コードは、アニメーションのたびにプラットフォームフラグを読み直してはいけません。 -
触覚フィードバックは一元的な対応表に従い、呼び出し箇所に散らばらせません。
HapticTheme拡張では、各イベントに対してHapticFeedbackの動詞を最大1つ割り当てます。選択はselectionClick以下、確定はlightImpact/mediumImpact以下、拒否はlightImpact以下、成功はmediumImpact以下とし、heavyImpactは本当のマイルストーンに限ります。nullのスロットは明示的な無音であり、指定漏れではありません。理由:呼び出し箇所ごとに場当たり的に追加される触覚フィードバックは、強度が際限なく上がり、アプリが振動し続ける原因になります。 -
確定したイベントごとに、確定フレームで触覚フィードバックを1回だけ発火させます。 アニメーションのコールバック内やフレームごとには発火させません。また、まとめられたジェスチャーの入力ごとにも発火させません。ドラッグで発火させるのは、開始時と確定時の最大2回です。理由:フレームごとの触覚フィードバックは単なる振動になり、まとめられた入力ごとの触覚フィードバックは、素早いジェスチャーをガタガタとした振動にしてしまいます。
-
heavyImpactをエラー音として使ってはいけません。 失敗時に強度を上げるのは、UI で防げたはずのミスをユーザーに罰することになります。拒否は伝えますが、強調しません。 -
アプリ内の1つの切り替え設定で、祝福演出も含むすべての触覚フィードバックを制御します。 プラットフォームには大まかなシステムの触覚設定がありますが、触覚フィードバックを発火するアプリには独自のスイッチも必要です。また、呼び出し箇所ごとに確認するフラグではなく、中央で一元的に制御しなければなりません。
-
すべてのアニメーションは中断可能にします。 トランジション中やお祝いの演出中にタップされた場合は、ただちに終了状態にします。演出中は入力を受け付けない、という状態を決して作らず、成功を待たせないでください。理由:ユーザーがお祝いの演出を2回目に見るときには、演出を飛ばして先に進みたいものです。
-
お祝いの演出には上限を設けます。 要素数と次の操作可能な状態までの時間に上限を設け、画面の端ではなく操作した箇所から始め、1回だけ再生し、ループさせません。失敗状態には、ユーザーを責めるような動きを使わず、状況を認識したことを伝えます。理由:無制限のお祝いの演出は、ユーザーが求めていないロード画面と同じです。
-
繰り返しアニメーションや環境アニメーションには、停止条件を必ず定義します。 対象のルートが現在表示されていないとき、アプリがバックグラウンドにあるとき、またはモーションを減らす設定が有効なときに停止します。停止条件のない
repeat()は、どのテストでも検出できず、どのレビュアーも気づかないバッテリー消費の不具合です。 -
フィードバックは複数の経路で重ねて伝えます。 モーションを減らす設定が有効でも、残された経路である触覚と音は使えるため、状態変化をモーションだけで伝えることはできません。(色だけでの伝達は
accessibility-as-codeが担当します。これは、それと同じルールをモーションだけでの伝達に適用するものです。) -
音を使う場合は、3つ目の経路とし、最初の経路にはしません。 プラットフォームの消音時の動作を尊重し、短く保ち、何が起きたかを理解するために音を必要とせず、触覚と同様に中央の切り替え設定で制御します。
-
トランジションは読み取り方向に従いますが、固定されたコンテンツ面の表示方向は反転しません。 水平方向のプッシュはナビゲーション方向に合わせて RTL で反転します。一方、独自のレイアウトが読み取り方向に依存しないキャンバス、ボード、チャート、メディア面はそのまま維持します。方向のジオメトリは
i18n-rtl-l10nが担当します。
モーメントの宣言
実装する前に、各モーメントを記録します。表そのものが仕様です。列を埋められない実装は、そのモーメントの設計が完了していません。
| モーメント | トリガー | 変化するもの | 時間の役割 | 触覚 | モーションを減らす設定時の代替 |
|---|---|---|---|---|---|
| 押下 | ポインターを押す | 押下時の変形 | 最短 | selectionClick | 即座に状態を反映し、変形はなし |
| 拒否 | 無効な入力 | 上限付きの揺れ+メッセージ | 短く、中断可能 | lightImpact | メッセージのみ。揺れはなし |
| 確定 | 状態の確定 | 変化そのもの | 中程度 | lightImpact | 終了状態を即座に表示 |
| 成功 | タスクの完了 | 認識の表示+結果 | 長く、スキップ可能 | mediumImpact | すでに完了した状態で結果を表示 |
| 増加 | カウンターの進行 | 強調のパルスを1回 | 短い | selectionClick またはなし | 新しい値を表示し、パルスはなし |
| トランジション | ルートの変更 | 特徴的なトランジション | 中程度 | なし | クロスフェードまたはなし |
ここで示す時間は数値ではなく、役割です。数値は design-system-structure が管理するトークンです。
触覚フィードバックを正確に1回発生させる
// 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);
}
HapticFeedback.* を AnimationController のリスナー、onEnd コールバック、またはアイテムごとのループから呼び出さないでください。これら3か所では、1つのイベントが連続して何度も発生してしまいます。
アンチパターン
- 押下状態のないコントロール — 「操作が登録された?」と確認するためのダブルタップは、すべてここから始まります。
- アニメーション中にしか存在しない状態 — アニメーションをオフにしても、アプリが正しく動作し、内容を読み取れる必要があります。
- 視差効果を減らす設定時の動作が設計されていないアニメーション、または機能コードがアニメーションごとにプラットフォームのフラグを読み直すこと。
HapticFeedback.mediumImpact()を呼び出し箇所に散りばめること — イベントを一度だけスロットに割り当てます。場当たり的な触覚フィードバックは、強度が上がる一方です。- アニメーションリスナー、
onEnd、またはまとめられたジェスチャーの項目ごとに発生する触覚フィードバック — 確定したイベントごとに1回、確定フレームで発生させます。 - エラーに
heavyImpactを使うこと — 失敗を知らせるのであって、罰してはいけません。 - スキップできない祝福アニメーション、または演出中の入力ブロック — タップですぐに解決できるようにします。
- ループする祝福アニメーション、画面端から出現するパーティクル、または無制限の要素数 — 上限を設けます。
- 「悲しげな」失敗アニメーション — ユーザーにはすでにわかっています。
- 停止条件のない
repeat()— 画面遷移後、バックグラウンド中、視差効果を減らす設定時には停止する必要があります。 - 唯一の合図となるサウンド、またはプラットフォームのサイレント動作を無視するサウンド。
- RTLでコンテンツ面を反転すること — ナビゲーションの方向は反転しますが、方向に依存しないキャンバスは反転しません。
完了の定義
- 確定を伴うすべての操作に、同じフレーム内で応答します。
- 対象となるすべての場面で宣言の各行が記入されています。内容は、トリガー、変更、時間の役割、触覚フィードバックのスロット、視差効果を減らす設定時の代替動作です。
- アニメーションを無効にしても最終状態は同じで、そこに到達できます。モーションだけに依存するものはありません。
- 触覚フィードバックは
HapticThemeマップから呼び出され、イベントの強度上限以下で、確定したイベントごとに1回、確定フレームで発生し、中央の1つの切り替えで制御されます。 -
nullの触覚フィードバックのスロットは意図的な無音として設定され、その旨が記録されています。 - すべてのアニメーションを中断できます。タップすると最終状態になり、入力がブロックされることはありません。
- 祝福アニメーションには上限があり、操作した場所から始まり、1回だけ再生され、ループしません。
- 繰り返しアニメーションには、画面遷移後、バックグラウンド中、視差効果を減らす設定時の明示的な停止条件があります。
- サウンドを使う場合は、補助的で、短く、サイレントモードに対応し、中央で制御されます。
- 画面遷移は読み取り方向を尊重し、方向に依存しないコンテンツ面は再反転しません。
関連スキル
- モーションのトークン、カーブのスロット、このスキルで使用する
resolveMotionヘルパーについては、design-system-structureを参照してください。Materialがデフォルトでマウントする3つのアニメーションも含まれます。 AnimationControllerからrepaint:としてキャンバスアニメーションを動かす方法や、RepaintBoundaryの配置については、custom-canvas-and-gesturesを参照してください。- 場面がフィードバックする対象となる状態については、
ui-states-and-feedbackを参照してください。読み込み/空/エラーの描画、スナックバー/バナー/ダイアログの段階的な使い分け、取り消し、SemanticsService.announceについて説明しています。 - このスキルの「モーションだけに依存しない」というルールの元になった「色だけに依存しない」というルールや、プラットフォームのアクセシビリティフラグの読み取り方については、
accessibility-as-codeを参照してください。 - 破棄、境界の予算管理、重く感じる処理のプロファイルモードでの計測については、
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