Tampilan
Bab 11 — Animasi
Animasi bukan hiasan. Ketika sebuah elemen muncul tiba-tiba, otak pengguna kehilangan jejak — dari mana benda itu datang, apa hubungannya dengan yang tadi diketuk. Animasi yang baik menjawab pertanyaan itu tanpa kata-kata.
Flutter membagi animasi menjadi dua kelompok, dan memilih yang tepat menghemat banyak pekerjaan.
Implisit versus eksplisit
Animasi implisit — kamu mengubah sebuah nilai, Flutter menganimasikan perpindahannya secara otomatis. Kamu tidak menulis satu baris pun kode animasi.
Animasi eksplisit — kamu mengendalikan jalannya animasi sendiri: kapan mulai, kapan berhenti, mundur, berulang, dan seterusnya.
IMPLISIT EKSPLISIT
──────────────────────── ────────────────────────
AnimatedContainer AnimationController
AnimatedOpacity AnimatedBuilder
AnimatedPositioned Tween
AnimatedPadding CurvedAnimation
AnimatedSwitcher
Ubah nilai → animasi jalan Kamu yang menyetir
✅ Mudah, cukup untuk 90% kasus ✅ Kendali penuh
❌ Tidak bisa diulang/dibalik ❌ Butuh StatefulWidget + dispose
secara terprogramAturan praktisnya: mulai dari implisit. Naik ke eksplisit hanya kalau kamu benar-benar butuh kendali.
Animasi implisit
AnimatedContainer
Container yang menganimasikan setiap perubahan propertinya:
dart
class KotakBerubah extends StatefulWidget {
const KotakBerubah({super.key});
@override
State<KotakBerubah> createState() => _KotakBerubahState();
}
class _KotakBerubahState extends State<KotakBerubah> {
bool _diperbesar = false;
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () => setState(() => _diperbesar = !_diperbesar),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
// Setiap properti di bawah ini akan dianimasikan
width: _diperbesar ? 280 : 140,
height: _diperbesar ? 280 : 140,
decoration: BoxDecoration(
color: _diperbesar ? Colors.indigo : Colors.teal,
borderRadius: BorderRadius.circular(_diperbesar ? 140 : 16),
),
alignment: Alignment.center,
child: Text(
_diperbesar ? 'Ketuk untuk kecil' : 'Ketuk untuk besar',
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
);
}
}Itu seluruh kodenya. Kamu hanya mengubah nilai lewat setState, dan AnimatedContainer menghitung sendiri seluruh langkah di antaranya.
AnimatedOpacity
Untuk memudarkan masuk dan keluar:
dart
AnimatedOpacity(
opacity: _terlihat ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
child: const Text('Muncul dan menghilang dengan halus'),
)Opacity nol tetap memakan ruang
Widget dengan opacity: 0 masih menempati tempatnya di tata letak dan masih bisa diketuk. Kalau kamu ingin ia benar-benar hilang setelah memudar, pakai AnimatedSwitcher atau kombinasikan dengan Visibility.
AnimatedSwitcher
Menganimasikan pergantian antara dua widget yang berbeda:
dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(scale: animation, child: child),
);
},
child: _memuat
? const CircularProgressIndicator(key: ValueKey('memuat'))
: Text('Selesai', key: const ValueKey('selesai')),
)Key wajib pada AnimatedSwitcher
AnimatedSwitcher mendeteksi pergantian dengan membandingkan key anaknya. Tanpa key yang berbeda, ia mengira widgetnya sama dan tidak menganimasikan apa pun. Alasan lengkapnya di Bab 12.
Widget implisit lainnya
dart
// Posisi di dalam Stack
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
top: _naik ? 20 : 200,
left: 20,
child: const Icon(Icons.star, size: 48),
)
// Padding
AnimatedPadding(
duration: const Duration(milliseconds: 250),
padding: EdgeInsets.all(_renggang ? 32 : 8),
child: const Text('Berjarak'),
)
// Perataan
AnimatedAlign(
duration: const Duration(milliseconds: 400),
alignment: _kanan ? Alignment.centerRight : Alignment.centerLeft,
child: const Icon(Icons.circle),
)
// Gaya teks
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontSize: _besar ? 32 : 16,
color: _besar ? Colors.indigo : Colors.grey,
fontWeight: _besar ? FontWeight.bold : FontWeight.normal,
),
child: const Text('Teks yang tumbuh'),
)TweenAnimationBuilder
Jembatan antara implisit dan eksplisit — menganimasikan nilai apa pun tanpa AnimationController:
dart
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _nilaiTarget),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
builder: (context, nilai, child) {
return Column(
children: [
LinearProgressIndicator(value: nilai),
const SizedBox(height: 8),
Text('${(nilai * 100).toInt()}%'),
],
);
},
)Setiap kali _nilaiTarget berubah, animasinya berjalan dari nilai saat ini ke nilai baru. Ini sangat berguna untuk penghitung angka, bilah progres, dan grafik yang tumbuh.
Curves: bentuk gerakan
Curve menentukan bagaimana kecepatan animasi berubah sepanjang durasinya. Memilih kurva yang tepat adalah perbedaan antara animasi yang terasa mahal dan yang terasa kaku.
dart
Curves.linear // kecepatan tetap — jarang terasa alami
Curves.easeIn // mulai lambat, makin cepat
Curves.easeOut // mulai cepat, melambat di akhir
Curves.easeInOut // lambat–cepat–lambat (paling serbaguna)
Curves.fastOutSlowIn // kurva standar Material
Curves.elasticOut // memantul melewati target
Curves.bounceOut // memantul seperti bola jatuhlinear ──────────────────── kecepatan tetap
easeIn ──────────▁▂▃▅▇████ lambat lalu cepat
easeOut ████▇▅▃▂▁────────── cepat lalu lambat
easeInOut ──▂▄▆████▆▄▂────── lambat–cepat–lambat
elasticOut ────████▆█▇█▇█████ memantul melewati targetPanduan praktis:
- Sesuatu masuk ke layar →
easeOut. Datang cepat, mendarat lembut. - Sesuatu keluar dari layar →
easeIn. Berangkat pelan, pergi cepat. - Berpindah antar keadaan →
easeInOutataufastOutSlowIn. - Ingin terasa main-main →
elasticOutataubounceOut, tetapi hemat.
Durasi yang umum dipakai:
| Durasi | Untuk |
|---|---|
| 100–200 ms | Umpan balik mikro — riak, sorotan |
| 200–300 ms | Transisi kecil — memudar, menggeser |
| 300–500 ms | Transisi halaman, perubahan tata letak |
| > 500 ms | Animasi dekoratif; jarang cocok untuk interaksi |
Animasi di atas 500 ms untuk interaksi biasa akan terasa lambat, bukan elegan.
Animasi eksplisit
Ketika kamu butuh mengulang, membalik, atau menghentikan animasi di tengah jalan, kamu butuh AnimationController.
Tiga bagian yang bekerja sama
┌─────────────────────────────────────────────────────────────┐
│ AnimationController │
│ Menghasilkan nilai 0.0 → 1.0 selama durasi tertentu │
│ Punya .forward(), .reverse(), .repeat(), .stop() │
└──────────────────────────┬──────────────────────────────────┘
│ 0.0 ... 1.0
▼
┌─────────────────────────────────────────────────────────────┐
│ Tween (opsional) │
│ Memetakan 0.0–1.0 ke rentang yang kamu mau │
│ Tween(begin: 0, end: 300) → 0.0 jadi 0, 1.0 jadi 300 │
└──────────────────────────┬──────────────────────────────────┘
│ nilai sesungguhnya
▼
┌─────────────────────────────────────────────────────────────┐
│ AnimatedBuilder │
│ Membangun ulang bagian tertentu setiap frame │
└─────────────────────────────────────────────────────────────┘Contoh: daftar yang meluncur masuk
dart
// lib/screens/layar_meal_beranimasi.dart
import 'package:flutter/material.dart';
class LayarMealBeranimasi extends StatefulWidget {
const LayarMealBeranimasi({super.key, required this.daftarMeal});
final List<Meal> daftarMeal;
@override
State<LayarMealBeranimasi> createState() => _LayarMealBeranimasiState();
}
class _LayarMealBeranimasiState extends State<LayarMealBeranimasi>
with SingleTickerProviderStateMixin {
late AnimationController _pengendali;
late Animation<Offset> _pergeseran;
@override
void initState() {
super.initState();
_pengendali = AnimationController(
vsync: this, // sinkron dengan layar
duration: const Duration(milliseconds: 400),
lowerBound: 0,
upperBound: 1,
);
// Petakan 0.0–1.0 menjadi pergeseran dari bawah ke posisi normal
_pergeseran = Tween<Offset>(
begin: const Offset(0, 0.3), // 30% tinggi widget ke bawah
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _pengendali,
curve: Curves.easeInOut,
),
);
_pengendali.forward(); // mulai animasinya
}
@override
void dispose() {
_pengendali.dispose(); // ⚠️ wajib, kalau tidak memori bocor
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _pengendali,
// `child` dibangun SEKALI dan dipakai ulang setiap frame
child: ListView.builder(
itemCount: widget.daftarMeal.length,
itemBuilder: (ctx, i) => ItemMeal(meal: widget.daftarMeal[i]),
),
builder: (context, child) {
return SlideTransition(
position: _pergeseran,
child: child,
);
},
);
}
}Beberapa bagian yang perlu dibedah:
with SingleTickerProviderStateMixin — mixin ini menyediakan vsync, yang menyinkronkan animasi dengan laju penyegaran layar. Tanpa itu, animasi tetap berjalan meskipun layar mati, membuang baterai. Kalau kamu punya lebih dari satu controller, pakai TickerProviderStateMixin (tanpa Single).
_pengendali.dispose() — wajib. Ini salah satu penyebab kebocoran memori paling umum di aplikasi Flutter.
Parameter child pada AnimatedBuilder — ini optimasi yang penting. Widget yang diberikan sebagai child dibangun sekali, lalu diteruskan ke builder setiap frame. Tanpa itu, seluruh ListView dibangun ulang enam puluh kali per detik.
Mengendalikan animasi
dart
_pengendali.forward(); // maju ke 1.0
_pengendali.reverse(); // mundur ke 0.0
_pengendali.repeat(); // berulang terus
_pengendali.repeat(reverse: true); // maju-mundur bergantian
_pengendali.stop(); // berhenti di tempat
_pengendali.reset(); // kembali ke 0.0 tanpa animasi
// Bereaksi terhadap penyelesaian
_pengendali.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_pengendali.reverse();
}
});Widget transisi bawaan
Alih-alih menulis AnimatedBuilder sendiri, sering ada widget siap pakai:
dart
// Bergeser
SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(_pengendali),
child: const KartuSaya(),
)
// Memudar
FadeTransition(
opacity: _pengendali,
child: const KartuSaya(),
)
// Membesar
ScaleTransition(
scale: CurvedAnimation(parent: _pengendali, curve: Curves.elasticOut),
child: const Icon(Icons.favorite, size: 64),
)
// Berputar
RotationTransition(
turns: _pengendali,
child: const Icon(Icons.refresh, size: 48),
)
// Ukuran
SizeTransition(
sizeFactor: _pengendali,
axis: Axis.vertical,
child: const PanelDetail(),
)Contoh: tombol favorit yang berdenyut
Pola yang sangat umum — ikon hati yang membesar sesaat ketika ditekan.
dart
// lib/widgets/tombol_favorit.dart
import 'package:flutter/material.dart';
class TombolFavorit extends StatefulWidget {
const TombolFavorit({
super.key,
required this.favorit,
required this.saatDitekan,
});
final bool favorit;
final VoidCallback saatDitekan;
@override
State<TombolFavorit> createState() => _TombolFavoritState();
}
class _TombolFavoritState extends State<TombolFavorit>
with SingleTickerProviderStateMixin {
late final AnimationController _pengendali;
late final Animation<double> _skala;
@override
void initState() {
super.initState();
_pengendali = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
// Membesar sampai 1.4 di tengah, lalu kembali ke 1.0
_skala = TweenSequence<double>([
TweenSequenceItem(
tween: Tween(begin: 1.0, end: 1.4)
.chain(CurveTween(curve: Curves.easeOut)),
weight: 50,
),
TweenSequenceItem(
tween: Tween(begin: 1.4, end: 1.0)
.chain(CurveTween(curve: Curves.easeIn)),
weight: 50,
),
]).animate(_pengendali);
}
@override
void dispose() {
_pengendali.dispose();
super.dispose();
}
void _tangani() {
// Jalankan animasi dari awal setiap kali ditekan
_pengendali.forward(from: 0);
widget.saatDitekan();
}
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: _tangani,
icon: ScaleTransition(
scale: _skala,
child: Icon(
widget.favorit ? Icons.star : Icons.star_border,
color: widget.favorit ? Colors.amber : null,
),
),
);
}
}TweenSequence memungkinkan merangkai beberapa tahap animasi dengan bobot masing-masing. Di sini: 50% pertama membesar, 50% berikutnya mengecil kembali.
forward(from: 0) memastikan animasi selalu mulai dari awal — kalau pengguna menekan berkali-kali dengan cepat, tiap tekanan menghasilkan denyut baru.
Transisi antar halaman
Secara bawaan MaterialPageRoute memberi animasi sesuai platform. Untuk transisi kustom, gunakan PageRouteBuilder:
dart
Navigator.of(context).push(
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 400),
reverseTransitionDuration: const Duration(milliseconds: 300),
pageBuilder: (ctx, animation, secondaryAnimation) {
return const LayarDetail();
},
transitionsBuilder: (ctx, animation, secondaryAnimation, child) {
// Meluncur dari kanan sambil memudar masuk
const mulai = Offset(1.0, 0.0);
const selesai = Offset.zero;
final tween = Tween(begin: mulai, end: selesai)
.chain(CurveTween(curve: Curves.easeInOut));
return SlideTransition(
position: animation.drive(tween),
child: FadeTransition(opacity: animation, child: child),
);
},
),
);Untuk transisi yang berlaku di seluruh aplikasi, setel di tema:
dart
MaterialApp(
theme: ThemeData(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
),
)Hero
Sudah disinggung di Bab 7, tetapi layak diulang karena inilah animasi navigasi dengan rasio hasil terhadap usaha yang tertinggi:
dart
// Di layar daftar
Hero(
tag: 'gambar-${meal.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(meal.urlGambar, height: 120, fit: BoxFit.cover),
),
)
// Di layar detail — tag identik
Hero(
tag: 'gambar-${meal.id}',
child: Image.network(meal.urlGambar, height: 300, fit: BoxFit.cover),
)Flutter menghitung sendiri jalur, ukuran, dan durasi transisinya.
Menganimasikan perubahan daftar
Untuk daftar yang itemnya bisa ditambah dan dihapus, AnimatedList memberikan animasi otomatis:
dart
class _DaftarBeranimasiState extends State<DaftarBeranimasi> {
final _kunciDaftar = GlobalKey<AnimatedListState>();
final List<String> _item = [];
void _tambah(String teks) {
_item.insert(0, teks);
_kunciDaftar.currentState?.insertItem(
0,
duration: const Duration(milliseconds: 300),
);
}
void _hapus(int indeks) {
final terhapus = _item.removeAt(indeks);
_kunciDaftar.currentState?.removeItem(
indeks,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: FadeTransition(
opacity: animation,
child: ListTile(title: Text(terhapus)),
),
),
duration: const Duration(milliseconds: 300),
);
}
@override
Widget build(BuildContext context) {
return AnimatedList(
key: _kunciDaftar,
initialItemCount: _item.length,
itemBuilder: (context, indeks, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOut),
),
child: ListTile(
title: Text(_item[indeks]),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _hapus(indeks),
),
),
);
},
);
}
}Kuncinya: daftar data dan AnimatedList harus diperbarui bersamaan. Kalau kamu menghapus dari _item tanpa memanggil removeItem, keduanya menjadi tidak sinkron dan aplikasi crash.
Alternatif yang lebih sederhana
Paket flutter_animate memungkinkan menganimasikan widget apa pun dengan sintaks berantai yang sangat ringkas:
dart
Text('Halo')
.animate()
.fadeIn(duration: 300.ms)
.slideY(begin: 0.2, end: 0);Untuk animasi dekoratif yang tidak butuh kendali rumit, ini menghemat banyak kode boilerplate.
Kesalahan yang sering terjadi
Lupa dispose(). Setiap AnimationController yang tidak dibuang akan terus berjalan dan memakan memori.
Memakai setState untuk animasi. Memanggil setState enam puluh kali per detik untuk menggerakkan sesuatu adalah cara yang boros. Pakai AnimatedBuilder yang hanya membangun ulang bagian yang berubah.
Tidak memakai parameter child. Widget berat di dalam builder dibangun ulang setiap frame. Pindahkan ke parameter child.
Durasi terlalu panjang. Animasi 800 ms terasa lambat, bukan mewah.
Terlalu banyak animasi. Kalau semuanya bergerak, tidak ada yang menonjol. Animasikan hal yang penting saja.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Tombol Tambah ke Keranjang — ⭐⭐ · 45–60 menit
Tantangan: Buat tombol "tambah ke keranjang" yang beranimasi: saat ditekan, ia menyusut sesaat, berubah menjadi lingkaran dengan tanda centang, lalu kembali ke bentuk semula setelah satu detik. Tambahkan juga penghitung isi keranjang yang angkanya berganti dengan transisi, bukan melompat.
Bayangan tampilan (urutan animasi):
text
Sebelum tekan Saat ditekan Setelah (1 detik)
┌────────────────┐ ┌───────┐ ┌───┐
│ + Tambah │ ──▶│ + │────────────▶│ ✓ │──▶ kembali ke awal
│ ke Keranjang │ └───────┘ └───┘
└────────────────┘ (menyusut sesaat)
Keranjang: 2 ──▶ 3 (angka berganti dengan transisi, bukan lompat)Kriteria selesai:
- Perubahan bentuk memakai animasi implisit — kamu hanya mengubah nilai dan Flutter yang menganimasikannya.
- Efek menyusut saat ditekan memakai animasi eksplisit dengan
AnimationControlleryang di-dispose(). - Angka penghitung berganti dengan transisi karena setiap nilai punya
keyyang berbeda. - Seluruh durasi berada di rentang 200–400 milidetik, dan kurvanya dipilih sadar — bukan
Curves.linearyang tersisa dari contoh.
Petunjuk: Bentuk, warna, dan ukuran yang berubah sekaligus adalah pekerjaan AnimatedContainer: ubah decoration dan width lewat setState, dan ia menangani sisanya. Untuk efek tekan, kamu butuh kendali dua arah — forward() saat jari turun, reverse() saat jari naik — dan itulah alasan animasi eksplisit ada. Untuk angka yang berganti, AnimatedSwitcher mendeteksi pergantian lewat key; kalau kedua Text punya key yang sama, atau tidak punya sama sekali, ia menganggap tidak ada yang berubah dan tidak menganimasikan apa pun. Soal kurva: easeOut untuk sesuatu yang masuk, easeIn untuk yang keluar.
Variasi 2: Kartu yang Membalik — ⭐⭐ · 30–45 menit
Tantangan: Buat kartu hafalan yang berbalik saat diketuk, menampilkan pertanyaan di satu sisi dan jawaban di sisi lain. Balikannya harus terasa seperti benda tiga dimensi — sisi belakang tidak boleh terlihat menembus dari depan di tengah putaran.
Bayangan tampilan (urutan animasi):
text
Depan (pertanyaan) Tengah putaran Belakang (jawaban)
┌──────────────┐ ┌───┐ ┌──────────────┐
│ Apa ibukota │ ──▶ │▐▐▐│ ──▶ │ Jakarta │
│ Indonesia? │ └───┘ │ │
└──────────────┘ (menyempit jadi garis; └──────────────┘
konten berganti PERSIS (tidak tampil terbalik
di titik tengah ini) cermin)Kriteria selesai:
- Putaran memakai
AnimationControlleryang bisa dibalik arah, dan di-dispose(). - Konten berganti tepat di titik tengah putaran, bukan di awal atau akhir.
- Sisi belakang tidak tampil terbalik cermin.
- Mengetuk saat animasi sedang berjalan tidak membuat kartu tersendat atau terjebak di posisi setengah.
Petunjuk: Putaran tiga dimensi memakai Transform dengan Matrix4 yang diberi sedikit perspektif; tanpa perspektif, hasilnya terlihat seperti kartu yang menyempit, bukan berputar. Konten yang berganti di titik tengah berarti kamu memeriksa apakah nilai animasi sudah melewati setengah — dan sisi belakang perlu diputar setengah lingkaran lagi supaya tidak tampil sebagai cermin.
Variasi 3: Menu Melingkar — ⭐⭐⭐ · 45–60 menit
Tantangan: Buat tombol aksi mengambang yang, saat ditekan, mengembang menjadi empat tombol kecil yang tersebar melingkar dengan jeda berurutan (tombol kedua muncul sedikit setelah yang pertama). Menekannya lagi menutup mereka dalam urutan terbalik.
Bayangan tampilan (urutan animasi):
text
Tertutup Membuka (berurutan) Terbuka penuh
●(4)
⊕ ──▶ ⊕ ↗●(1) ──▶ ●(3) ⊕ ●(1)
↗●(2, muncul ●(2)
sedikit setelah 1)Menekan ⊕ lagi → keempat tombol menutup dalam urutan terbalik (4, 3, 2, 1), bukan menghilang serentak.
Kriteria selesai:
- Keempat tombol memakai satu
AnimationControllerdenganIntervalyang berbeda, bukan empat controller terpisah. - Ikon tombol utama ikut berputar dan berubah saat menu terbuka.
- Menutup menu menjalankan animasi mundur, bukan menghilang seketika.
- Menekan berkali-kali dengan cepat tidak meninggalkan tombol di posisi aneh.
Petunjuk: Interval sebagai kurva membiarkan satu controller mengendalikan banyak animasi yang bergeser waktunya — anak pertama memakai rentang 0,0 sampai 0,6, anak kedua 0,1 sampai 0,7, dan seterusnya. Ini jauh lebih mudah dikelola daripada empat controller yang harus kamu sinkronkan, dan ia otomatis benar saat dijalankan mundur.
Variasi 4: Indikator Unduhan — ⭐⭐⭐ · 45–60 menit
Tantangan: Buat indikator unduhan melingkar yang mengisi dari nol sampai seratus persen, berubah warna sesuai kemajuan, dan berubah menjadi tanda centang dengan animasi ketika selesai. Persentasenya ditampilkan di tengah dan ikut berubah dengan halus.
Bayangan tampilan (urutan animasi):
text
0% 45% 100%
╭───╮ ╭───╮ ╭───╮
│ 0% │ ──▶ │ 45% │ ──▶ │ ✓ │
╰───╯ ╰──●──╯ ╰───╯
(merah) (kuning, warna (hijau, jadi
berinterpolasi) tanda centang)Kriteria selesai:
- Lingkaran progres digambar sendiri, bukan memakai indikator bawaan.
- Widget berat di dalamnya tidak dibangun ulang setiap frame — kamu memakai parameter
childpadaAnimatedBuilder. - Perubahan warna sepanjang progres memakai interpolasi, bukan lompatan antar tiga warna.
- Transisi ke tanda centang terasa menyatu, bukan seperti dua animasi terpisah.
Petunjuk: CustomPainter adalah alat untuk menggambar busur, dan AnimatedBuilder menghubungkannya ke controller. Parameter child pada AnimatedBuilder sering diabaikan padahal ia gratis: apa pun yang kamu taruh di sana dibangun sekali dan dioper ke builder, sehingga teks persentase yang mahal tidak ikut dibangun ulang enam puluh kali per detik. Untuk warna, Color.lerp memberi peralihan yang mulus.
Variasi 5: Transisi Halaman Kustom — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Ganti transisi bawaan pada tiga perpindahan layar dengan transisi yang kamu rancang sendiri: satu yang menggeser dan memudar, satu yang membesar dari titik sentuh, dan satu yang menggabungkan Hero dengan transisi latar yang menyatu.
Bayangan tampilan (tiga jenis transisi):
text
1) Geser + memudar: [Layar A] ──slide+fade──▶ [Layar B]
2) Membesar dari titik sentuh:
[tombol]• ┌──────────────┐
╲ │ Layar Baru │
╲──────────────▶ │ (mengembang │
│ dari titik │
│ sentuh itu) │
└──────────────┘
3) Hero: [Thumbnail kecil] ══════▶ [Gambar besar di layar detail]
(posisi & ukuran menyatu mulus, tanpa kedipan di akhir)Kriteria selesai:
- Ketiganya memakai
PageRouteBuilderdengan durasi masuk dan keluar yang dipilih sadar. - Transisi yang membesar dari titik sentuh benar-benar berasal dari posisi jari, bukan dari tengah layar.
- Transisi
Herobekerja dua arah tanpa kedipan di akhir. - Menekan kembali di tengah transisi tidak meninggalkan layar setengah tampil.
Petunjuk: Untuk membesar dari titik sentuh, kamu butuh posisi global widget yang ditekan — RenderBox dari context-nya bisa memberikan itu lewat localToGlobal. Kedipan di akhir transisi Hero hampir selalu berarti widget di kedua layar tidak benar-benar identik bentuknya; bungkus keduanya dengan Material bertipe transparan kalau yang berkedip adalah latar teksnya.
Variasi 6: Daftar yang Hidup — ⭐⭐⭐⭐⭐ · 90–120 menit
Tantangan: Bangun daftar di mana item baru menyelinap masuk dari samping, item yang dihapus mengecil sambil memudar, dan item bisa disusun ulang dengan menyeret — semuanya beranimasi dengan mulus dan tidak pernah kehilangan sinkronisasi dengan datanya.
Bayangan tampilan (urutan animasi):
text
Sebelum Item baru masuk Item dihapus
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Item A │ │ Item A │ │ Item A │
│ Item B │ │ Item B │ │ (mengecil │
│ Item C │ │◀Item Baru│ (masuk │ + memudar│
└──────────┘ │ Item C │ dari kanan)│ di sini)│
└──────────┘ │ Item C │
└──────────┘Menyeret "Item C" ke atas "Item A" → urutan data ikut berubah dan tetap tersimpan setelah layar ditutup lalu dibuka kembali.
Kriteria selesai:
- Menambah dan menghapus beberapa item dengan cepat berturut-turut tidak pernah membuat aplikasi berhenti.
- Menyeret item ke posisi baru memperbarui data yang mendasarinya, dan urutannya bertahan setelah layar dibuka ulang.
- Animasi hapus menampilkan item yang benar, bukan item penggantinya.
- Daftar tetap mulus ketika berisi dua ratus item.
Petunjuk: AnimatedList adalah bagian paling rawan crash di seluruh bab ini, dan penyebabnya selalu sama: daftar data dan daftar yang diketahui AnimatedList menjadi tidak sinkron. Setiap penambahan dan penghapusan harus memberi tahu keduanya dalam urutan yang benar, dan animasi hapus butuh salinan item yang sudah dibuang untuk digambar selama animasinya berjalan. Untuk menyeret, ReorderableListView sudah menangani sebagian besar pekerjaannya — perhatikan bahwa indeks tujuan yang ia berikan perlu disesuaikan ketika item dipindah ke bawah.
Ikhtisar
- Mulai dari animasi implisit.
AnimatedContainer,AnimatedOpacity, dan saudara-saudaranya cukup untuk sebagian besar kebutuhan — kamu hanya mengubah nilai lewatsetState. - Naik ke eksplisit hanya ketika butuh mengulang, membalik, atau menghentikan animasi secara terprogram.
- Animasi eksplisit butuh
AnimationController+vsyncdariSingleTickerProviderStateMixin, dan wajib di-dispose(). Tweenmemetakan rentang 0.0–1.0 ke nilai yang kamu inginkan;CurvedAnimationmemberi bentuk gerakannya.- Pakai parameter
childpadaAnimatedBuilderagar widget berat tidak dibangun ulang setiap frame. - Kurva:
easeOutuntuk masuk,easeInuntuk keluar,easeInOutuntuk berpindah keadaan. - Durasi 200–400 ms cocok untuk hampir semua interaksi. Di atas 500 ms terasa lambat.
AnimatedSwitcherbutuhkeyyang berbeda untuk mendeteksi pergantian.Heromemberi transisi antar halaman yang mengesankan dengan usaha paling sedikit — cukuptagyang identik.AnimatedListbutuh daftar data dan widget-nya diperbarui bersamaan, atau aplikasi akan crash.
Berikutnya: Bab 12 — Tiga Pohon & Keys, tempat kita membuka kap mesin dan melihat cara Flutter bekerja sebenarnya.
Transkrip asli
Disintesis dari 1_flutter_core/9_meals-app_adding-annimation.md (9 video) dan 1_flutter_zero-to-hero/12_next-steps.md (bagian Animations). Lihat PDF Flutter Core.