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-teman ➡ Membuat Animasi Staggered Menu
Seperti yang kita ketahui aplikasi obrolan modern saat ini menampilkan indikator ketika pengguna lain secara aktif mengetik tanggapan pesan yang kita kirim. Indikator ini membantu mencegah respons yang terlalu cepat dan bertentangan antara kita dan orang lain. Dalam artikel ini, kita akan membuat speech bubble typing indicator yang dianimasikan masuk dan keluar dari tampilan. Contohnya sebagai berikut:
Berikut langkah-langkah untuk membuatnya:
1. Mendefinisikan Widget untuk Typing Indicator
Perlu diketahui, Indikator pengetikan ada dalam widget-nya sendiri sehingga dapat digunakan di mana saja di sebuah aplikasi. Seperti halnya widget apa pun yang mengontrol animasi, typing Indicator harus berupa widget yang bersifat stateful. Widget menerima nilai boolean yang menentukan apakah indikator terlihat, kemudian typing Indicator yang berbentuk gelembung ucapan ini menerima satu warna untuk gelembung dan dua warna untuk fase terang dan gelap dari lingkaran yang berkedip di dalam gelembung ucapan besar.
Mendefinisikan widget stateful baru yang disebut TypingIndicator
.
class TypingIndicator extends StatefulWidget { const TypingIndicator({ Key? key, this.showIndicator = false, this.bubbleColor = const Color(0xFF646b7f), this.flashingCircleDarkColor = const Color(0xFF333333), this.flashingCircleBrightColor = const Color(0xFFaec1dd), }) : super(key: key); final bool showIndicator; final Color bubbleColor; final Color flashingCircleDarkColor; final Color flashingCircleBrightColor; @override _TypingIndicatorState createState() => _TypingIndicatorState(); } class _TypingIndicatorState extends State<TypingIndicator> { @override Widget build(BuildContext context) { // TODO: return SizedBox(); } }
2. Membuat Ruang untuk Typing Indicator
Secara default, typing Indicator tidak menempati ruang apa pun saat tidak ditampilkan. Oleh karena itu, ketinggian ruang indikator perlu bertambah saat indikator muncul dan tinggi akan menyusut saat indikator menghilang. Maka kita harus mendefinisikan animasi untuk ketinggian typing Indicator, lalu terapkan nilai animasi tersebut ke widgetSizedBox
di dalam typing Indicator.
class _TypingIndicatorState extends State<TypingIndicator> with TickerProviderStateMixin { late AnimationController _appearanceController; late Animation<double> _indicatorSpaceAnimation; @override void initState() { super.initState(); _appearanceController = AnimationController( vsync: this, ); _indicatorSpaceAnimation = CurvedAnimation( parent: _appearanceController, curve: const Interval(0.0, 0.4, curve: Curves.easeOut), reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), ).drive(Tween<double>( begin: 0.0, end: 60.0, )); if (widget.showIndicator) { _showIndicator(); } } @override void didUpdateWidget(TypingIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (widget.showIndicator != oldWidget.showIndicator) { if (widget.showIndicator) { _showIndicator(); } else { _hideIndicator(); } } } @override void dispose() { _appearanceController.dispose(); super.dispose(); } void _showIndicator() { _appearanceController ..duration = const Duration(milliseconds: 750) ..forward(); } void _hideIndicator() { _appearanceController ..duration = const Duration(milliseconds: 150) ..reverse(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _indicatorSpaceAnimation, builder: (context, child) { return SizedBox( height: _indicatorSpacerAnimation.value, ); }, ); } }
TypingIndicator
menjalankan animasi maju atau mundur bergantung pada apakah variabel showIndicator
yang masuk masing-masing true
atau false
,
Animasi yang mengontrol ketinggian menggunakan kurva animasi yang berbeda bergantung pada arahnya. Saat animasi bergerak maju, perlu disediakan ruang dengan cepat untuk speech bubbles. Karena alasan ini, kurva maju menjalankan seluruh animasi ketinggian dalam 40% pertama dari keseluruhan animasi tampilan. Saat animasi terbalik, speech bubbles perlu diberi cukup waktu untuk menghilang sebelum mengecilkan ketinggian. Kurva ease-out yang menggunakan semua waktu yang tersedia adalah cara yang baik untuk mencapai perilaku ini.
3. Menganimasikan Speech Bubbles
Typing Indicator menampilkan 3 speech bubbles, dua gelembung pertama berukuran kecil dan bulat. Gelembung ketiga berbentuk lonjong dan berisi beberapa lingkaran berkedip. Gelembung ini ditempatkan secara staggered dari kiri bawah ruang yang tersedia.
Setiap gelembung muncul dengan menganimasikan skalanya dari 0% hingga 100%, dan setiap gelembung melakukannya pada waktu yang sedikit berbeda sehingga tampak seperti setiap gelembung muncul setelah gelembung sebelumnya. Ini disebut Staggered Animation.
Warnai tiga gelembung di posisi yang diinginkan dari kiri bawah. Kemudian, animasikan skala gelembung sehingga gelembung tersebut terkenan efek Staggered setiap kali properti showIndicator
berubah.
class _TypingIndicatorState extends State<TypingIndicator> with TickerProviderStateMixin { late AnimationController _appearanceController; late Animation<double> _indicatorSpaceAnimation; late Animation<double> _smallBubbleAnimation; late Animation<double> _mediumBubbleAnimation; late Animation<double> _largeBubbleAnimation; @override void initState() { super.initState(); _appearanceController = AnimationController( vsync: this, ); _indicatorSpaceAnimation = CurvedAnimation( parent: _appearanceController, curve: const Interval(0.0, 0.4, curve: Curves.easeOut), reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), ).drive(Tween<double>( begin: 0.0, end: 60.0, )); _smallBubbleAnimation = CurvedAnimation( parent: _appearanceController, curve: const Interval(0.0, 0.5, curve: Curves.elasticOut), reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut), ); _mediumBubbleAnimation = CurvedAnimation( parent: _appearanceController, curve: const Interval(0.2, 0.7, curve: Curves.elasticOut), reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut), ); _largeBubbleAnimation = CurvedAnimation( parent: _appearanceController, curve: const Interval(0.3, 1.0, curve: Curves.elasticOut), reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut), ); if (widget.showIndicator) { _showIndicator(); } } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _indicatorSpaceAnimation, builder: (context, child) { return SizedBox( height: _indicatorSpacerAnimation.value, child: child, ); }, child: Stack( children: [ _buildAnimatedBubble( animation: _smallBubbleAnimation, left: 8, bottom: 8, bubble: _buildCircleBubble(8), ), _buildAnimatedBubble( animation: _mediumBubbleAnimation, left: 10, bottom: 10, bubble: _buildCircleBubble(16), ), _buildAnimatedBubble( animation: _largeBubbleAnimation, left: 12, bottom: 12, bubble: _buildStatusBubble(), ), ], ), ); } Widget _buildAnimatedBubble({ required Animation<double> animation, required double left, required double bottom, required Widget bubble, }) { return Positioned( left: left, bottom: bottom, child: AnimatedBuilder( animation: animation, builder: (context, child) { return Transform.scale( scale: animation.value, alignment: Alignment.bottomLeft, child: child, ); }, child: bubble, ), ); } Widget _buildCircleBubble(double size) { return Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, color: widget.bubbleColor, ), ); } Widget _buildStatusBubble() { return Container( width: 85, height: 44, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(27), color: widget.bubbleColor, ), ); } }
4. Membuat Animasi Flashing Circles
Di dalam speech bubble yang besar, typing Indicator menampilkan tiga lingkaran kecil yang berkedip berulang kali. Setiap lingkaran berkedip pada waktu yang sedikit berbeda, memberikan kesan bahwa satu sumber cahaya bergerak di belakang setiap lingkaran tersebut dan animasi berkedip ini berulang tanpa batas. Sehingga kita akan menggunakan AnimationController
berulang untuk mengimplementasikan flashing Circles.
class _TypingIndicatorState extends State<TypingIndicator> with TickerProviderStateMixin { late AnimationController _repeatingController; final List<Interval> _dotIntervals = const [ Interval(0.25, 0.8), Interval(0.35, 0.9), Interval(0.45, 1.0), ]; @override void initState() { super.initState(); // ... _repeatingController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), ); if (widget.showIndicator) { _showIndicator(); } } @override void dispose() { _appearanceController.dispose(); _repeatingController.dispose(); super.dispose(); } void _showIndicator() { _appearanceController ..duration = const Duration(milliseconds: 750) ..forward(); _repeatingController.repeat(); } void _hideIndicator() { _appearanceController ..duration = const Duration(milliseconds: 150) ..reverse(); _repeatingController.stop(); } Widget _buildStatusBubble() { return Container( width: 85, height: 44, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(27), color: widget.bubbleColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildFlashingCircle(0), _buildFlashingCircle(1), _buildFlashingCircle(2), ], ), ); } Widget _buildFlashingCircle(int index) { return AnimatedBuilder( animation: _repeatingController, builder: (context, child) { final circleFlashPercent = _dotIntervals[index].transform(_repeatingController.value); final circleColorPercent = sin(pi * circleFlashPercent); return Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: Color.lerp(widget.flashingCircleDarkColor, widget.flashingCircleBrightColor, circleColorPercent), ), ); }, ); } }
Setiap lingkaran menghitung warnanya menggunakan fungsi sinus (sin
) sehingga warna berubah secara bertahap pada titik minimum dan maksimum. Selain itu, setiap lingkaran menganimasikan warnanya dalam interval tertentu yang menghabiskan sebagian waktu animasi secara keseluruhan. Posisi interval ini menghasilkan efek visual dari satu sumber cahaya yang bergerak di belakang tiga titik.
Berikut cuplikan kode dan simulasinya, jika teman-teman menggunakan VSCode jalankan projectnya dengan menekan F5, klik hot reload (⚡) atau klik tombol ▶, berikut tampilannya :
Klik tombol bulat on / off di bagian bawah layar untuk mengaktifkan dan menonaktifkan gelembung Typing Indicator.
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//