Tác giả Đoàn Ngọc Tú là giảng viên khóa Lộ trình Flutter
Thông tin chi tiết khóa học tại : https://flutter.techmaster.vn/


Bạn sẽ học được gì

  • Cách sử dụng các class cơ bản từ thư viện Animation và thêm Animation vào 1 widget.
  • Phân biệt khi nào sử dụng AnimatedWidget và AnimatedBuilder.

Bài viết chỉ cho bạn cách để tạo ra 1 explicit animation( explicit animation là gì mình sẽ giới thiệu sau) trong Flutter. Sau khi giới thiệu 1 vài khái niệm, class và method quan trọng, mình sẽ giúp các bạn hiểu rõ hơn thông qua một vài ví dụ cụ thể nhé! Các ví dụ này được xây dựng dựa trên nhau, từ đó sẽ giới thiệu cho các bạn các khía cạnh khác nhau của thư viện animation.

SDK của Flutter cũng cung cấp 1 bộ explicit animation được tích hợp sẵn, ví dụ: FadeTransition, SizeTransition, and SlideTransition. Những animation đơn giản này được kích hoạt bằng cách cài đặt điểm bắt đầu và điểm kết thúc. Chúng dễ triển khai hơn những explicit animation tùy chỉnh, sẽ được mô tả cụ thể trong bài viết.

Các khái niệm và class về animation quan trọng

Vấn đề ở đây là gì?

  • Animation, 1 class lõi trong thư viện animation của Flutter, thêm các giá trị để hướng dẫn 1 animation.
  • Một Animation object biết trạng thái hiện tại của animation (ví dụ, trạng thái có thể là đã bắt đầu, đã dừng lại hay đang diễn tiếp, hay đang trong trạng thái đảo ngược.), nhưng không biết bất cứ điều gì về những cái xuất hiện trên màn hình.
  • Một AnimationController quản lý Animation.
  • Một CurvedAnimation xác định tiến trình như 1 đường cong phi tuyến tính.
  • Một Tween xác định 1 mảng giá trị cho object đang được tạo animation. Ví dụ, 1 Tween có thể xác định 1 phép biến đổi từ màu đỏ sang màu xanh, hoặc giá trị từ 0 đến 255.
  • Sử dụng Listeners hoặc StatusListeners để theo dõi trạng thái của animation.

Hệ thống animation trong Flutter dựa trên object Animation. Widgets có thể kết hợp nhiều animation bằng cách đọc giá trị hiện tại của nó và lắng nghe sự thay đổi trạng thái, hoặc có thể sử dụng animation làm cơ sở cho các animation phức tạp hơn mà chúng sử dụng cho các widget khác.

Animation

Trong Flutter, một Animation object không biết 1 điều gì về những cái đang hiển thị trên màn hình. Một Animation là một abstract class có thể biết được giá trị và trạng thái hiện tại ( hoàn thành hay bị biến mất). Một trong những loại animation được sử dụng rộng rãi nhất.

Một Animation object tạo ra 1 cách tuần tự các số có giá trị nằm giữa 2 giá trị trong 1 khoảng thời gian xác định. Đầu ra của một Animation object có thể là 1 đường thẳng, 1 đường cong hoặc 1 chức năng theo bước, hoặc bất cứ ánh xạ nào khác mà bạn có thể nghĩ ra. Phụ thuộc vào cách Animation object đó được kiểm soát, nó có thể chạy đảo ngược, hoặc thậm chí rẽ sang hướng khác ở đoạn giữa.

Animation có thể dùng nhiều loại dữ liệu khác double, ví như Animation<Color> hoặc Animation<Size>.

Một Animation object có trạng thái. Giá trị hiện tại của nó luôn luôn có sẵn trong field value.

Một Animation object không biết bất cứ gì về việc rendering hay hàm build().

Curved­Animation

Một Curved­Animation xác định tiến trình của 1 animation như 1 đường cong phi tuyến tính.

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

Lưu ý: Class Curves định nghĩa nhiều đường cong thường được sử dụng hoặc bạn có thể tạo đường cong của riêng mình. Ví dụ:

import 'dart:math';

class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}

Bạn có thể xem tài liệu về Curve để có cái nhìn hoàn thiện về các hằng số của Curves đi kèm trong Flutter.

CurvedAnimationAnimationController (được mô tả trong phần tiếp theo) đều thuộc loại Animation <double>, vì vậy bạn có thể chuyển chúng cho nhau. CurvedAnimation bao bọc đối tượng mà nó đang sửa đổi — bạn đừng tạo lớp con của AnimationController để triển khai một đường cong.

Animation­Controller

AnimationController là một đối tượng Animation đặc biệt tạo ra một giá trị mới bất cứ khi nào phần cứng sẵn sàng cho một khung hình mới. Theo mặc định, AnimationController tạo tuyến tính các số từ 0.0 đến 1.0 trong một khoảng thời gian nhất định. Ví dụ: đoạn code sau tạo một đối tượng Animation, nhưng không bắt đầu chạy:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

AnimationController bắt nguồn từ Animation <double>, vì vậy nó có thể được sử dụng ở bất cứ nơi nào đối tượng Animation cần . Tuy nhiên, AnimationController có các phương thức bổ sung để kiểm soát animation. Ví dụ: bạn bắt đầu một animation bằng phương thức .forward(). Việc tạo ra các số được gắn với việc làm mới màn hình, do đó, thường có 60 số được tạo mỗi giây. Sau mỗi số được tạo, mỗi đối tượng Animation sẽ gọi các đối tượng Listener đính kèm. Để tạo danh sách hiển thị tùy chỉnh cho từng phần tử con, hãy xem RepaintBoundary.

Khi tạo một AnimationController, bạn chuyển cho nó một đối số vsync. Sự hiện diện của vsync ngăn cản các animation ngoài màn hình tiêu tốn tài nguyên không cần thiết. Bạn có thể sử dụng đối tượng trạng thái của mình làm vsync bằng cách thêm SingleTickerProviderStateMixin vào định nghĩa class.

Tween

Mặc định, AnimationController object quy định giá trị nằm trong khoảng 0.0 đến 1.0. Nếu bạn cần 1 khoảng giá trị khác hoặc loại data khác, bạn có thể sử dụng Trên để cấu hình 1 animation và tạo ra 1 khoảng giá trị hoặc loại data khác. Ví dụ:

tween = Tween<double>(begin: -200, end: 0);

Tween là 1 stateless object có khoảng beginend. Việc duy nhất của Tween là định nghĩa 1 ánh xạ từ khoảng giá trị input ra giá trị output. Giá trị input thường là 0.0 tới 1.0, nhưng là không bắt buộc.

Tween kế thừa từ Animatable<T>, không phải kế thừa từ Animation<T>. Một Animatable, giống như Animation, nhưng không phải có giá trị đầu ra. Ví dụ ColorTween xác định tiến trình biến đổi giữa 2 màu:

colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween object không lưu trữ state. Thay vào đó, nó cung cấp phương thức evaluate giúp ánh xạ phương thức tới giá trị hiện tại của animation. Giá trị hiện tại của Animation object có thể được tìm thấy trong phương thức .value.

Tween.animate

Để sử dụng Tween object, bạn có thể gọi method animate(). Ví dụ sau đây tạo ra các giá trị từ 0 tới 255 trong khoảng thời gian 500ms:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

animate() method trả về Animation, không phải là một Animatable.

Ví dụ dưới đây tạo 1 controller, 1 curve, và 1 Tween:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

Animation notifications

Một Animation object có thể có ListenersStatusListeners, được định nghĩa bằng addListener()addStatusListener(). Một Listener được gọi bất cứ khi nào giá trị của Animation thay đổi. Hành vi phổ biến nhất của một Listener là gọi setState () để tạo lại giao diện. StatusListener được gọi khi một Animation bắt đầu, kết thúc, di chuyển về phía trước hoặc di chuyển ngược lại, giống như được định nghĩa bởi AnimationStatus. Phần tiếp theo của bài viết sẽ có một ví dụ về phương thức addListener() và theo dõi tiến trình của Animation sẽ chỉ ra một ví dụ về addStatusListener().

Các ví dụ về Animation

Lý thuyết dài dòng và hơi khó hiểu đúng không? Vậy thì phần tiếp theo sau đây chúng ta sẽ đến với các ví dụ thực tế về animation để hiểu rõ hơn nhé. Mỗi ví dụ mình sẽ demo các đoạn code cũng như link tới source code để các bạn có thể tham khảo dễ dàng hơn.

Rendering animations

Phần này chúng ta học được gì?

  • Cách tạo 1 animation đơn giản và thêm nó vào trong widget sử dụng addListener()setState().
  • Mỗi khi Animation tạo 1 số mới, addListener() function sẽ gọi setState().
  • Cách để định nghĩa 1 AnimationController với param vsync.
  • Hiểu được .. syntax trong ..addListener, còn được biết đến dưới cách gọi Dart’s cascade notation.
  • Để tạo 1 class private, bắt đầu tên nó với tiền tố _.

Cho đến lúc này, bạn đã học được cách tạo ra một dãy số trong 1 khoảng thời gian. Không gì được render vào màn hình. Để render với 1 Animation object, lưu trữ Animation object như là 1 thành phần của widget của bạn, sau đó sử dụng giá trị của nó để quyết định khi nào vẽ lên trên màn hình.

Ví dụ dưới đây sẽ vẽ Flutter logo mà không có animation:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);

@override
_LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: const FlutterLogo(),
      ),
    );
}
}

App source animate()

Đoạn code tiếp theo sử dụng animation để tạo logo có size từ 0 đến full sim. Khi định nghĩa 1 AnimationController, bạn phải truyền vsync object. Chúng ta sẽ nói cụ thể hơn về vsync trong phần tới.

	@@ -9,16 +9,39 @@
99	    _LogoAppState createState() => _LogoAppState();
10	  }
11	- class _LogoAppState extends State<LogoApp> {
11	+ class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
12	+   late Animation<double> animation;
13	+   late AnimationController controller;
14	+
15	+   @override
16	+   void initState() {
17	+     super.initState();
18	+     controller =
19	+         AnimationController(duration: const Duration(seconds: 2), vsync: this);
20	+     animation = Tween<double>(begin: 0, end: 300).animate(controller)
21	+       ..addListener(() {
22	+         setState(() {
23	+           // The state that has changed here is the animation object’s value.
24	+         });
25	+       });
26	+     controller.forward();
27	+   }
28	+
29	    @override
30	    Widget build(BuildContext context) {
31	      return Center(
32	        child: Container(
33	          margin: const EdgeInsets.symmetric(vertical: 10),
17	-         height: 300,
18	-         width: 300,
34	+         height: animation.value,
35	+         width: animation.value,
36	          child: const FlutterLogo(),
37	        ),
38	      );
39	    }
40	+
41	+   @override
42	+   void dispose() {
43	+     controller.dispose();
44	+     super.dispose();
45	+   }
46	  }

App source animate1

Phương thức addListener() sẽ gọi setState(), nên mỗi lần Animation tạo ra 1 số mới, Frame hiện tại sẽ được đánh dấu là đã cũ và sẽ buộc build() được gọi lại. Trong build(), container thay đổi size bởi vì chiều cao và rộng của nó sử dụng animation.value thay vì size được fix cứng. Chú ý dispose controller khi state object bị hủy để tránh Memory leak nhé.

Như vậy, với 1 vài thay đổi, bạn đã tạo ra animation đầu tiên trong Flutter rồi đó.

Chú ý: trong ví dụ trên có sử dụng Dart’s cascade notation: ..addListener():

animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
    // ···
});

tương đương đoạn code sau:

animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation.addListener(() {
    // ···
});

Simplifying with Animated­Widget - Đơn giản hóa animation với Animated­Widget

Ở phần này chúng ta sẽ học được gì:

  • Cách sử dụng Animated­Widget class ( thay vì sử dụng addListener()setState()) để tạo 1 widget có thể animate.
  • Sử dụng Animated­Widget để tạo widget chạy 1 animation có thể tái sử dụng. Để phân tách sự dịch chuyển từ widget, sử dụng AnimatedBuilder, sẽ được đề cập tới ở phần sau.
  • Các ví dụ về các Animated­Widget trong Flutter API: AnimatedBuilder, AnimatedModalBarrier, DecoratedBoxTransition, FadeTransition, PositionedTransition, RelativePositionedTransition, RotationTransition, ScaleTransition, SizeTransition, SlideTransition.

Ví dụ số 1: AnimatedLogo:

class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
}
}

AnimatedLogo sử dụng giá trị hiện tại của animation khi tự vẽ chính nó.

LogoApp vẫn sẽ quản lý AnimationControllerTween và truyền Animation object vào trong AnimatedLogo. Chúng ta sẽ có đoạn code như sau:

Ví dụ số 2:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

// #docregion AnimatedLogo
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
}
}
// #enddocregion AnimatedLogo

class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);

@override
_LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
}

@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);

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

Monitoring the progress of the animation

Những gì sẽ nắm được:

  • Sử dụng addStatusListener() cho thông báo thay đổi trạng thái của animation, như là bắt đầu, dừng lại hay đổi chiều animation.
  • Chạy 1 animation lặp lại bằng cách đảo ngược animation khi animation chạy xong hoặc trở lại trạng thái ban đầu.

Việc biết khi nào trạng thái animation thay đổi( kết thúc, bắt đầu, đang chạy, quay ngược lại) là rất cần thiết. Bạn có thể nhận thông báo cho việc này bằng cách sử dụng addStatusListener(). Đoạn code sau chỉnh sửa đôi chút ví dụ trước đó để có thể lắng nghe thay đổi.

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) => print('$status'));
    controller.forward();
}
// ...
}

Output của đoạn code trên:

AnimationStatus.forward
AnimationStatus.completed

Tiếp theo, chúng ta sẽ sử dụng addStatusListener() để đảo ngược animation lúc bắt đầu hoặc lúc kết thúc

Ví dụ số 3:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
}
}

class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);

@override
_LogoAppState createState() => _LogoAppState();
}

// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      // #enddocregion print-state
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })
      // #docregion print-state
      ..addStatusListener((status) => print('$status'));
    controller.forward();
}
// #enddocregion print-state

@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);

@override
void dispose() {
    controller.dispose();
    super.dispose();
}
// #docregion print-state
}

Refactoring with AnimatedBuilder

Chúng ta sẽ học được gì?

  • Một AnimatedBuilder hiểu được cách hiển thị quá trình chuyển đổi.

  • AnimatedBuilder không biết cách hiển thị widget con, cũng như không quản lý đối tượng Animation.

  • Sử dụng AnimatedBuilder để mô tả animation như 1 phần của build() cho widget khác.

    Nếu đơn giản bạn muốn xác định 1 widget với 1 animation có khả năng tái sử dụng, bạn có thể dùng AnimatedWidget ( chúng ta đã nói tới ở phần trước đó)

  • Một số ví dụ về AnimatedBuilder trong flutter: BottomSheet, ExpansionTile, PopupMenu, ProgressIndicator, RefreshIndicator, Scaffold, SnackBar, TabBar, TextField.

Có một vấn đề với đoạn code ở ví dụ số 3, đó là thay đổi animation yêu cầu theo việc thay đổi widget tạo ra logo. Một giải pháp tốt hơn đó là phân tách trách nhiệm cho từng class khác nhau:

  • Class tạo logo.

  • Class xác định Animation object.

  • Class tạo Quá trình chuyển đổi.

    Bạn có thể đạt được việc phân tách này với sự giúp đỡ từ class AnimatedBuilder . Một AnimatedBuilder là 1 class trong render trê. Giống như AnimatedWidget , AnimatedBuilder tự động lắng nghe thông báo từ Animation object, và đánh dấu widget cần render lại, do vậy bạn không cần gọi addListener()

Widget tree cho ví dụ số 4 này như sau:

Bắt đầu từ dưới lên theo widget tree trên, code cho logo như sau:

class LogoWidget extends StatelessWidget {
const LogoWidget({Key? key}) : super(key: key);

// Leave out the height and width so it fills the animating parent
@override
Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
}
}

Ba khối ở giữa trong sơ đồ đều được tạo trong phương thức build () trong GrowTransition, được hiển thị bên dưới. Bản thân widget GrowTransition là stateless và chứa tập hợp các biến cuối cùng cần thiết để xác định chuyển tiếp của animation. Hàm build() tạo và trả về AnimatedBuilder, hàm này nhận phương thức ( Anonymous builder) và đối tượng LogoWidget làm tham số. Công việc hiển thị quá trình chuyển đổi thực sự xảy ra trong phương thức ( Anonymous builder), phương pháp này tạo một Container chứa có kích thước thích hợp để buộc LogoWidget phải thu nhỏ lại cho vừa vặn.

Một điểm khó khăn trong đoạn mã dưới đây là child trông giống như nó được chỉ định hai lần. Điều đang xảy ra là tham chiếu bên ngoài của child được chuyển đến AnimatedBuilder, tham chiếu này sẽ chuyển nó tới vùng ẩn danh, sau đó sử dụng đối tượng đó làm con của nó. Kết quả thực là AnimatedBuilder được chèn vào giữa hai widget trong widget tree.

class GrowTransition extends StatelessWidget {
const GrowTransition({required this.child, required this.animation, Key? key})
      : super(key: key);

final Widget child;
final Animation<double> animation;

@override
Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
}
}

Cuối cùng, đoạn code tạo animation ở ví dụ này khá giống với ví dụ số 2. initState() tạo 1 AnimationController và 1 Tween, sau đó bind chúng với phương thức animate(). Source code đầy đủ như sau:

Ví dụ số 4:

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
const LogoWidget({Key? key}) : super(key: key);

// Leave out the height and width so it fills the animating parent
@override
Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
}
}
// #enddocregion LogoWidget

// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
const GrowTransition({required this.child, required this.animation, Key? key})
      : super(key: key);

final Widget child;
final Animation<double> animation;

@override
Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
}
}
// #enddocregion GrowTransition

class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);

@override
_LogoAppState createState() => _LogoAppState();
}

// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
}
// #enddocregion print-state

@override
Widget build(BuildContext context) {
    return GrowTransition(
      child: const LogoWidget(),
      animation: animation,
    );
}

@override
void dispose() {
    controller.dispose();
    super.dispose();
}
// #docregion print-state
}

Simultaneous animations

Trong phần này, bạn sẽ tạo 1 ví dụ từ việc theo dõi tiến trình của animation từ ví dụ số 3, ví dụ sử dụng AnimatedWidget để tạo animation phóng to và thu nhỏ liên tục. Hãy xem xét trường hợp bạn muốn tạo animation phóng to và thu nhỏ trong khi độ mờ của animation từ trong suốt sang mờ đục.

Ví dụ dưới đây sẽ hướng dẫn bạn cách kết hợp nhiều với cùng 1 AnimationController, mỗi một tween quản lý 1 hiệu ứng khác nhau cho animation. Nếu bạn sử dụng animation liên quan tới size và opacity, bạn có thể sử dụng  FadeTransition vả  SizeTransition thay thế.

Mỗi tween quản lý 1 phần của animation. Ví dụ:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);

Bạn sẽ lấy được kích cỡ từ sizeAnimation.value và lấy được độ mờ từ sizeAnimation.value.

Code cho ví dụ số 5:

class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({Key? key, required Animation<double> animation})
      : super(key: key, listenable: animation);

// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);

@override
Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: const EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: const FlutterLogo(),
        ),
      ),
    );
}
}

class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);

@override
_LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
}

@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);

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

Tổng kết

Hướng dẫn này cung cấp cho bạn nền tảng để tạo animation trong Flutter bằng Tweens, nhưng có nhiều class khác để khám phá. Bạn có thể tìm hiểu thêm các lớp Tween, animation chuyên biệt dành riêng cho Material Design, ReverseAnimation, chuyển đổi phần tử được chia sẻ (còn được gọi là Hero animation), mô phỏng vật lý và phương thức fling(). Bạn có thể xem thêm tại đây cho các tài liệu và ví dụ mới nhất hiện có.

Tham khảo

Tham khảo them https://docs.flutter.dev/development/ui/widgets/animation