Animasi

Menghidupkan UI dengan AnimatedContainer dan Hero serta membedakan animasi implisit dan eksplisit.

Daftar isi

Topik ini membahas dua cara menghidupkan tampilan pada Flutter, yaitu animasi implisit dengan widget berawalan Animated dan transisi elemen bersama dengan Hero. Pembahasan juga memetakan perbedaan animasi implisit dengan animasi eksplisit, sehingga pemilihan teknik berdasarkan kebutuhan gerakan, bukan kebiasaan. Satu demo interaktif meniru perilaku AnimatedContainer, sehingga perubahan nilai antarkeadaan dapat diamati langsung.

Animasi implisit dengan AnimatedContainer#

Animasi implisit adalah animasi yang dijalankan framework secara otomatis setiap kali properti widgetnya berubah nilai. Widget berawalan Animated, seperti AnimatedContainer, AnimatedOpacity, dan AnimatedPadding, menyediakan perilaku tersebut: properti yang biasa diisi nilai tetap, kini diisi nilai yang boleh berubah, dan setiap perubahan diinterpolasi secara halus alih-alih terjadi seketika. Satu-satunya tambahan yang wajib adalah properti duration, yaitu lama peralihan dari nilai lama ke nilai baru.

dart Kartu tugas yang berganti keadaan
// tidak-dijalankan
class KartuTugas extends StatefulWidget {
  const KartuTugas({super.key});

  @override
  State<KartuTugas> createState() => _KartuTugasState();
}

class _KartuTugasState extends State<KartuTugas> {
  bool _menonjol = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedContainer(
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          width: _menonjol ? 240 : 160,
          height: 56,
          alignment: Alignment.center,
          decoration: BoxDecoration(
            color: _menonjol ? Colors.teal.shade700 : Colors.teal.shade100,
            borderRadius: BorderRadius.circular(_menonjol ? 28 : 8),
          ),
          child: Text(
            _menonjol ? 'Tugas prioritas' : 'Tugas biasa',
            style: TextStyle(
              color: _menonjol ? Colors.white : Colors.black87,
            ),
          ),
        ),
        TextButton(
          onPressed: () => setState(() => _menonjol = !_menonjol),
          child: const Text('Ganti keadaan kartu'),
        ),
      ],
    );
  }
}

Setiap ketukan pada tombol mengubah _menonjol melalui setState, sehingga metode build dijalankan ulang dan AnimatedContainer menerima nilai width, warna, serta radius yang baru. Framework menyimpan nilai sebelumnya, kemudian menghitung nilai antara pada setiap frame sepanjang 300 milidetik. Properti curve berisi Curves.easeInOut yang mengatur laju perhitungan nilai antara itu, misalnya lambat pada awal dan akhir peralihan. Tanpa AnimatedContainer, perubahan properti yang sama terjadi seketika pada satu frame tanpa jeda yang terlihat.

Widget lain pada keluarga ini bekerja dengan cara yang sama, misalnya AnimatedOpacity yang menganimasikan nilai opacity dan AnimatedPadding yang menganimasikan jarak. Pergantian keadaan tetap berupa perubahan data biasa melalui setState, sedangkan pergerakannya ditangani framework. Dengan demikian, jumlah kode animasi yang ditulis hampir nol, dan biaya utamanya hanya menentukan lama serta laju peralihan yang pas.

Mengamati interpolasi pada demo#

Demo berikut meniru perilaku AnimatedContainer pada kartu tugas menggunakan properti transition milik CSS, karena aplikasi Dart tidak dapat dijalankan langsung di halaman. Tekan tombol Ganti mode, lalu amati lebar, warna latar, radius sudut, warna teks, dan label kartu berubah secara bertahap, bukan seketika. Kedua keadaan kartu pada demo bersesuaian dengan dua cabang nilai pada kode Dart sebelumnya, sedangkan peran duration dan curve pada kode tersebut diambil alih oleh properti transition pada CSS.

Selama peralihan, nilai lebar dan warna yang tampil adalah hasil interpolasi antara keadaan lama dan keadaan baru, bukan nilai lama ataupun nilai baru. Perilaku inilah yang dikerjakan AnimatedContainer pada kode Dart setiap kali _menonjol berganti nilai. Peralihan yang halus juga membawa makna, yaitu menegaskan bahwa kartu sedang berpindah keadaan, sehingga pengguna tidak kehilangan jejak perubahan yang baru terjadi.

Perhatikan pula bahwa demo ini tidak memuat penghitung waktu maupun kode animasi per frame. Properti transition pada CSS cukup menyatakan lama dan laju peralihan, sedangkan perhitungan nilai antaranya dikerjakan browser. Struktur yang sama berlaku pada AnimatedContainer, yang menerima duration dan curve sebagai deklarasi, tanpa perhitungan per frame yang ditulis manual.

Hero untuk transisi antarlayar#

Hero adalah widget yang menganimasikan satu elemen bersama (shared element) antara dua layar pada perpindahan Navigator. Kedua layar memasang Hero dengan tag yang sama, yaitu string penanda elemen tersebut. Ketika layar baru di-push, framework menyalin tampilan elemen itu dari posisi dan ukuran lama ke posisi serta ukuran barunya, sehingga pengguna melihat elemen yang sama berpindah, bukan dua elemen berbeda yang muncul dan menghilang.

Contoh yang lazim adalah avatar anggota piket, yang kecil pada layar daftar dan besar pada layar detail. Kode berikut memasang Hero dengan tag yang sama pada kedua layar.

dart Hero dengan tag sama pada layar daftar dan detail
// tidak-dijalankan
// Layar daftar
Hero(
  tag: 'avatar-dewi',
  child: const CircleAvatar(radius: 24, child: Text('D')),
)

// Layar detail
Hero(
  tag: 'avatar-dewi',
  child: const CircleAvatar(radius: 64, child: Text('D')),
)

Selama transisi berjalan, CircleAvatar membesar dari radius 24 menjadi 64 sambil berpindah posisi, lalu kembali mengecil ketika layar di-pop. Transisi berlaku dua arah karena proses push dan pop diperlakukan sama. Tag berfungsi sebagai kunci pencocokan, sehingga setiap tag hanya boleh dimiliki satu Hero pada satu layar, sedangkan layar berbeda boleh menggunakan tag yang sama persis. Isi child tidak dibatasi jenisnya, sehingga elemen bersama dapat berupa gambar, kartu, atau gabungan widget lainnya.

Implisit versus eksplisit#

Animasi eksplisit adalah animasi yang dikendalikan langsung oleh kode melalui AnimationController, yaitu objek yang memutar waktu animasi dan mengeluarkan nilai dari 0.0 hingga 1.0. Nilai controller dihubungkan ke warna atau ukuran melalui Tween, yaitu kelas yang memetakan rentang angka menjadi rentang nilai lain, misalnya rentang warna atau rentang ukuran. Widget yang menggunakan controller umumnya StatefulWidget yang menyertakan TickerProviderStateMixin agar controller dapat berjalan, dan controller wajib di-dispose saat widget dilepas. Kelas AnimatedContainer sendiri sebenarnya dibangun di atas mekanisme ini, sehingga kedua kategori terhubung erat.

AspekAnimasi implisitAnimasi eksplisit
Widget utamaAnimatedContainer, AnimatedOpacity, dan sejenisnyaAnimationController dengan Tween
PemicuPerubahan properti pada buildPanggilan forward(), reverse(), atau repeat()
Tambahan stateTidak adaController yang dibuat dan di-dispose
Cocok untukPeralihan keadaan sederhanaGerakan berulang, berurutan, atau yang dikendalikan penuh

Untuk sebagian besar kebutuhan tampilan, animasi implisit sudah memadai dan jumlah kodenya lebih sedikit. Animasi eksplisit baru diperlukan ketika gerakan harus diulang, dirangkai, dibalik arah di tengah peralihan, atau dihitung mengikuti masukan pengguna, misalnya indikator pemuatan yang berputar terus dan urutan kemunculan butir daftar. Kedua kategori menjadi dasar bagi materi animasi tingkat lanjut yang dijelaskan pada dokumentasi resmi.

Kesalahan umum#

Kesalahan pada animasi implisit jarang berupa kerusakan tampilan, tetapi lebih sering berupa animasi yang tidak berjalan sama sekali. Tiga kasus berikut muncul paling sering.

  • Lupa mengisi duration. Properti tersebut wajib diisi, dan kelalaian ini menghentikan build dengan pesan error. Perbaikannya, isi duration sesuai lama peralihan yang diinginkan, misalnya Duration(milliseconds: 300).
  • Mengubah properti tanpa setState. Nilai berubah di memori tetapi build tidak pernah dipanggil ulang, sehingga tidak ada animasi sama sekali. Perbaikannya, ubah nilai di dalam closure setState pada peristiwa yang relevan.
  • Menggunakan tag Hero yang sama dua kali pada satu layar. Framework melempar error ketika transisi berjalan karena lebih dari satu elemen bersama memiliki tag yang sama. Perbaikannya, pastikan setiap tag unik di antara seluruh Hero pada layar yang sama.

Ringkasan#

  • Widget berawalan Animated, seperti AnimatedContainer, menganimasikan perubahan properti secara otomatis sepanjang duration dengan kurva yang dapat diatur melalui curve.
  • Hero menyalin satu elemen bersama antarlayar selama push dan pop Navigator, dengan tag yang sama pada kedua layar dan unik di satu layar.
  • Animasi implisit cukup untuk peralihan keadaan sederhana, sedangkan animasi eksplisit dengan AnimationController dan Tween diperlukan untuk gerakan yang dikendalikan penuh.
  • Pemilihan teknik mengikuti kebutuhan gerakannya, bukan selera.

Referensi#