Flutter: Hướng dẫn Animation
Mục lục bài viết:
Bạn sẽ học
- Cách sử dụng các lớp cơ bản từ thư viện hoạt ảnh để thêm hoạt ảnh vào một widget.
- Khi nào thì sử dụng
AnimatedWidgetvàAnimatedBuilder.
Hướng dẫn này chỉ cho bạn cách tạo hoạt ảnh tường minh trong Flutter. Sau khi giới thiệu một số khái niệm, lớp và phương thức thiết yếu trong thư viện hoạt ảnh, bạn sẽ được hướng dẫn qua 5 ví dụ về hoạt ảnh. Các ví dụ được xây dựng dựa trên nhau, giới thiệu cho bạn các khía cạnh khác nhau của thư viện hoạt ảnh.
Flutter SDK cũng cung cấp hoạt ảnh chuyển động ngầm định, chẳng hạn như FadeTransition, SizeTransition, và SlideTransition. Những hoạt ảnh đơn giản này được kích hoạt bằng cách đặt điểm bắt đầu và điểm kết thúc. Chúng dễ triển khai hơn các hoạt ảnh tường minh.
Các khái niệm và lớp hoạt ảnh cơ bản
Vấn đề ở đây là gì?
- Animation, một lớp cốt lõi trong thư viện hoạt ảnh của Flutter, nội suy các giá trị được sử dụng để hướng dẫn hoạt ảnh.
- Một đối tượng
Animationbiết trạng thái hiện tại của hoạt ảnh (ví dụ: cho dù nó được bắt đầu, dừng lại hay đang di chuyển về phía trước hoặc ngược lại), nhưng không biết gì về những gì xuất hiện trên màn hình. - Một AnimationController để quản lý
Animation. - Một CurvedAnimation để định nghĩa tiến trình là một đường cong phi tuyến tính.
- Một Tween để nội suy giữa phạm vi dữ liệu được sử dụng bởi đối tượng đang được làm động. Ví dụ:
Tweencó thể định nghĩa một phép nội suy từ màu đỏ sang màu xanh lam hoặc từ 0 đến 255. - Sử dụng
Listeners vàStatusListeners để theo dõi các thay đổi trạng thái hoạt ảnh.
Hệ thống hoạt ảnh trong Flutter dựa trên các đối tượng Animation được định kiểu. Các widget có thể kết hợp các hoạt ảnh này trực tiếp trong các hàm xây dựng của chúng bằng cách đọc giá trị hiện tại của chúng và lắng nghe các thay đổi trạng thái của chúng hoặc chúng có thể sử dụng các hoạt ảnh làm cơ sở cho các hoạt ảnh phức tạp hơn mà chúng chuyển cho các widget khác.
Animation<double>
Trong Flutter, đối tượng Animation không hề biết gì về màn hình. Animation là một lớp trừu tượng hiểu giá trị hiện tại và trạng thái của nó (đã hoàn thành hoặc bị loại bỏ). Một trong những kiểu hoạt ảnh được sử dụng phổ biến hơn là Animation<double>.
Một đối tượng Animation tuần tự tạo ra các số nội suy giữa hai giá trị trong một khoảng thời gian nhất định. Đầu ra của đối tượng Animation có thể là tuyến tính, một đường cong, một hàm bước hoặc bất kỳ ánh xạ nào khác mà bạn có thể tạo ra. Tùy thuộc vào cách Animation được điều khiển, nó có thể chạy ngược lại hoặc thậm chí chuyển hướng ở giữa.
Hoạt ảnh cũng có thể nội suy các loại khác với double, chẳng hạn như Animation<Color> hoặc Animation<Size>.
Mỗi đối tượng Animation đều có trạng thái. Giá trị hiện tại của nó luôn có sẵn trong phương thức .value.
Animation không biết gì về kết xuất hoặc các hàm build().
CurvedAnimation
CurvedAnimation định nghĩa tiến trình của hoạt ảnh là một đường cong phi tuyến tính.
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
Lưu ý: Lớp Curves định nghĩa nhiều đường cong thường được sử dụng, hoặc bạn có thể tạo riêng của bạn. Ví dụ:
import 'dart:math'; class ShakeCurve extends Curve { @override double transform(double t) => sin(t * pi * 2); }
Duyệt qua tài liệu Curves để có danh sách đầy đủ (với bản xem trước trực quan) về các hằng Curves đi kèm với Flutter.
CurvedAnimation và AnimationController (được mô tả trong phần tiếp theo) đều thuộc loại Animation<double>, vì vậy bạn có thể truyền chúng thay thế cho nhau. CurvedAnimation sẽ gộp đối tượng mà nó đang sửa đổi - bạn không phân lớp AnimationController để triển khai một đường cong.
AnimationController
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 mới. Theo mặc định, một giá trị AnimationController tuyến tính tạo ra 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 khởi động chạy nó:
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 cần đối tượng Animation. Tuy nhiên, AnimationController có các phương thức bổ sung để kiểm soát hoạt ảnh. Ví dụ: bạn bắt đầu một hoạt ảnh với 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 thường 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 con, hãy xem RepaintBoundary.
Khi tạo một AnimationController, bạn truyền cho nó một đối số vsync. Sự hiện diện của vsync sẽ ngăn chặn các hoạt ảnh 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 lớp. Bạn có thể xem một ví dụ về điều này trong animate1 trên GitHub.
Lưu ý: Trong một số trường hợp, một vị trí có thể vượt quá phạm vi 0.0-1.0 của
AnimationController. Ví dụ, hàmfling()cho phép bạn cung cấp vận tốc, lực và vị trí (thông qua đối tượng Force). Vị trí có thể là bất kỳ thứ gì và vì vậy có thể nằm ngoài phạm vi 0.0-1.0.
CurvedAnimationcũng có thể vượt quá phạm vi 0.0-1.0, ngay cả khiAnimationControllerkhông vượt quá. Tùy thuộc vào đường cong được chọn, đầu ra củaCurvedAnimationcó thể có phạm vi rộng hơn đầu vào. Ví dụ: các đường cong đàn hồi chẳng hạn nhưCurves.elasticInvượt quá mức đáng kể hoặc vượt quá phạm vi mặc định.
Tween
Theo mặc định, đối tượng AnimationController nằm trong khoảng từ 0.0 đến 1.0. Nếu bạn cần một phạm vi khác hoặc một kiểu dữ liệu khác, bạn có thể sử dụng một Tween để định cấu hình hoạt ảnh để nội suy cho một phạm vi hoặc kiểu dữ liệu khác. Ví dụ: giá trị Tween sau đây là từ -200.0 đến 0.0:
tween = Tween<double>(begin: -200, end: 0);
Tween là đối tượng không trạng thái chỉ nhận begin và end. Công việc duy nhất của Tween là định nghĩa một ánh xạ từ phạm vi đầu vào đến phạm vi đầu ra. Phạm vi đầu vào thường là 0.0 đến 1.0, nhưng đó không phải là một yêu cầu.
Tween thừa kế từ Animatable<T>, không phải từ Animation<T>. Animatable giống Animation ở chô không cần phải xuất ra giá trị double. Ví dụ: ColorTween chỉ định sự tiến triển giữa hai màu.
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween không lưu trữ bất kỳ trạng thái nào. Thay vào đó, nó cung cấp phương thức evaluate(Animation<double> animation) áp dụng chức năng ánh xạ cho giá trị hiện tại của hoạt ảnh. Giá trị hiện tại của Animation có thể được tìm thấy trong phương thức .value. Phương thức evaluate cũng thực hiện một số công việc quản lý, chẳng hạn như đảm bảo rằng phần bắt đầu và kết thúc được trả về khi giá trị hoạt ảnh tương ứng là 0.0 và 1.0.
Tween.animate
Để sử dụng đối tượng Tween thì ta gọi animate() vào Tween, đi qua trong đối tượng điều khiển. Ví dụ, đoạn mã sau tạo ra các giá trị nguyên từ 0 đến 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);
Lưu ý: Phương thức
animate()trả về một Animation, không phải là một Animatable.
Ví dụ sau cho thấy một controller, một đường cong và một Tween:
AnimationController controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this); final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut); Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
Thông báo hoạt ảnh
Đối tượng Animation có thể có nhiều Listener và nhiều StatusListener, được định nghĩa với các phương thức tương ứng là addListener() và addStatusListener(). Listener được gọi bất cứ khi nào giá trị của hoạt ảnh thay đổi. Hành vi phổ biến nhất của Listener là gọi setState() để gây dựng lại. StatusListener được gọi khi một hoạt ảnh bắt đầu, kết thúc, di chuyển về phía trước hoặc di chuyển ngược lại, như được định nghĩa bởi AnimationStatus. Phần tiếp theo có một ví dụ về phương thức addListener() và Giám sát tiến trình của hoạt ảnh có một ví dụ về addStatusListener().
Ví dụ về hoạt ảnh
Phần này sẽ hướng dẫn bạn qua 5 ví dụ về hoạt ảnh. Mỗi phần cung cấp một liên kết đến mã nguồn cho ví dụ đó.
Kết xuất hoạt ảnh
Vấn đề ở đây là:
- Cách thêm hoạt ảnh cơ bản vào tiện ích con bằng cách sử dụng
addListener()vàsetState(). - Mỗi khi Animation tạo ra một số mới thì phương thức
addListener()sẽ gọisetState(). - Cách định nghĩa một
AnimationControllervới tham sốvsyncbắt buộc. - Hiểu cú pháp "
.."trong "..addListener", còn được gọi là ký hiệu phân tầng của Dart. - Để đặt một lớp ở chế độ riêng tư, hãy bắt đầu tên của nó bằng dấu gạch dưới (
_).
Cho đến thời điểm này thì bạn đã học được cách tạo một chuỗi số theo thời gian, nhưng không có gì được hiển thị (render) trên màn hình. Để hiển thị với một đối tượng Animation, hãy lưu trữ Animation như một thành viên của widget con của bạn, sau đó sử dụng giá trị của nó để quyết định cách vẽ.
Hãy xem ứng dụng sau đây vẽ biểu trưng Flutter mà không có hoạt ảnh:
import 'package:flutter/material.dart'; void main() => runApp(LogoApp()); class LogoApp extends StatefulWidget { _LogoAppState createState() => _LogoAppState(); } class _LogoAppState extends State<LogoApp> { @override Widget build(BuildContext context) { return Center( child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: 300, width: 300, child: FlutterLogo(), ), ); } }
Nguồn ứng dụng: animate0
Phần sau cho thấy cùng một đoạn mã đã được sửa đổi để tạo hiệu ứng hoạt hình cho logo phát triển từ kích thước 0 lên kích thước đầy đủ (full size). Khi định nghĩa một AnimationController, bạn phải truyền vào một đối tượng vsync. Tham số vsync được mô tả trong phần AnimationController.
Những thay đổi từ ví dụ không hoạt ảnh được đánh dấu:
Nguồn ứng dụng: animate1
Phương thức addListener() sẽ gọi setState(), vì vậy mỗi khi Animation tạo ra một số điện thoại mới, khung hiện tại sẽ được đánh dấu, và build() sẽ được gọi một lần nữa. Trong build(), vùng chứa thay đổi kích thước vì chiều cao và chiều rộng của nó hiện sử dụng animation.value thay vì giá trị được mã hóa cứng. Ta bỏ controller khi đối tượng State bị loại bỏ để tránh rò rỉ bộ nhớ.
Với một vài thay đổi này, bạn đã tạo hoạt ảnh đầu tiên của mình trong Flutter!
Thủ thuật ngôn ngữ của Dart: Bạn có thể không quen với ký hiệu phân tầng của Dart - hai dấu chấm trong ..addListener(). Cú pháp này có nghĩa là phương thức addListener() được gọi với giá trị trả về từ animate(). Hãy xem xét ví dụ sau:
animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() { // ··· });
Đoạn mã trên tương đương với:
animation = Tween<double>(begin: 0, end: 300).animate(controller); animation.addListener(() { // ··· });
Bạn có thể tìm hiểu thêm về ký hiệu tầng trong Tham quan Ngôn ngữ Dart.
Đơn giản hóa với AnimatedWidget
Vấn đề ở đây là:
- Cách sử dụng lớp trợ giúp AnimatedWidget (thay vì
addListener()vàsetState()) để tạo widget con hoạt ảnh.- Sử dụng
AnimatedWidgetđể tạo một widget thực hiện hoạt ảnh có thể sử dụng lại. Để tách quá trình chuyển đổi khỏi widget, hãy sử dụng mộtAnimatedBuilder, như được thể hiện trong phần Cấu trúc lại bằng AnimatedBuilder.- Ví dụ về các
AnimatedWidgettrong API Flutter:AnimatedBuilder,AnimatedModalBarrier,DecoratedBoxTransition,FadeTransition,PositionedTransition,RelativePositionedTransition,RotationTransition,ScaleTransition,SizeTransition,SlideTransition.
Lớp cơ sở AnimatedWidget cho phép bạn tách mã widget cốt lõi khỏi mã hoạt ảnh. AnimatedWidget không cần duy trì đối tượng State để giữ hoạt ảnh. Ta thêm lớp AnimatedLogo sau:
class AnimatedLogo extends AnimatedWidget { AnimatedLogo({Key key, Animation<double> animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { final animation = listenable as Animation<double>; return Center( child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: animation.value, width: animation.value, child: FlutterLogo(), ), ); } }
AnimatedLogo sử dụng giá trị hiện tại của animation khi tự vẽ.
LogoApp vẫn quản lý AnimationController và Tween, và nó truyền đối tượng Animation tới AnimatedLogo:
Nguồn ứng dụng: animate2
Theo dõi tiến trình của hoạt ảnh
Vấn đề ở đây là:
- Sử dụng
addStatusListener()cho các thông báo về các thay đổi đối với trạng thái của hoạt ảnh, chẳng hạn như bắt đầu, dừng hoặc đảo ngược hướng.- Chạy hoạt ảnh trong một vòng lặp vô hạn bằng cách đảo ngược hướng khi hoạt ảnh đã hoàn thành hoặc trở lại trạng thái bắt đầu.
Thường sẽ hữu ích khi biết khi nào một hoạt ảnh thay đổi trạng thái, chẳng hạn như kết thúc, tiến lên hoặc đảo ngược. Bạn có thể nhận thông báo cho điều này với addStatusListener(). Đoạn mã sau sửa đổi ví dụ trên để nó lắng nghe sự thay đổi trạng thái và in bản cập nhật. Dòng được đánh dấu vàng thể hiện sự thay đổi:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { Animation<double> animation; 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((state) => print('$state')); controller.forward(); } // ... }
Chạy đoạn mã trên ta được:
AnimationStatus.forward
AnimationStatus.completed
Tiếp theo, ta sử dụng addStatusListener() để đảo ngược hoạt ảnh ở đầu hoặc cuối. Điều này tạo ra hiệu ứng "thở":
Nguồn ứng dụng: animate3
Cấu trúc lại bằng AnimatedBuilder
Vấn đề ở đây là:
- AnimatedBuilder hiểu cách hiển thị quá trình chuyển đổi.
AnimatedBuilderkhông biết cách hiển thị widget, cũng như không quản lý đối tượngAnimation.- Sử dụng
AnimatedBuilderđể mô tả hoạt ảnh như một phần của phương pháp xây dựng cho một tiện ích con khác. Nếu bạn chỉ muốn định nghĩa một tiện ích con có hoạt ảnh có thể sử dụng lại, hãy sử dụngAnimatedWidget, như được thể hiện trong phần Đơn giản hóa với AnimatedWidget.- Ví dụ về
AnimatedBuilderstrong API Flutter:BottomSheet,ExpansionTile,PopupMenu,ProgressIndicator,RefreshIndicator,Scaffold,SnackBar,TabBar,TextField.
Một vấn đề với code trong ví dụ animate3, đó là việc thay đổi hoạt ảnh bắt buộc phải thay đổi tiện ích hiển thị biểu trưng. Một giải pháp tốt hơn là tách các công việc thành các lớp khác nhau như sau:
- Kết xuất logo
- Định nghĩa đối tượng
Animation - Kết xuất quá trình chuyển đổi
Bạn có thể thực hiện việc phân tách này với sự trợ giúp của lớp AnimatedBuilder. AnimatedBuilder là một lớp riêng biệt trong cây kết xuất. Giống như AnimatedWidget, AnimatedBuilder sẽ tự động lắng nghe thông báo từ đối tượng Animation và đánh dấu cây widget đó nếu cần, vì vậy bạn không cần phải gọi addListener().
Cây widget cho ví dụ animate4 trông giống như sau:
Bắt đầu từ cuối cây tiện ích con, đoạn code để hiển thị icon rất đơn giản như sau:
class LogoWidget extends StatelessWidget { // Leave out the height and width so it fills the animating parent Widget build(BuildContext context) => Container( margin: EdgeInsets.symmetric(vertical: 10), child: FlutterLogo(), ); }
Ba khối ở giữa trong sơ đồ đều được tạo theo phương thức build() trong GrowTransition, được hiển thị bên dưới. Bản thân widget GrowTransition là không trạng thái và chứa tập hợp các biến cuối cùng cần thiết để xác định hoạt ảnh chuyển tiếp. Hàm build() tạo và trả về AnimatedBuilder, lấy 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 ra một kích thước Container 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à con sẽ trông giống như nó được chỉ định hai lần. Điều này xảy ra là vì tham chiếu bên ngoài của con được truyền tới AnimatedBuilder, tham chiếu này sẽ chuyển nó tới bao đóng ẩn danh, sau đó sử dụng đối tượng đó làm con của nó. Kết quả là AnimatedBuilder được chèn vào giữa hai widget trong cây kết xuất.
class GrowTransition extends StatelessWidget { GrowTransition({this.child, this.animation}); final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center( child: AnimatedBuilder( animation: animation, builder: (context, child) => Container( height: animation.value, width: animation.value, child: child, ), child: child), ); }
Cuối cùng, đoạn code để khởi tạo hoạt ảnh trông rất giống với ví dụ animate2. Phương thức initState() tạo ra một AnimationController và một Tween, sau đó liên kết chúng với animate(). Điều kỳ diệu xảy ra trong phương thức build(), phương thức này trả về một đối tượng GrowTransition với một LogoWidget như là con và một đối tượng hoạt ảnh để thúc đẩy quá trình chuyển đổi. Đây là ba yếu tố được liệt kê trong các gạch đầu dòng ở trên.
Nguồn ứng dụng: animate4
Hoạt ảnh đồng thời
Vấn đề ở đây là:
- Lớp Curves định nghĩa một loạt các đường cong thường được sử dụng mà bạn có thể sử dụng với một CurvedAnimation.
Trong phần này, bạn sẽ xây dựng dựa trên ví dụ từ việc theo dõi tiến trình của hoạt ảnh (animate3 ), được sử dụng AnimatedWidget để tạo hoạt ảnh vào và ra liên tục. Hãy xem xét trường hợp bạn muốn tạo hoạt ảnh trong và ngoài trong khi độ mờ chuyển động từ trong suốt sang mờ đục.
Lưu ý: Ví dụ này cho thấy cách sử dụng nhiều tween trên cùng một bộ điều khiển hoạt ảnh, trong đó mỗi tween quản lý một hiệu ứng khác nhau trong hoạt ảnh. Nó chỉ dành cho mục đích minh họa. Nếu bạn đang chỉnh sửa độ mờ và kích thước trong mã sản phẩm, bạn có thể sử dụng FadeTransition và SizeTransition thay thế.
Mỗi tween quản lý một khía cạnh của hoạt ảnh. 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 có thể lấy kích thước bằng sizeAnimation.value và độ mờ bằng opacityAnimation.value, nhưng hàm tạo AnimatedWidget chỉ lấy một đối tượng Animation duy nhất. Để giải quyết vấn đề này, ví dụ này sẽ tạo các đối tượng Tween của riêng nó và tính toán các giá trị một cách rõ ràng.
Thay đổi AnimatedLogo để đóng gói các đối tượng Tween của chính nó và phương thức build() của nó gọi Tween.evaluate() trên hoạt ảnh của cha nó để tính toán các giá trị kích thước và độ mờ cần thiết. Đoạn mã sau đây hiển thị các thay đổi với các phần highlight màu vàng:
class AnimatedLogo extends AnimatedWidget { // 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); AnimatedLogo({Key key, Animation<double> animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { final animation = listenable as Animation<double>; return Center( child: Opacity( opacity: _opacityTween.evaluate(animation), child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: _sizeTween.evaluate(animation), width: _sizeTween.evaluate(animation), child: FlutterLogo(), ), ), ); } } class LogoApp extends StatefulWidget { _LogoAppState createState() => _LogoAppState(); } class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { Animation<double> animation; 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(); } }
Nguồn ứng dụng: animate5
Bước tiếp theo
Bài hướng dẫn này cung cấp cho bạn nền tảng để tạo hoạt ảnh trong Flutter bằng cách sử dụng Tweens, nhưng có nhiều lớp khác để khám phá. Bạn có thể xem thêm các lớp Tween chuyên biệt, hoạt ảnh dành riêng cho Material Design, ReverseAnimation, chuyển đổi phần tử được chia sẻ (còn được gọi là hoạt ảnh Hero), mô phỏng vật lý và và phương thức fling(). Xem trang đích hoạt ảnh để biết các tài liệu và ví dụ mới nhất hiện có.