Flutter Effects – Membuat Animasi Staggered Menu

Untuk teman-teman yang akan baru memulai belajar Flutter silahkan masuk ke artikel ini dulu ya teman-teman ➡ Aplikasi Pertamaku “Halo Semuaaa…“. Jika sudah yuk lanjut baca artikel ini…

Jangan lupa baca artikel sebelumnya ya teman-temanMembuat Efek Scrolling Parallax


Satu layar aplikasi mungkin berisi banyak animasi, namun memutar semua animasi tersebut pada saat yang sama dapat membuat pengguna kebingungan tapi memutar animasi satu persatu pun dapat memakan waktu. Jalan terbaik adalah mengatur animasi tersebut secara bergantian (Staggered Effect). Setiap animasi dimulai pada waktu yang berbeda, tetapi animasi tersebut tumpang tindih untuk membuat durasi yang lebih pendek. Pada artikel ini, kita akan membuat menu dengan konten animasi staggered dan memiliki tombol yang muncul di bagian bawahnya. Contohnya sebagai berikut:

Staggered Menu Animation Example

Berikut langkah-langkah untuk membuatnya:

1. Buat Menu tanpa Animasi

Drawer menu menampilkan list judul, yang kemudian diikuti dengan tombol “Get Started” pada bagian bawah menu.

Selanjutnya kita definisikan statefull widget Menu yang menampilkan list dan tombol pada lokasi statis.

class Menu extends StatefulWidget {
 @override
 _MenuState createState() => _MenuState();
}

class _MenuState extends State<Menu> {
 static const _menuTitles = [
   'Declarative Style',
   'Premade Widgets',
   'Stateful Hot Reload',
   'Native Performance',
   'Great Community',
 ];

 @override
 Widget build(BuildContext context) {
   return Container(
     color: Colors.white,
     child: Stack(
       fit: StackFit.expand,
       children: [
         _buildFlutterLogo(),
         _buildContent(),
       ],
     ),
   );
 }

 Widget _buildFlutterLogo() {...}

 Widget _buildContent() {
   return Column(
     crossAxisAlignment: CrossAxisAlignment.start,
     children: [
       const SizedBox(height: 16),
       ..._buildListItems(),
       const Spacer(),
       _buildGetStartedButton(),
     ],
   );
 }

 List<Widget> _buildListItems() {
   final listItems = <Widget>[];
   for (var i = 0; i < _menuTitles.length; ++i) {
     listItems.add(
       Padding(
         padding: const EdgeInsets.symmetric(horizontal: 36.0, vertical: 16),
         child: Text(
           _menuTitles[i],
           textAlign: TextAlign.left,
           style: const TextStyle(
             fontSize: 24,
             fontWeight: FontWeight.w500,
           ),
         ),
       ),
     );
   }
   return listItems;
 }

 Widget _buildGetStartedButton() {
   return SizedBox(
     width: double.infinity,
     child: Padding(
       padding: const EdgeInsets.all(24.0),
       child: RaisedButton(
         shape: const StadiumBorder(),
         color: Colors.blue,
         padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 14),
         onPressed: () {},
         child: const Text(
           'Get Started',
           style: TextStyle(
             color: Colors.white,
             fontSize: 22,
           ),
         ),
       ),
     ),
   );
 }
}

2. Menyiapkan Animasi

Untuk mengontrol waktu dari animasi kita membutuhkan AnimationController. Oleh karena itu kita tambahkan SingleTickerProviderStateMixin pada class MenuState kemudian deklarasikan dan buat instance AnimationController.

class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {

 late AnimationController _staggeredController;

 @override
 void initState() {
   super.initState();

   _staggeredController = AnimationController(
     vsync: this,
   );
 }

 @override
 void dispose() {
   _staggeredController.dispose();
   super.dispose();
 }
}

Lamanya delay setiap animasi tergantung berapa lama kita mengaturnya, maka disini kita akan mendefinisikan delay animasi, durasi animasi individual, dan total durasi animasi.

class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
 static const _initialDelayTime = Duration(milliseconds: 50);
 static const _itemSlideTime = Duration(milliseconds: 250);
 static const _staggerTime = Duration(milliseconds: 50);
 static const _buttonDelayTime = Duration(milliseconds: 150);
 static const _buttonTime = Duration(milliseconds: 500);
 final _animationDuration = _initialDelayTime +
     (_staggerTime * _menuTitles.length) +
     _buttonDelayTime +
     _buttonTime;
}

Dalam hal ini, semua animasi memiliki delay hingga 50 milidetik. Setelah itu, list item mulai muncul. Kemunculan setiap list item tertunda selama 50 milidetik setelah list item sebelumnya mulai meluncur masuk. Setiap list item membutuhkan waktu 250 milidetik untuk bergeser dari kanan ke kiri. Setelah list item terakhir mulai meluncur masuk, tombol di bagian bawah menunggu 150 milidetik lagi untuk muncul sehingga animasi tombol membutuhkan waktu 500 milidetik.

Dengan ditentukannya setiap delay dan durasi animasi, kemudian durasi total dihitung sehingga dapat digunakan untuk menghitung waktu animasi individual. Berikut waktu animasi yang ditunjukkan pada diagram berikut:

Flutter menyediakan kelas Interval dan interval sendiri membutuhkan persentase waktu mulai dan persentase waktu berakhir. Interval tersebut kemudian dapat digunakan untuk menganimasikan nilai antara waktu mulai dan akhir tersebut. Misalnya, untuk animasi yang membutuhkan waktu 1 detik, interval animasi tersebut dari 0,2 hingga 0,5 akan dimulai pada 200 milidetik (20%) dan berakhir pada 500 milidetik (50%).

Selanjutnya deklarasikan dan hitung Interval setiap list item dan Interval tombol bawah.

class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
 final List<Interval> _itemSlideIntervals = [];
 late Interval _buttonInterval;

 @override
 void initState() {
   super.initState();

   _createAnimationIntervals();

   _staggeredController = AnimationController(
     vsync: this,
     duration: _animationDuration,
   );
 }

 void _createAnimationIntervals() {
   for (var i = 0; i < _menuTitles.length; ++i) {
     final startTime = _initialDelayTime + (_staggerTime * i);
     final endTime = startTime + _itemSlideTime;
     _itemSlideIntervals.add(
       Interval(
         startTime.inMilliseconds / _animationDuration.inMilliseconds,
         endTime.inMilliseconds / _animationDuration.inMilliseconds,
       ),
     );
   }

   final buttonStartTime =
       Duration(milliseconds: (_menuTitles.length * 50)) + _buttonDelayTime;
   final buttonEndTime = buttonStartTime + _buttonTime;
   _buttonInterval = Interval(
     buttonStartTime.inMilliseconds / _animationDuration.inMilliseconds,
     buttonEndTime.inMilliseconds / _animationDuration.inMilliseconds,
   );
 }
}

3. Memberikan Animasi pada List Item dan Tombol

Efek animasi staggered akan berjalan ketika menu muncul dan cara memulai animasi tersebut menggunakan initState().

@override
void initState() {
  super.initState();

  _createAnimationIntervals();

  _staggeredController = AnimationController(
    vsync: this,
    duration: _animationDuration,
  )..forward();
}

Setiap list item Each bergeser dari kanan ke kiri dan fade in pada saat bersamaan. Oleh karena itu kita akan menggunakan Interval pada list item’s dan easeOut curve untuk menganimasikan opacity dan menerjemahkan setiap list item.

List<Widget> _buildListItems() {
 final listItems = <Widget>[];
 for (var i = 0; i < _menuTitles.length; ++i) {
   listItems.add(
     AnimatedBuilder(
       animation: _staggeredController,
       builder: (context, child) {
         final animationPercent = Curves.easeOut.transform(
           _itemSlideIntervals[i].transform(_staggeredController.value),
         );
         final opacity = animationPercent;
         final slideDistance = (1.0 - animationPercent) * 150;

         return Opacity(
           opacity: opacity,
           child: Transform.translate(
             offset: Offset(slideDistance, 0),
             child: child,
           ),
         );
       },
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 36.0, vertical: 16),
         child: Text(
           _menuTitles[i],
           textAlign: TextAlign.left,
           style: const TextStyle(
             fontSize: 24,
             fontWeight: FontWeight.w500,
           ),
         ),
       ),
     ),
   );
 }
 return listItems;
}

Selanjutnya gunakan pendekatan yang sama untuk menganimasikan opacity dan scale dari tombol bawah. Kali ini kita gunakan elasticOut curve untuk memberi springy effect pada tombol.

Widget _buildGetStartedButton() {
 return SizedBox(
   width: double.infinity,
   child: Padding(
     padding: const EdgeInsets.all(24.0),
     child: AnimatedBuilder(
       animation: _staggeredController,
       builder: (context, child) {
         final animationPercent = Curves.elasticOut.transform(
             _buttonInterval.transform(_staggeredController.value));
         final opacity = animationPercent.clamp(0.0, 1.0);
         final scale = (animationPercent * 0.5) + 0.5;

         return Opacity(
           opacity: opacity,
           child: Transform.scale(
             scale: scale,
             child: child,
           ),
         );
       },
       child: RaisedButton(
         shape: const StadiumBorder(),
         color: Colors.blue,
         padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 14),
         onPressed: () {},
         child: const Text(
           'Get Started',
           style: TextStyle(
             color: Colors.white,
             fontSize: 22,
           ),
         ),
       ),
     ),
   ),
 );
}

Berikut cuplikan kode dan simulasinya, jika teman-teman menggunakan VSCode jalankan projectnya dengan menekan F5, klik hot reload (⚡) atau klik tombol ▶, berikut tampilannya :


Jika ada pertanyaan silahkan komen dan jika artikel ini dirasa bermanfaat, jangan lupa like dan sharenya ya teman-teman. ??????? Sampai bertemu di artikel selanjutnya.
Terima Kasih, Assalamu’alaykum… Salam KODINGINDONESIA

Referensi : https://flutter.dev//

Anton Prafanto

Konten developer kodingindonesia.com & staf pengajar tetap di Universitas Mulawarman Samarinda

all author posts