1. Pengantar
Animasi adalah cara yang bagus untuk meningkatkan pengalaman pengguna aplikasi Anda, menyampaikan informasi penting kepada pengguna, dan membuat aplikasi Anda lebih rapi dan menyenangkan untuk digunakan.
Ringkasan framework animasi Flutter
Flutter menampilkan efek animasi dengan mem-build ulang sebagian hierarki widget di setiap frame. Library ini menyediakan efek animasi bawaan dan API lainnya untuk mempermudah pembuatan dan komposisi animasi.
- Animasi implisit adalah efek animasi bawaan yang menjalankan seluruh animasi secara otomatis. Saat nilai target animasi berubah, nilai tersebut akan menjalankan animasi dari nilai saat ini ke nilai target, dan menampilkan setiap nilai di antaranya sehingga widget akan dianimasikan dengan lancar. Contoh animasi implisit meliputi
AnimatedSize
,AnimatedScale
, danAnimatedPositioned
. - Animasi eksplisit juga merupakan efek animasi bawaan, tetapi memerlukan objek
Animation
agar dapat berfungsi. Contohnya meliputiSizeTransition
,ScaleTransition
, atauPositionedTransition
. - Animation adalah class yang mewakili animasi yang berjalan atau berhenti, dan terdiri dari value yang mewakili nilai target yang dijalankan animasi, dan status, yang mewakili nilai saat ini yang ditampilkan animasi di layar pada waktu tertentu. Ini adalah subclass dari
Listenable
, dan memberi tahu pemrosesnya saat status berubah saat animasi berjalan. - AnimationController adalah cara untuk membuat Animasi dan mengontrol statusnya. Metodenya seperti
forward()
,reset()
,stop()
, danrepeat()
dapat digunakan untuk mengontrol animasi tanpa perlu menentukan efek animasi yang ditampilkan, seperti skala, ukuran, atau posisi. - Tween digunakan untuk mengalurkan nilai antara nilai awal dan akhir, serta dapat mewakili jenis apa pun, seperti ganda,
Offset
, atauColor
. - Kurva digunakan untuk menyesuaikan laju perubahan parameter dari waktu ke waktu. Saat animasi berjalan, biasanya kurva easing diterapkan untuk membuat laju perubahan lebih cepat atau lebih lambat di awal atau akhir animasi. Kurva menggunakan nilai input antara 0,0 dan 1,0 dan menampilkan nilai output antara 0,0 dan 1,0.
Yang akan Anda bangun
Dalam codelab ini, Anda akan membuat game kuis pilihan ganda yang menampilkan berbagai efek dan teknik animasi.
Anda akan melihat cara...
- Mem-build widget yang menganimasikan ukuran dan warnanya
- Membuat efek putaran kartu 3D
- Menggunakan efek animasi bawaan yang menarik dari paket animasi
- Menambahkan dukungan gestur kembali prediktif yang tersedia di versi Android terbaru
Yang akan Anda pelajari
Dalam codelab ini, Anda akan mempelajari:
- Cara menggunakan efek animasi implisit untuk mendapatkan animasi yang terlihat bagus tanpa memerlukan banyak kode.
- Cara menggunakan efek animasi eksplisit untuk mengonfigurasi efek Anda sendiri menggunakan widget animasi bawaan seperti
AnimatedSwitcher
atauAnimationController
. - Cara menggunakan
AnimationController
untuk menentukan widget Anda sendiri yang menampilkan efek 3D. - Cara menggunakan paket
animations
untuk menampilkan efek animasi yang menarik dengan penyiapan minimal.
Yang Anda butuhkan
- Flutter SDK
- IDE, seperti VSCode atau Android Studio / IntelliJ
2. Menyiapkan lingkungan pengembangan Flutter Anda
Anda memerlukan dua software untuk menyelesaikan lab ini — Flutter SDK dan editor.
Anda dapat menjalankan codelab menggunakan salah satu perangkat berikut:
- Perangkat Android (direkomendasikan untuk menerapkan kembali prediktif di langkah 7) atau iOS fisik yang terhubung ke komputer dan disetel ke mode Developer.
- Simulator iOS (memerlukan penginstalan alat Xcode).
- Android Emulator (memerlukan penyiapan di Android Studio).
- Browser (Chrome diperlukan untuk proses debug).
- Komputer desktop Windows, Linux, atau macOS. Anda harus melakukan pengembangan di platform tempat Anda berencana men-deploy aplikasi. Jadi, jika ingin mengembangkan aplikasi desktop Windows, Anda harus mengembangkannya di Windows untuk mengakses rantai build yang sesuai. Ada persyaratan spesifik per sistem operasi yang dibahas secara mendetail di docs.flutter.dev/desktop.
Memverifikasi penginstalan
Untuk memverifikasi bahwa Flutter SDK Anda dikonfigurasi dengan benar, dan Anda telah menginstal setidaknya salah satu platform target di atas, gunakan alat Flutter Doctor:
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale en) [✓] Android toolchain - develop for Android devices [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio [✓] IntelliJ IDEA Ultimate Edition [✓] VS Code [✓] Connected device (4 available) [✓] Network resources • No issues found!
3. Menjalankan aplikasi awal
Mendownload aplikasi awal
Gunakan git
untuk meng-clone aplikasi awal dari repositori flutter/samples di GitHub.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
Atau, Anda dapat mendownload kode sumber sebagai file .zip.
Menjalankan aplikasi
Untuk menjalankan aplikasi, gunakan perintah flutter run
dan tentukan perangkat target, seperti android
, ios
, atau chrome
. Untuk mengetahui daftar lengkap platform yang didukung, lihat halaman Platform yang didukung.
$ flutter run -d android
Anda juga dapat menjalankan dan men-debug aplikasi menggunakan IDE pilihan Anda. Lihat dokumentasi Flutter resmi untuk mengetahui informasi selengkapnya.
Menerapkan kode
Aplikasi awal adalah game kuis pilihan ganda yang terdiri dari dua layar yang mengikuti pola desain model-tampilan-tampilan-model, atau MVVM. QuestionScreen
(Tampilan) menggunakan class QuizViewModel
(Model Tampilan) untuk mengajukan pertanyaan pilihan ganda kepada pengguna dari class QuestionBank
(Model).
- home_screen.dart - Menampilkan layar dengan tombol New Game
- main.dart - Mengonfigurasi
MaterialApp
untuk menggunakan Material 3 dan menampilkan layar utama - model.dart - Menentukan class inti yang digunakan di seluruh aplikasi
- question_screen.dart - Menampilkan UI untuk game kuis
- view_model.dart - Menyimpan status dan logika untuk game kuis, yang ditampilkan oleh
QuestionScreen
Aplikasi belum mendukung efek animasi apa pun, kecuali untuk transisi tampilan default yang ditampilkan oleh class Navigator
Flutter saat pengguna menekan tombol Game Baru.
4. Menggunakan efek animasi implisit
Animasi implisit adalah pilihan yang tepat dalam banyak situasi, karena tidak memerlukan konfigurasi khusus. Di bagian ini, Anda akan memperbarui widget StatusBar
agar menampilkan papan skor animasi. Untuk menemukan efek animasi implisit umum, jelajahi dokumentasi API ImplicitlyAnimatedWidget.
Membuat widget papan skor tanpa animasi
Buat file baru, lib/scoreboard.dart
dengan kode berikut:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color:
score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
)
],
),
);
}
}
Kemudian, tambahkan widget Scoreboard
di turunan widget StatusBar
, yang menggantikan widget Text
yang sebelumnya menampilkan skor dan total jumlah pertanyaan. Editor Anda akan otomatis menambahkan import "scoreboard.dart"
yang diperlukan di bagian atas file.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
Widget ini menampilkan ikon bintang untuk setiap pertanyaan. Saat pertanyaan dijawab dengan benar, bintang lain akan langsung menyala tanpa animasi apa pun. Pada langkah-langkah berikut, Anda akan membantu memberi tahu pengguna bahwa skor mereka berubah dengan menganimasikan ukuran dan warnanya.
Menggunakan efek animasi implisit
Buat widget baru bernama AnimatedStar
yang menggunakan widget AnimatedScale
untuk mengubah jumlah scale
dari 0.5
menjadi 1.0
saat bintang menjadi aktif:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
Sekarang, saat pengguna menjawab pertanyaan dengan benar, widget AnimatedStar
akan memperbarui ukurannya menggunakan animasi implisit. color
Icon
tidak dianimasikan di sini, hanya scale
, yang dilakukan oleh widget AnimatedScale
.
Menggunakan Tween untuk melakukan interpolasi antara dua nilai
Perhatikan bahwa warna widget AnimatedStar
langsung berubah setelah kolom isActive
berubah menjadi benar.
Untuk mendapatkan efek warna animasi, Anda dapat mencoba menggunakan widget AnimatedContainer
(yang merupakan subclass lain dari ImplicitlyAnimatedWidget
), karena widget ini dapat menganimasikan semua atributnya secara otomatis, termasuk warna. Sayangnya, widget kita harus menampilkan ikon, bukan penampung.
Anda juga dapat mencoba AnimatedIcon
, yang menerapkan efek transisi di antara bentuk ikon. Namun, tidak ada implementasi default ikon bintang di class AnimatedIcons
.
Sebagai gantinya, kita akan menggunakan subclass ImplicitlyAnimatedWidget
lain yang disebut TweenAnimationBuilder
, yang menggunakan Tween
sebagai parameter. Tween adalah class yang mengambil dua nilai (begin
dan end
) dan menghitung nilai di antaranya, sehingga animasi dapat menampilkannya. Dalam contoh ini, kita akan menggunakan ColorTween
, yang memenuhi antarmuka Tween<Color>
yang diperlukan untuk membuat efek animasi.
Pilih widget Icon
dan gunakan tindakan cepat "Gabungkan dengan Builder" di IDE Anda, ubah namanya menjadi TweenAnimationBuilder
. Kemudian, berikan durasi dan ColorTween
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(
Icons.star,
size: 50,
color: value, // Modify from here...
);
}, // To here.
),
);
}
}
Sekarang, lakukan hot reload aplikasi untuk melihat animasi baru.
Perhatikan bahwa nilai end
dari ColorTween
berubah berdasarkan nilai parameter isActive
. Hal ini karena TweenAnimationBuilder
menjalankan ulang animasinya setiap kali nilai Tween.end
berubah. Jika hal ini terjadi, animasi baru akan berjalan dari nilai animasi saat ini ke nilai akhir baru, yang memungkinkan Anda mengubah warna kapan saja (bahkan saat animasi berjalan) dan menampilkan efek animasi yang halus dengan nilai perantara yang benar.
Menerapkan Kurva
Kedua efek animasi ini berjalan dengan kecepatan konstan, tetapi animasi sering kali lebih menarik dan informatif secara visual saat dipercepat atau diperlambat.
Curve
menerapkan fungsi easing, yang menentukan laju perubahan parameter dari waktu ke waktu. Flutter dilengkapi dengan kumpulan kurva easing bawaan di class Curves
, seperti easeIn
atau easeOut
.
Diagram ini (tersedia di halaman dokumentasi Curves
API) memberikan petunjuk tentang cara kerja kurva. Kurva mengonversi nilai input antara 0,0 dan 1,0 (ditampilkan pada sumbu x) menjadi nilai output antara 0,0 dan 1,0 (ditampilkan pada sumbu y). Diagram ini juga menampilkan pratinjau tampilan berbagai efek animasi saat menggunakan kurva easing.
Buat kolom baru di AnimatedStar bernama _curve
dan teruskan sebagai parameter ke widget AnimatedScale
dan TweenAnimationBuilder
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(
Icons.star,
size: 50,
color: value,
);
},
),
);
}
}
Dalam contoh ini, kurva elasticOut
memberikan efek pegas yang berlebihan yang dimulai dengan gerakan pegas dan seimbang di bagian akhir.
Lakukan hot reload pada aplikasi untuk melihat kurva ini diterapkan ke AnimatedSize
dan TweenAnimationBuilder
.
Menggunakan DevTools untuk mengaktifkan animasi lambat
Untuk men-debug efek animasi apa pun, Flutter DevTools menyediakan cara untuk memperlambat semua animasi di aplikasi, sehingga Anda dapat melihat animasi dengan lebih jelas.
Untuk membuka DevTools, pastikan aplikasi berjalan dalam mode debug, dan buka Widget Inspector dengan memilihnya di Debug toolbar di VSCode atau dengan memilih tombol Open Flutter DevTools di Debug tool window di IntelliJ / Android Studio.
Setelah widget inspector terbuka, klik tombol Slow animations di toolbar.
5. Menggunakan efek animasi eksplisit
Seperti animasi implisit, animasi eksplisit adalah efek animasi bawaan, tetapi bukan mengambil nilai target, animasi ini mengambil objek Animation
sebagai parameter. Hal ini membuatnya berguna dalam situasi ketika animasi sudah ditentukan oleh transisi navigasi, AnimatedSwitcher
, atau AnimationController
, misalnya.
Menggunakan efek animasi eksplisit
Untuk memulai efek animasi eksplisit, gabungkan widget Card
dengan AnimatedSwitcher
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher
menggunakan efek cross-fade secara default, tetapi Anda dapat menggantinya menggunakan parameter transitionBuilder
. Builder transisi menyediakan widget turunan yang diteruskan ke AnimatedSwitcher
, dan objek Animation
. Ini adalah kesempatan bagus untuk menggunakan animasi eksplisit.
Untuk codelab ini, animasi eksplisit pertama yang akan kita gunakan adalah SlideTransition
, yang menggunakan Animation<Offset>
yang menentukan offset awal dan akhir yang akan dilalui widget masuk dan keluar.
Tween memiliki fungsi helper, animate()
, yang mengonversi Animation
menjadi Animation
lain dengan tween yang diterapkan. Ini berarti Tween<Offset>
dapat digunakan untuk mengonversi Animation<double>
yang disediakan oleh AnimatedSwitcher
menjadi Animation<Offset>
, yang akan disediakan ke widget SlideTransition
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Perhatikan bahwa ini menggunakan Tween.animate
untuk menerapkan Curve
ke Animation
, lalu mengonversinya dari Tween<double>
yang berkisar dari 0,0 hingga 1,0, menjadi Tween<Offset>
yang bertransisi dari -0,1 ke 0,0 pada sumbu x.
Atau, class Animasi memiliki fungsi drive()
yang menggunakan Tween
(atau Animatable
) dan mengonversinya menjadi Animation
baru. Hal ini memungkinkan tween "dikaitkan", sehingga kode yang dihasilkan lebih ringkas:
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
Keuntungan lain dari penggunaan animasi eksplisit adalah animasi tersebut dapat dengan mudah disusun bersama. Tambahkan animasi eksplisit lainnya, FadeTransition yang menggunakan animasi melengkung yang sama dengan menggabungkan widget SlideTransition.
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
Menyesuaikan layoutBuilder
Anda mungkin melihat masalah kecil pada AnimationSwitcher. Saat beralih ke pertanyaan baru, QuestionCard akan menatanya di tengah ruang yang tersedia saat animasi berjalan, tetapi saat animasi dihentikan, widget akan ditarik ke bagian atas layar. Hal ini menyebabkan animasi yang tidak lancar karena posisi akhir kartu pertanyaan tidak cocok dengan posisi saat animasi berjalan.
Untuk memperbaikinya, AnimatedSwitcher juga memiliki parameter layoutBuilder, yang dapat digunakan untuk menentukan tata letak. Gunakan fungsi ini untuk mengonfigurasi builder tata letak agar menyelaraskan kartu ke bagian atas layar:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
Kode ini adalah versi modifikasi dari defaultLayoutBuilder dari class AnimatedSwitcher, tetapi menggunakan Alignment.topCenter
, bukan Alignment.center
.
Ringkasan
- Animasi eksplisit adalah efek animasi yang menggunakan objek Animasi (berbeda dengan ImplicitlyAnimatedWidgets, yang menggunakan nilai dan durasi target)
- Class Animation mewakili animasi yang sedang berjalan, tetapi tidak menentukan efek tertentu.
- Gunakan Tween().animate atau Animation.drive() untuk menerapkan Tween dan Kurva (menggunakan CurveTween) ke animasi.
- Gunakan parameter layoutBuilder AnimatedSwitcher untuk menyesuaikan cara menata letak turunannya.
6. Mengontrol status animasi
Sejauh ini, setiap animasi telah dijalankan secara otomatis oleh framework. Animasi implisit berjalan secara otomatis, dan efek animasi eksplisit memerlukan Animasi agar berfungsi dengan benar. Di bagian ini, Anda akan mempelajari cara membuat objek Animation Anda sendiri menggunakan AnimationController, dan menggunakan TweenSequence untuk menggabungkan Tween.
Menjalankan animasi menggunakan AnimationController
Untuk membuat animasi menggunakan AnimationController, Anda harus mengikuti langkah-langkah berikut:
- Membuat StatefulWidget
- Gunakan mixin SingleTickerProviderStateMixin di class State untuk menyediakan Ticker ke AnimationController
- Lakukan inisialisasi AnimationController dalam metode siklus proses initState, yang menyediakan objek Status saat ini ke parameter
vsync
(TickerProvider). - Pastikan widget Anda di-build ulang setiap kali AnimationController memberi tahu pemrosesnya, baik dengan menggunakan AnimatedBuilder atau dengan memanggil listen() dan setState secara manual.
Buat file baru, flip_effect.dart, lalu salin dan tempel kode berikut:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: widget.duration);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
Class ini menyiapkan AnimationController dan menjalankan ulang animasi setiap kali framework memanggil didUpdateWidget untuk memberi tahu bahwa konfigurasi widget telah berubah, dan mungkin ada widget turunan baru.
AnimatedBuilder memastikan bahwa hierarki widget dibuat ulang setiap kali AnimationController memberi tahu pemrosesnya, dan widget Transform digunakan untuk menerapkan efek rotasi 3D guna menyimulasikan kartu yang dibalik.
Untuk menggunakan widget ini, gabungkan setiap kartu jawaban dengan widget CardFlipEffect. Pastikan untuk memberikan key
ke widget Kartu:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
Sekarang, hot reload aplikasi untuk melihat kartu jawaban dibalik menggunakan widget CardFlipEffect.
Anda mungkin melihat bahwa class ini terlihat sangat mirip dengan efek animasi eksplisit. Bahkan, sebaiknya Anda memperluas class AnimatedWidget secara langsung untuk menerapkan versi Anda sendiri. Sayangnya, karena class ini perlu menyimpan widget sebelumnya dalam Statusnya, class ini perlu menggunakan StatefulWidget. Untuk mempelajari lebih lanjut cara membuat efek animasi eksplisit Anda sendiri, lihat dokumentasi API untuk AnimatedWidget.
Menambahkan penundaan menggunakan TweenSequence
Di bagian ini, Anda akan menambahkan penundaan ke widget CardFlipEffect sehingga setiap kartu dibalik satu per satu. Untuk memulai, tambahkan kolom baru bernama delayAmount
.
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
Kemudian, tambahkan delayAmount
ke metode build AnswerCards
.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
Kemudian, di _CardFlipEffectState
, buat Animasi baru yang menerapkan penundaan menggunakan TweenSequence
. Perhatikan bahwa ini tidak menggunakan utilitas apa pun dari library dart:async
, seperti Future.delayed
. Hal ini karena penundaan adalah bagian dari animasi dan bukan sesuatu yang dikontrol secara eksplisit oleh widget saat menggunakan AnimationController. Hal ini membuat efek animasi lebih mudah di-debug saat mengaktifkan animasi lambat di DevTools, karena menggunakan TickerProvider yang sama.
Untuk menggunakan TweenSequence
, buat dua TweenSequenceItem
, satu berisi ConstantTween
yang mempertahankan animasi pada 0 selama durasi relatif dan Tween
reguler yang beralih dari 0.0
ke 1.0
.
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: widget.duration * (widget.delayAmount + 1));
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
Terakhir, ganti animasi AnimationController dengan animasi tertunda baru dalam metode build.
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
Sekarang, lakukan hot reload pada aplikasi dan lihat kartu berbalik satu per satu. Sebagai tantangan, coba bereksperimen dengan mengubah perspektif efek 3D yang disediakan oleh widget Transform
.
7. Menggunakan transisi navigasi kustom
Sejauh ini, kita telah melihat cara menyesuaikan efek di satu layar, tetapi cara lain untuk menggunakan animasi adalah menggunakannya untuk melakukan transisi antarlayar. Di bagian ini, Anda akan mempelajari cara menerapkan efek animasi ke transisi layar menggunakan efek animasi bawaan dan efek animasi bawaan yang menarik yang disediakan oleh paket animasi resmi di pub.dev
Menganimasikan transisi navigasi
Class PageRouteBuilder
adalah Route
yang memungkinkan Anda menyesuaikan animasi transisi. Hal ini memungkinkan Anda mengganti callback transitionBuilder
-nya, yang menyediakan dua objek Animasi, yang mewakili animasi masuk dan keluar yang dijalankan oleh Navigator.
Untuk menyesuaikan animasi transisi, ganti MaterialPageRoute
dengan PageRouteBuilder
dan untuk menyesuaikan animasi transisi saat pengguna membuka dari HomeScreen
ke QuestionScreen
. Gunakan FadeTransition (widget yang dianimasikan secara eksplisit) untuk membuat layar baru memudar di atas layar sebelumnya.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
Paket animasi menyediakan efek animasi bawaan yang menarik, seperti FadeThroughTransition. Impor paket animasi dan ganti FadeTransition dengan widget FadeThroughTransition:
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
Menyesuaikan animasi kembali prediktif
Kembali prediktif adalah fitur Android baru yang memungkinkan pengguna melihat di balik rute atau aplikasi saat ini untuk melihat apa yang ada di baliknya sebelum menavigasi. Animasi peek didorong oleh lokasi jari pengguna saat mereka menarik kembali di layar.
Flutter mendukung kembali prediktif sistem dengan mengaktifkan fitur di tingkat sistem saat Flutter tidak memiliki rute untuk muncul di stack navigasinya, atau dengan kata lain, saat kembali akan keluar dari aplikasi. Animasi ini ditangani oleh sistem, bukan oleh Flutter itu sendiri.
Flutter juga mendukung kembali prediktif saat berpindah antar-rute dalam aplikasi Flutter. PageTransitionsBuilder khusus yang disebut PredictiveBackPageTransitionsBuilder
memproses gestur kembali prediktif sistem dan mendorong transisi halamannya dengan progres gestur.
Kembali prediktif hanya didukung di Android U dan yang lebih baru, tetapi Flutter akan kembali ke perilaku gestur kembali asli dan ZoomPageTransitionBuilder dengan lancar. Lihat postingan blog kami untuk mengetahui informasi selengkapnya, termasuk bagian tentang cara menyiapkannya di aplikasi Anda sendiri.
Dalam konfigurasi ThemeData untuk aplikasi Anda, konfigurasikan PageTransitionsTheme untuk menggunakan PredictiveBack di Android, dan efek transisi memudar dari paket animasi di platform lain:
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
Sekarang Anda dapat mengubah panggilan balik Navigator.push() ke MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
Menggunakan FadeThroughTransition untuk mengubah pertanyaan saat ini
Widget AnimatedSwitcher hanya menyediakan satu Animasi dalam callback builder-nya. Untuk mengatasi hal ini, paket animations
menyediakan PageTransitionSwitcher.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Menggunakan OpenContainer
Widget OpenContainer dari paket animations
memberikan efek animasi transformasi penampung yang diperluas untuk membuat koneksi visual antara dua widget.
Widget yang ditampilkan oleh closedBuilder
ditampilkan pada awalnya, dan diperluas ke widget yang ditampilkan oleh openBuilder
saat penampung diketuk atau saat callback openContainer
dipanggil.
Untuk menghubungkan callback openContainer
ke model tampilan, tambahkan kartu baru yang meneruskan viewModel ke widget QuestionCard dan simpan callback yang akan digunakan untuk menampilkan layar "Game Over":
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel =
QuizViewModel(onGameOver: _handleGameOver);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
)
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
Tambahkan widget baru, GameOverScreen:
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text(
'You Win!',
style: Theme.of(context).textTheme.displayLarge,
),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
Di widget QuestionCard, ganti Kartu dengan widget OpenContainer dari paket animasi, dengan menambahkan dua kolom baru untuk viewModel dan callback penampung terbuka:
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}
8. Selamat
Selamat, Anda telah berhasil menambahkan efek animasi ke aplikasi Flutter, dan mempelajari komponen inti sistem animasi Flutter. Secara khusus, Anda telah mempelajari:
- Cara menggunakan ImplicitlyAnimatedWidget
- Cara menggunakan ExplicitlyAnimatedWidget
- Cara menerapkan Kurva dan Tween ke animasi
- Cara menggunakan widget transisi bawaan seperti AnimatedSwitcher atau PageRouteBuilder
- Cara menggunakan efek animasi bawaan yang menarik dari paket
animations
, seperti FadeThroughTransition dan OpenContainer - Cara menyesuaikan animasi transisi default, termasuk menambahkan dukungan untuk Kembali Prediktif di Android.
Apa selanjutnya?
Lihat beberapa codelab ini:
- Mem-build tata letak aplikasi animasi yang responsif dengan Material 3
- Mem-build Transisi yang Indah dengan Gerakan Material untuk Flutter
- Membuat tampilan aplikasi Flutter menjadi lebih menarik
Atau download aplikasi contoh animasi, yang menampilkan berbagai teknik animasi
Bacaan lebih lanjut
Anda dapat menemukan lebih banyak referensi animasi di flutter.dev:
- Pengantar animasi
- Tutorial animasi (tutorial)
- Animasi implisit (tutorial)
- Menganimasikan properti penampung (cookbook)
- Membuat widget memudar dan muncul (cookbook)
- Animasi hero
- Menganimasikan transisi rute halaman (cookbook)
- Menganimasikan widget menggunakan simulasi fisika (cookbook)
- Animasi bertingkat
- Widget animasi dan gerakan (Katalog widget)
Atau baca artikel ini di Medium:
- Pembahasan mendalam tentang animasi
- Animasi implisit kustom di Flutter
- Pengelolaan animasi dengan Flutter dan Flux / Redux
- Bagaimana Cara Memilih Widget Animasi Flutter yang Tepat untuk Anda?
- Animasi arah dengan animasi eksplisit bawaan
- Dasar-dasar animasi Flutter dengan animasi implisit
- Kapan saya harus menggunakan AnimatedBuilder atau AnimatedWidget?