Kenapa Motion Penting
Fondasi motion language MASA
DS v2.0 Jun 2026

Kenapa Motion Penting

Motion bukan dekorasi. Di MASA, motion adalah bahasa — cara app berkomunikasi bahwa sesuatu telah terjadi, sedang terjadi, atau siap terjadi. Tanpa sistem, setiap developer membuat keputusan sendiri dan hasilnya tidak konsisten.

MASA Motion Philosophy: "Calm, purposeful, respectful." Motion di MASA harus tenang seperti brand personality-nya — tidak agresif, tidak berlebihan, tidak mengalihkan dari ibadah dan komunitas. Setiap animasi harus menjawab: apakah ini membantu user memahami apa yang terjadi?

3 Fungsi Motion di MASA

01 · Komunikasi
Feedback & Status
User tahu aksi mereka diterima. Loading, sukses, error — semua dikomunikasikan melalui motion sebelum state change selesai.
02 · Orientasi
Spatial Awareness
User tahu di mana mereka berada dalam app. Page transition membangun mental model navigasi — naik, turun, masuk, keluar.
03 · Emosi
Delight & Trust
Sedekah sukses terasa lebih bermakna dengan spring pop. Streak milestone terasa lebih memuaskan dengan bounce. Trust dibangun melalui konsistensi motion.

Tanpa sistem motion, yang terjadi:

✕ Dev A pakai 300ms ease untuk semua
✕ Dev B pakai 0.5s linear karena default
✕ Dev C tidak pakai animasi sama sekali
✕ Hasil: app terasa "patah" di satu layar, "lambat" di layar lain, "dingin" di layar lain
✕ Tidak ada yang bisa review karena tidak ada standar

5 Motion Principles

Prinsip ini adalah filter keputusan. Sebelum menambah animasi apapun, jawab: apakah sesuai dengan 5 prinsip ini?

P1
Purposeful — Setiap animasi punya alasan
Tidak ada animasi hanya untuk terlihat keren. Setiap motion harus menjawab: ini membantu user melakukan apa? Jika tidak bisa dijawab, hapus animasinya.
Test: "Jika animasi ini dihapus, apakah user lebih bingung?" → Kalau tidak, hapus.
P2
Swift — Lebih cepat dari yang kamu kira
User tidak ingin menunggu animasi selesai. Standard transition: 200–300ms. Di atas 400ms sudah terasa lambat untuk aksi yang sering dilakukan. Animasi yang lambat mengurangi perceived performance.
Rule: Animasi yang diulang ratusan kali sehari harus ≤200ms.
P3
Natural — Ikuti hukum fisika
Objek di alam tidak bergerak linear. Mereka akselerasi dan deselerasi. Easing curve yang baik membuat motion terasa "nyata" dan tidak robotic. Bottom sheet masuk dengan decelerate (sudah cepat, melambat). Notifikasi keluar dengan accelerate (melambat, lalu pergi cepat).
P4
Respectful — Hormati pilihan aksesibilitas user
Beberapa user punya vestibular disorder atau motion sensitivity. Semua animasi WAJIB bisa dimatikan dengan prefers-reduced-motion. Ini bukan optional — ini legal obligation di banyak negara dan etika dasar.
Rule: Setiap animasi HARUS punya fallback di reduced-motion mode.
P5
Consistent — Sama di seluruh app
Bottom sheet selalu slide dari bawah. Success state selalu spring pop. Error selalu shake. Konsistensi membangun muscle memory user — mereka tahu apa yang akan terjadi sebelum terjadi. Ini membangun trust.

Duration Tokens

7 tingkat durasi dari 50ms hingga 1000ms. Pilih berdasarkan seberapa sering animasi diulang dan seberapa besar perubahan visual yang terjadi.

Token
Visual scale
Value
Penggunaan
--dur-instant
50ms
Hover state, focus ring, button pressed state. Respons langsung — user tidak merasakan delay.
--dur-fast
100ms
Toggle switch, checkbox, ripple. Aksi kecil yang dilakukan berkali-kali per sesi.
--dur-normal
200ms
Default untuk sebagian besar UI. Card expand, input focus, tab switch. Terasa natural tanpa menunggu.
--dur-moderate
300ms
Bottom sheet open, dialog appear, page push forward. Perubahan yang lebih besar butuh waktu sedikit lebih lama.
--dur-slow
500ms
Bottom sheet dismiss, large modal, Sedekah success state. Perubahan signifikan yang butuh perhatian user.
--dur-gentle
700ms
Onboarding transitions, splash exit, impact card reveal. Momen yang sengaja dibuat "berasa".
--dur-patient
1000ms
Streak milestone celebration, Qurban confirmation. Momen emosional — user harus benar-benar merasakannya. Gunakan sangat jarang.
Aturan pemilihan durasi:
Aksi harian berulang (toggle, tap, scroll) → 50–200ms
Perubahan layar (push, modal, sheet) → 200–300ms
Momen emosional (sukses, milestone, konfirmasi penting) → 500–1000ms
Jika ragu: mulai dari 200ms, naikkan hanya jika terasa terlalu tiba-tiba.

Live demo — rasakan perbedaannya

50ms (instant)
200ms (normal) ★
500ms (slow)
1000ms (patient)

Token CSS — cara pakai

/* lib/core/tokens/motion.dart */ class MasaDuration { static const instant = Duration(milliseconds: 50); static const fast = Duration(milliseconds: 100); static const normal = Duration(milliseconds: 200); // default static const moderate = Duration(milliseconds: 300); static const slow = Duration(milliseconds: 500); static const gentle = Duration(milliseconds: 700); static const patient = Duration(milliseconds: 1000); }

Easing Curves

6 kurva easing. Pilih berdasarkan konteks: objek masuk (decelerate), objek keluar (accelerate), atau objek berpindah dalam layar (standard). Jangan gunakan linear kecuali untuk loading shimmer.

standard DEFAULT
cubic-bezier(0.4, 0.0, 0.2, 1)
Akselerasi lalu deselerasi. Gerakan terasa natural dan responsif. Mulai lambat, cepat di tengah, melambat di akhir.
Tab switch, card expand, chip select, sebagian besar UI transitions
decelerate
cubic-bezier(0.0, 0.0, 0.2, 1)
Masuk cepat, berhenti lembut. Seperti objek yang datang dari jauh dan mendarat. Terasa "hadir".
Bottom sheet open, modal appear, page push (element masuk ke layar)
accelerate
cubic-bezier(0.4, 0.0, 1, 1)
Mulai lambat, pergi cepat. Seperti objek yang pergi dan meninggalkan. Terasa "pergi".
Bottom sheet dismiss, dialog keluar, page pop (element keluar dari layar)
sharp
cubic-bezier(0.4, 0.0, 0.6, 1)
Bergerak di antara dua titik tanpa berhenti di manapun. Untuk toggle yang tidak perlu emphasis.
Drawer open/close, accordion expand, element yang bergerak dari satu state ke state lain
spring DELIGHT
cubic-bezier(0.34, 1.56, 0.64, 1)
overshoot
Overshoot sedikit lalu settle. Terasa "hidup" dan ekspresif. Gunakan sparingly — untuk momen yang ingin "dirayakan".
Sedekah success, streak milestone, FAB appear, badge counter update
linear
linear
Kecepatan konstan. Terasa robotic untuk transisi. Namun bagus untuk efek yang memang berulang konstan.
Hanya untuk: skeleton shimmer, loading spinner, progress bar indeterminate

Live demo — hover setiap box

standard
📖
decelerate
🕌
accelerate
spring ✨
linear (shimmer)

Flutter Curves mapping

class MasaCurves { static const standard = Curves.easeInOut; // cubic-bezier(0.4, 0.0, 0.2, 1) static const decelerate = Curves.easeOutCubic; // cubic-bezier(0.0, 0.0, 0.2, 1) static const accelerate = Curves.easeInCubic; // cubic-bezier(0.4, 0.0, 1, 1) static const sharp = Curves.easeInOutCubic; // cubic-bezier(0.4, 0.0, 0.6, 1) static const spring = Curves.elasticOut; // overshoot spring static const springLight = Curves.easeOutBack; // lighter spring (--ease-spring) static const linear = Curves.linear; // skeleton/spinner only static const gentle = Curves.easeOutQuart; // --ease-gentle }

Reduced Motion

Setiap animasi di MASA WAJIB memiliki fallback untuk prefers-reduced-motion: reduce. Ini bukan opsional — ini aksesibilitas dasar dan compliance WCAG 2.1 Level AA.

Siapa yang terdampak: Vestibular disorder, motion sensitivity, epilepsi, atau siapapun yang mengaktifkan "Reduce Motion" di Settings Android/iOS. Di Indonesia, kemungkinan besar ada user MASA dengan kondisi ini terutama dari segmen lansia.

3 Level fallback

Level 1 · Ganti
Transform → Fade
Slide, scale, bounce → simple opacity fade. User masih lihat transisi, tapi tidak ada gerakan yang mengganggu.
Page push, bottom sheet, card expand
Level 2 · Percepat
Normal → Instant
Kurangi duration ke 50ms atau 0ms. Animasi tetap ada tapi hampir tidak terasa.
Toggle, tab switch, chip select
Level 3 · Hapus
Animasi loop → Matikan
Skeleton shimmer, loading spinner, pulse animation — matikan sepenuhnya. Ganti dengan static placeholder.
Skeleton, spinner pulse, infinite loops

Flutter implementation

// lib/core/motion/reduced_motion.dart import 'package:flutter/material.dart'; class MotionHelper { // Check if reduced motion is enabled static bool isReducedMotion(BuildContext context) { return MediaQuery.of(context).disableAnimations; } // Get duration respecting reduced motion static Duration safeDuration(BuildContext context, Duration normal) { return isReducedMotion(context) ? MasaDuration.instant // 50ms fallback : normal; } // Get curve respecting reduced motion static Curve safeCurve(BuildContext context, Curve normal) { return isReducedMotion(context) ? Curves.linear : normal; } } // Usage in widget: AnimatedOpacity( duration: MotionHelper.safeDuration(context, MasaDuration.normal), opacity: _visible ? 1.0 : 0.0, child: widget, )

Page Transitions

Navigasi antar layar. 4 pola standar berdasarkan arah navigasi dan hubungan hierarki layar. Konsisten di seluruh app.

4 Navigation Patterns

Push Forward
300ms · decelerate
Beranda
Tap Qur'an
new screen slides
from right
Qur'an Reader
Masuk: slide dari kanan (translateX: +100% → 0) · Keluar current: slide ke kiri (translateX: 0 → -30%)
Gunakan: navigasi ke halaman detail, fitur child, form screen
Pop Back
250ms · accelerate
Qur'an Reader
Tap Back
current slides
to right
Beranda
Keluar current: slide ke kanan (translateX: 0 → +100%) · Masuk previous: dari kiri (-30% → 0)
Modal Present
300ms · decelerate + scale
Current screen
Present modal
new screen slides
from bottom
Payment Screen
Masuk: slide dari bawah (translateY: +100% → 0) + scale current (1.0 → 0.95, fade slightly)
Gunakan: payment screen, full-screen form, settings detail
Bottom Sheet
300ms open · 250ms close · decelerate/accelerate
showModalBottomSheet
sheet slides up
backdrop fades in
Bottom Sheet
Open: translateY(+100% → 0) · backdrop opacity(0 → 0.4) · Close: translateY(0 → +100%)
Gunakan: metode bayar, share, action menu, payment method picker
Flutter PageRouteBuilder:
Semua page transition di MASA menggunakan PageRouteBuilder dengan transitionDuration dan transitionsBuilder yang sudah dikonfigurasi. Jangan gunakan MaterialPageRoute default — transisi-nya tidak sesuai design MASA.

Component Motion

Setiap komponen yang berubah state punya motion yang terdefinisi. Tidak ada "bebas interpretasi" dari dev.

Komponen
Duration
Easing
Apa yang bergerak
Reduced
Iuran VA Error
P0 Bug
100ms
--ease-sharp
Input border color: neutral → red. Error text fade in from below.
optional
Button: default → pressed
50ms
--ease-standard
scale: 1.0 → 0.97, opacity: 1 → 0.85
optional
Button: loading state
200ms
--ease-standard
Label fade out, spinner fade in. Width stays constant (no layout shift).
optional
Toggle: off → on
200ms
--ease-standard
Thumb: translateX(0 → 20px). Track: color neutral → blue. Both simultaneous.
optional
Skeleton loader
1500ms ∞
--ease-linear
Background gradient slides horizontally (shimmer). Repeat infinite.
wajib hapus
Toast appear
200ms
--ease-decelerate
translateY(16px → 0) + opacity(0 → 1). Dismiss: opposite accelerate.
fade only
Bottom sheet open
300ms
--ease-decelerate
translateY(100% → 0). Backdrop: opacity(0 → 0.4) same duration.
fade only
Bottom sheet dismiss
250ms
--ease-accelerate
translateY(0 → 100%). Backdrop: opacity(0.4 → 0).
fade only
Dialog appear
200ms
--ease-decelerate
scale(0.9 → 1) + opacity(0 → 1). Backdrop: fade in simultaneously.
fade only
Slide to confirm drag
real-time
physics
Handle follows finger. Background fills proportionally. Spring back if released early.
optional
Sedekah success
500ms
--ease-spring
Check icon: scale(0 → 1) spring. Card: fade in from below. Count +1: spring.
fade only
Chip select
150ms
--ease-standard
Background color transition. Scale: 1.0 → 1.03 → 1.0.
optional
Progress bar fill
500ms
--ease-decelerate
Width: 0 → target%. Runs on mount, not on every render.
skip anim
Empty state appear
300ms
--ease-decelerate
Staggered: icon(0ms) → title(50ms) → desc(100ms) → cta(150ms). Each fade+translateY.
fade only

Micro-interactions

Animasi kecil yang membuat interface terasa "hidup". Dipakai sparingly — hanya untuk momen yang signifikan secara emosional atau fungsional.

Live demos — klik untuk memicu

Sedekah success
Error shake
❌ VA gagal dibuat
Streak milestone
7
hari
FAB pulse (Sedekah hint)
Button loading
Bounce in (new badge)
🔔
3

Animation definitions

/* CSS keyframes — pasang di app global CSS / Flutter custom painters */ @keyframes masa-success-pop { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.15); } 100% { transform: scale(1); opacity: 1; } /* duration: 500ms, spring */ } @keyframes masa-error-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-6px); } 40% { transform: translateX(6px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } /* duration: 400ms, linear */ } @keyframes masa-bounce-in { 0% { transform: scale(0); opacity: 0; } 60% { transform: scale(1.2); } 80% { transform: scale(0.9); } 100% { transform: scale(1); opacity: 1; } /* duration: 400ms, spring */ } @keyframes masa-fade-slide-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } /* duration: 200ms, decelerate */ } @keyframes masa-stagger-item { /* apply with animation-delay: index * 50ms */ from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }

Feedback Animations

Animasi khusus untuk momen-momen yang harus "dirasakan" user — bukan hanya dilihat. Payment sukses, error, streak milestone, donation impact.

Payment Success
Sedekah / Iuran / Donasi berhasil
Sequence (1000ms total):
0ms — backdrop fade in (200ms)
100ms — check circle scale in, spring (500ms)
400ms — title fade+slide up (300ms)
550ms — subtitle fade in (200ms)
700ms — CTA button slide up (300ms)
Haptic (Android/iOS):
HapticFeedback.mediumImpact()
Saat check muncul (100ms)
Reduced motion:
Semua transform → fade only. Duration masing-masing → 50ms.
Error State
Payment gagal / VA error
Sequence (600ms total):
0ms — border color: neutral → red (100ms)
50ms — error shake animation (400ms)
100ms — error message fade+slide down (200ms)
0ms — recovery options fade in (200ms, after shake)
Haptic:
HapticFeedback.heavyImpact()
Sync dengan shake awal
Note:
Shake hanya 1x. Jangan ulangi tiap kali user tap retry. Sekali cukup untuk komunikasi error.
Habit Streak Milestone
Hari ke-7, ke-30, ke-100
Sequence (1500ms total):
0ms — confetti particle burst (800ms)
200ms — number bounce from old → new (600ms spring)
500ms — milestone card slide up (500ms)
1000ms — "Masya Allah" text fade in (300ms)
1200ms — haptic pattern (3 light pulses)
Tone:
Perayaan yang tenang, tidak berlebihan. Confetti: max 20 partikel, 1 warna. Bukan Duolingo-style celebration yang berlebihan.
Reduced motion:
Hapus confetti dan bounce. Hanya fade in milestone card.
Qurban Distribution Update
Tracking status berubah
Timeline item: dot color transition (neutral → green, 300ms standard) + checkmark scale in (spring, 400ms).
Push notif trigger: saat masuk app, play full sequence.
Background: tidak ada animasi — hanya saat layar aktif.

Component → Token Map

Referensi cepat — untuk setiap komponen, token mana yang digunakan. Copy-paste langsung ke Flutter widget.

KomponenActionDurationEasingProperty yang berubah
ButtonPress--dur-instant (50ms)--ease-standardscale 1→0.97, opacity 1→0.85
ButtonLoading--dur-normal (200ms)--ease-standardlabel opacity, spinner opacity
ToggleSwitch--dur-normal (200ms)--ease-standardtranslateX, background-color
CheckboxCheck--dur-fast (100ms)--ease-standardcheckmark scale, border-color
ChipSelect150ms--ease-standardbackground-color, scale 1→1.03→1
Text FieldFocus--dur-normal (200ms)--ease-standardborder-color, box-shadow
Text FieldError--dur-fast (100ms) + 400ms shake--ease-sharp + linearborder-color → shake
ToastAppear--dur-normal (200ms)--ease-deceleratetranslateY +16→0, opacity 0→1
ToastDismiss--dur-fast (100ms)--ease-accelerateopacity 1→0
DialogOpen--dur-normal (200ms)--ease-deceleratescale 0.9→1, opacity 0→1
Bottom SheetOpen--dur-moderate (300ms)--ease-deceleratetranslateY 100%→0, backdrop 0→0.4
Bottom SheetDismiss250ms--ease-acceleratetranslateY 0→100%
Page PushNavigate forward--dur-moderate (300ms)--ease-deceleratetranslateX +100%→0
Page PopBack250ms--ease-acceleratetranslateX 0→+100%
SkeletonShimmer1500ms ∞--ease-linearbackground-position
Progress BarFill on mount--dur-slow (500ms)--ease-deceleratewidth 0→target%
FABAppear--dur-moderate (300ms)--ease-springscale 0→1, opacity 0→1
Empty StateAppear (stagger)--dur-moderate (300ms)--ease-deceleratetranslateY 12→0, opacity 0→1, delay +50ms per item
Sedekah SuccessConfirm--dur-slow (500ms)--ease-springcheck scale 0→1, card fade+slide
Streak MilestoneAchievement--dur-gentle (700ms)--ease-springnumber bounce, confetti, card slide
e-KTAM QRFlip to QR--dur-moderate (300ms)--ease-standardrotateY 0→90 (first half), 90→0 (second half)
Verification BadgePending → Verified--dur-normal (200ms)--ease-standardbackground-color, checkmark scale 0→1

Motion Anti-patterns

Yang TIDAK boleh dilakukan. Lebih mudah menghindari anti-pattern daripada mendesain dari nol. Kalau ragu — tanya: apakah animasi ini membantu user, atau hanya membuatnya menunggu?

✕ Jangan
  • Durasi >400ms untuk aksi yang sering dilakukan (toggle, tap button, chip)
  • Linear easing untuk transisi UI — terasa robotic
  • Animasi tanpa reduced-motion fallback
  • Lebih dari 3 elemen beranimasi bersamaan tanpa stagger
  • Animasi yang memblokir interaksi user (pointer-events: none terlalu lama)
  • Spring/overshoot untuk error state — terasa tidak serius
  • Terlalu banyak "delight" animation — merusak kepercayaan
  • Animasi yang berbeda untuk gesture yang sama di layar berbeda
  • Animasi infinite loop di konten (bukan loading)
  • Page transition yang sama untuk forward dan back navigation
✓ Lakukan ini
  • ≤200ms untuk semua aksi yang berulang per sesi
  • Selalu decelerate untuk "masuk", accelerate untuk "keluar"
  • Satu property bergerak per momen (tidak scale + rotate + opacity bersamaan)
  • Stagger 40–60ms antar item dalam list yang muncul bersamaan
  • Haptic sync dengan visual animation (HapticFeedback)
  • Spring hanya untuk momen emosional penting (sukses, milestone)
  • Test setiap animasi di low-end device (Redmi 9A)
  • Gunakan `AnimatedWidget` atau `ImplicitlyAnimatedWidget` Flutter — bukan manual interpolation
  • Reduced motion test di setiap PR yang mengubah animasi

Contoh spesifik yang sering salah di MASA context

Sedekah Subuh streak counter: Jangan animasi angka bouncing setiap kali Beranda dibuka. Hanya animasi saat pertama kali user melihat hari ini (setelah sedekah).
Iuran payment: Jangan gunakan spring/bounce untuk payment confirmation. Ini transaksi uang — gunakan standard/decelerate yang serius dan terpercaya.
Al-Qur'an reader: Jangan animasi per-ayat saat scroll. Page-level fade in cukup. Animasi per-ayat mengganggu konsentrasi membaca.
FAB Sedekah: Pulse animation (opacity 1→0.5, 2s infinite) saat user belum pernah sedekah hari ini. Matikan setelah sedekah. Tidak ada pulse di reduced-motion mode.

Flutter Implementation

File-file yang perlu dibuat di lib/core/motion/. Copy-paste dan sesuaikan.

masa_motion.dart — file utama

// lib/core/motion/masa_motion.dart // Import ini di setiap file yang butuh animasi export 'masa_duration.dart'; export 'masa_curves.dart'; export 'masa_transitions.dart'; export 'reduced_motion.dart'; // ── masa_duration.dart ── class MasaDuration { static const instant = Duration(milliseconds: 50); static const fast = Duration(milliseconds: 100); static const normal = Duration(milliseconds: 200); static const moderate = Duration(milliseconds: 300); static const slow = Duration(milliseconds: 500); static const gentle = Duration(milliseconds: 700); static const patient = Duration(milliseconds: 1000); } // ── masa_curves.dart ── class MasaCurves { static const standard = Curves.easeInOut; static const decelerate = Curves.easeOutCubic; static const accelerate = Curves.easeInCubic; static const sharp = Curves.easeInOutCubic; static const spring = Curves.easeOutBack; static const springFull = Curves.elasticOut; static const linear = Curves.linear; static const gentle = Curves.easeOutQuart; }

Page transitions — GoRouter config

// lib/core/router/masa_page_transition.dart import 'package:flutter/material.dart'; import 'masa_motion.dart'; class MasaPageTransition { // Push forward (slide from right) static CustomTransitionPage pushPage({ required Widget child, required GoRouterState state, }) => CustomTransitionPage( key: state.pageKey, child: child, transitionDuration: MasaDuration.moderate, reverseTransitionDuration: Duration(milliseconds: 250), transitionsBuilder: (context, animation, secondary, child) { final push = CurvedAnimation(parent: animation, curve: MasaCurves.decelerate); final pop = CurvedAnimation(parent: secondary, curve: MasaCurves.accelerate); if (MotionHelper.isReducedMotion(context)) { return FadeTransition(opacity: animation, child: child); } return Stack(children: [ SlideTransition( position: Tween(begin: Offset.zero, end: const Offset(-0.3, 0)) .animate(pop), child: const SizedBox.shrink(), ), SlideTransition( position: Tween(begin: const Offset(1.0, 0), end: Offset.zero) .animate(push), child: child, ), ]); }, ); // Modal present (slide from bottom) static CustomTransitionPage modalPage({ required Widget child, required GoRouterState state, }) => CustomTransitionPage( key: state.pageKey, child: child, transitionDuration: MasaDuration.moderate, reverseTransitionDuration: Duration(milliseconds: 250), transitionsBuilder: (context, animation, secondary, child) { if (MotionHelper.isReducedMotion(context)) { return FadeTransition(opacity: animation, child: child); } return SlideTransition( position: Tween(begin: const Offset(0, 1.0), end: Offset.zero) .animate(CurvedAnimation(parent: animation, curve: MasaCurves.decelerate)), child: child, ); }, ); }

Animated component example — Sedekah Success

// lib/features/sedekah/widgets/sedekah_success.dart class SedekahSuccessWidget extends StatefulWidget { ... } class _SedekahSuccessState extends State<SedekahSuccessWidget> with TickerProviderStateMixin { late AnimationController _checkCtrl; late AnimationController _cardCtrl; late Animation<double> _checkScale; late Animation<double> _cardOpacity; late Animation<Offset> _cardSlide; @override void initState() { super.initState(); final reducedMotion = MotionHelper.isReducedMotion(context); _checkCtrl = AnimationController( duration: reducedMotion ? MasaDuration.fast : MasaDuration.slow, vsync: this, ); _checkScale = CurvedAnimation( parent: _checkCtrl, curve: reducedMotion ? Curves.linear : MasaCurves.spring, ); _cardCtrl = AnimationController( duration: reducedMotion ? MasaDuration.fast : MasaDuration.moderate, vsync: this, ); _cardOpacity = _cardCtrl.drive(CurveTween(curve: MasaCurves.decelerate)); _cardSlide = Tween( begin: reducedMotion ? Offset.zero : const Offset(0, 0.3), end: Offset.zero, ).animate(_cardCtrl); // Sequence: check at 100ms, card at 400ms Future.delayed(MasaDuration.fast, () => _checkCtrl.forward()); Future.delayed(const Duration(milliseconds: 400), () { _cardCtrl.forward(); HapticFeedback.mediumImpact(); }); } ... }
Performance tip: Gunakan RepaintBoundary wrapping animated widget untuk mencegah parent rebuild. Untuk animasi list stagger, gunakan AnimationLimiter dari package flutter_staggered_animations — lebih performant dari manual delay.
Index · UI Components · DS v2 Motion & Animation Tokens v1.0 · 15 Jun 2026