Flutter - Hoạt ảnh

Hoạt ảnh là một thủ tục phức tạp trong bất kỳ ứng dụng di động nào. Bất chấp sự phức tạp của nó, Hoạt ảnh nâng cao trải nghiệm người dùng lên một cấp độ mới và cung cấp tương tác người dùng phong phú. Do sự phong phú của nó, hoạt ảnh trở thành một phần không thể thiếu trong ứng dụng di động hiện đại. Flutter framework công nhận tầm quan trọng của Animation và cung cấp một framework đơn giản và trực quan để phát triển tất cả các loại hoạt ảnh.

Giới thiệu

Hoạt ảnh là một quá trình hiển thị một loạt hình ảnh / bức tranh theo một thứ tự cụ thể trong một khoảng thời gian cụ thể để tạo ra ảo giác về chuyển động. Các khía cạnh quan trọng nhất của hoạt ảnh như sau:

  • Hoạt ảnh có hai giá trị riêng biệt: Giá trị bắt đầu và Giá trị kết thúc. Hoạt ảnh bắt đầu từ giá trị Bắt đầu và trải qua một loạt các giá trị trung gian và cuối cùng kết thúc ở giá trị Kết thúc. Ví dụ: để tạo hoạt ảnh cho một tiện ích con mờ đi, giá trị ban đầu sẽ là độ mờ đầy đủ và giá trị cuối cùng sẽ là độ mờ bằng không.

  • Các giá trị trung gian có thể là tuyến tính hoặc phi tuyến tính (đường cong) về bản chất và nó có thể được cấu hình. Hiểu rằng hoạt ảnh hoạt động khi nó được định cấu hình. Mỗi cấu hình cung cấp một cảm giác khác nhau cho hoạt ảnh. Ví dụ: làm mờ một tiện ích sẽ có bản chất là tuyến tính trong khi độ nảy của một quả bóng sẽ là phi tuyến tính về bản chất.

  • Thời lượng của quá trình hoạt ảnh ảnh hưởng đến tốc độ (độ chậm hoặc nhanh) của hoạt ảnh.

  • Khả năng kiểm soát quá trình hoạt ảnh như bắt đầu hoạt ảnh, dừng hoạt ảnh, lặp lại hoạt ảnh để đặt số lần, đảo ngược quá trình hoạt ảnh, v.v.,

  • Trong Flutter, hệ thống hoạt ảnh không thực hiện bất kỳ hoạt ảnh thực nào. Thay vào đó, nó chỉ cung cấp các giá trị cần thiết ở mỗi khung hình để hiển thị hình ảnh.

Lớp dựa trên hoạt ảnh

Hệ thống hoạt hình Flutter dựa trên các đối tượng Animation. Các lớp hoạt ảnh cốt lõi và cách sử dụng nó như sau:

Hoạt hình

Tạo giá trị nội suy giữa hai số trong một khoảng thời gian nhất định. Các lớp Hoạt hình phổ biến nhất là -

  • Animation<double> - nội suy các giá trị giữa hai số thập phân

  • Animation<Color> - nội suy màu sắc giữa hai màu

  • Animation<Size> - nội suy kích thước giữa hai kích thước

  • AnimationController- Đối tượng Animation đặc biệt để điều khiển hoạt ảnh của chính nó. Nó tạo ra các giá trị mới bất cứ khi nào ứng dụng sẵn sàng cho một khung mới. Nó hỗ trợ hoạt ảnh dựa trên tuyến tính và giá trị bắt đầu từ 0,0 đến 1,0

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

Ở đây, bộ điều khiển kiểm soát hoạt ảnh và tùy chọn thời lượng kiểm soát thời lượng của quá trình hoạt ảnh. vsync là một tùy chọn đặc biệt được sử dụng để tối ưu hóa tài nguyên được sử dụng trong hoạt ảnh.

CurvedAnimation

Tương tự như AnimationController nhưng hỗ trợ hoạt ảnh phi tuyến tính. CurvedAnimation có thể được sử dụng cùng với đối tượng Animation như bên dưới:

controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); 
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)

Tween <T>

Bắt nguồn từ Animatable <T> và được sử dụng để tạo các số giữa hai số bất kỳ khác 0 và 1. Nó có thể được sử dụng cùng với đối tượng Animation bằng cách sử dụng phương thức animate và chuyển đối tượng Animation thực tế.

AnimationController controller = AnimationController( 
   duration: const Duration(milliseconds: 1000), 
vsync: this); Animation<int> customTween = IntTween(
   begin: 0, end: 255).animate(controller);
  • Tween cũng có thể được sử dụng cùng với CurvedAnimation như bên dưới:

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

Ở đây, controller là bộ điều khiển hoạt ảnh thực tế. Đường cong cung cấp loại không tuyến tính và customTween cung cấp phạm vi tùy chỉnh từ 0 đến 255.

Luồng công việc của Flutter Animation

Luồng công việc của hoạt ảnh như sau:

  • Xác định và khởi động bộ điều khiển hoạt ảnh trong initState của StatefulWidget.

AnimationController(duration: const Duration(seconds: 2), vsync: this); 
animation = Tween<double>(begin: 0, end: 300).animate(controller); 
controller.forward();
  • Thêm trình nghe dựa trên hoạt ảnh, addListener để thay đổi trạng thái của tiện ích.

animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addListener(() {
   setState(() { 
      // The state that has changed here is the animation object’s value. 
   }); 
});
  • Có thể sử dụng các widget tích hợp, AnimatedWidget và AnimatedBuilder để bỏ qua quá trình này. Cả hai widget đều chấp nhận đối tượng Animation và nhận các giá trị hiện tại cần thiết cho hoạt ảnh.

  • Nhận các giá trị hoạt ảnh trong quá trình xây dựng tiện ích con và sau đó áp dụng nó cho chiều rộng, chiều cao hoặc bất kỳ thuộc tính nào có liên quan thay vì giá trị ban đầu.

child: Container( 
   height: animation.value, 
   width: animation.value, 
   child: <Widget>, 
)

Ứng dụng làm việc

Hãy để chúng tôi viết một ứng dụng dựa trên hoạt ảnh đơn giản để hiểu khái niệm về hoạt ảnh trong Flutter framework.

  • Tạo ứng dụng Flutter mới trong Android studio, product_animation_app.

  • Sao chép thư mục nội dung từ product_nav_app sang product_animation_app và thêm nội dung bên trong tệp pubspec.yaml.

flutter: 
   assets: 
   - assets/appimages/floppy.png 
   - assets/appimages/iphone.png 
   - assets/appimages/laptop.png 
   - assets/appimages/pendrive.png 
   - assets/appimages/pixel.png 
   - assets/appimages/tablet.png
  • Loại bỏ mã khởi động mặc định (main.dart).

  • Thêm nhập và chức năng chính cơ bản.

import 'package:flutter/material.dart'; 
void main() => runApp(MyApp());
  • Tạo tiện ích MyApp có nguồn gốc từ StatefulWidgtet.

class MyApp extends StatefulWidget { 
   _MyAppState createState() => _MyAppState(); 
}
  • Tạo tiện ích _MyAppState và triển khai initState và loại bỏ ngoài phương pháp xây dựng mặc định.

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin { 
   Animation<double> animation; 
   AnimationController controller; 
   @override void initState() {
      super.initState(); 
      controller = AnimationController(
         duration: const Duration(seconds: 10), vsync: this
      ); 
      animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); 
      controller.forward(); 
   } 
   // This widget is the root of your application. 
   @override 
   Widget build(BuildContext context) {
      controller.forward(); 
      return MaterialApp(
         title: 'Flutter Demo',
         theme: ThemeData(primarySwatch: Colors.blue,), 
         home: MyHomePage(title: 'Product layout demo home page', animation: animation,)
      ); 
   } 
   @override 
   void dispose() {
      controller.dispose();
      super.dispose();
   }
}

Đây,

  • Trong phương thức initState, chúng ta đã tạo một đối tượng điều khiển hoạt ảnh (controller), một đối tượng hoạt ảnh (animation) và bắt đầu hoạt ảnh bằng controller.earch.

  • Trong phương thức vứt bỏ, chúng ta đã xử lý đối tượng điều khiển hoạt ảnh (controller).

  • Trong phương thức xây dựng, gửi hoạt ảnh tới tiện ích MyHomePage thông qua hàm tạo. Giờ đây, tiện ích MyHomePage có thể sử dụng đối tượng hoạt ảnh để tạo hoạt ảnh cho nội dung của nó.

  • Bây giờ, hãy thêm tiện ích ProductBox

class ProductBox extends StatelessWidget {
   ProductBox({Key key, this.name, this.description, this.price, this.image})
      : super(key: key);
   final String name; 
   final String description; 
   final int price; 
   final String image; 
   
   Widget build(BuildContext context) {
      return Container(
         padding: EdgeInsets.all(2), 
         height: 140, 
         child: Card( 
            child: Row( 
               mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
               children: <Widget>[ 
                  Image.asset("assets/appimages/" + image), 
                  Expanded( 
                     child: Container( 
                        padding: EdgeInsets.all(5), 
                        child: Column( 
                           mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
                           children: <Widget>[ 
                              Text(this.name, style: 
                                 TextStyle(fontWeight: FontWeight.bold)), 
                              Text(this.description), 
                                 Text("Price: " + this.price.toString()), 
                           ], 
                        )
                     )
                  )
               ]
            )
         )
      ); 
   }
}
  • Tạo một widget mới, MyAnimatedWidget để tạo hoạt ảnh mờ dần đơn giản bằng cách sử dụng độ mờ.

class MyAnimatedWidget extends StatelessWidget { 
   MyAnimatedWidget({this.child, this.animation}); 
      
   final Widget child; 
   final Animation<double> animation; 
   
   Widget build(BuildContext context) => Center( 
   child: AnimatedBuilder(
      animation: animation, 
      builder: (context, child) => Container( 
         child: Opacity(opacity: animation.value, child: child), 
      ), 
      child: child), 
   ); 
}
  • Ở đây, chúng tôi đã sử dụng AniatedBuilder để làm hoạt ảnh của chúng tôi. AnimatedBuilder là một widget xây dựng nội dung của nó trong khi thực hiện hoạt ảnh cùng một lúc. Nó chấp nhận một đối tượng hoạt ảnh để nhận giá trị hoạt ảnh hiện tại. Chúng tôi đã sử dụng giá trị hoạt ảnh, animation.value để đặt độ mờ của tiện ích con. Trên thực tế, tiện ích con sẽ tạo hoạt ảnh cho tiện ích con bằng cách sử dụng khái niệm độ mờ.

  • Cuối cùng, tạo tiện ích MyHomePage và sử dụng đối tượng hoạt ảnh để tạo hoạt ảnh cho bất kỳ nội dung nào của nó.

class MyHomePage extends StatelessWidget {
   MyHomePage({Key key, this.title, this.animation}) : super(key: key); 
   
   final String title; 
   final Animation<double> 
   animation; 
   
   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(title: Text("Product Listing")),body: ListView(
            shrinkWrap: true,
            padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), 
            children: <Widget>[
               FadeTransition(
                  child: ProductBox(
                     name: "iPhone", 
                     description: "iPhone is the stylist phone ever", 
                     price: 1000, 
                     image: "iphone.png"
                  ), opacity: animation
               ), 
               MyAnimatedWidget(child: ProductBox(
                  name: "Pixel", 
                  description: "Pixel is the most featureful phone ever", 
                  price: 800, 
                  image: "pixel.png"
               ), animation: animation), 
               ProductBox(
                  name: "Laptop", 
                  description: "Laptop is most productive development tool", 
                  price: 2000, 
                  image: "laptop.png"
               ), 
               ProductBox(
                  name: "Tablet", 
                  description: "Tablet is the most useful device ever for meeting", 
                  price: 1500, 
                  image: "tablet.png"
               ), 
               ProductBox(
                  name: "Pendrive", 
                  description: "Pendrive is useful storage medium", 
                  price: 100, 
                  image: "pendrive.png"
               ),
               ProductBox(
                  name: "Floppy Drive", 
                  description: "Floppy drive is useful rescue storage medium", 
                  price: 20, 
                  image: "floppy.png"
               ),
            ],
         )
      );
   }
}

Ở đây, chúng tôi đã sử dụng FadeAnimation và MyAnimationWidget để tạo hoạt ảnh cho hai mục đầu tiên trong danh sách. FadeAnimation là một lớp hoạt ảnh tích hợp, chúng tôi đã sử dụng để tạo hoạt ảnh con của nó bằng cách sử dụng khái niệm độ mờ.

  • Mã hoàn chỉnh như sau:

import 'package:flutter/material.dart'; 
void main() => runApp(MyApp()); 

class MyApp extends StatefulWidget { 
   _MyAppState createState() => _MyAppState(); 
} 
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
   Animation<double> animation; 
   AnimationController controller; 
   
   @override 
   void initState() {
      super.initState(); 
      controller = AnimationController(
         duration: const Duration(seconds: 10), vsync: this); 
      animation = Tween<double>(begin: 0.0, end: 1.0).animate(controller); 
      controller.forward(); 
   } 
   // This widget is the root of your application. 
   @override 
   Widget build(BuildContext context) {
      controller.forward(); 
      return MaterialApp( 
         title: 'Flutter Demo', theme: ThemeData(primarySwatch: Colors.blue,), 
         home: MyHomePage(title: 'Product layout demo home page', animation: animation,) 
      ); 
   } 
   @override 
   void dispose() {
      controller.dispose();
      super.dispose(); 
   } 
}
class MyHomePage extends StatelessWidget { 
   MyHomePage({Key key, this.title, this.animation}): super(key: key);
   final String title; 
   final Animation<double> animation; 
   
   @override 
   Widget build(BuildContext context) {
      return Scaffold(
         appBar: AppBar(title: Text("Product Listing")), 
         body: ListView(
            shrinkWrap: true, 
            padding: const EdgeInsets.fromLTRB(2.0, 10.0, 2.0, 10.0), 
            children: <Widget>[
               FadeTransition(
                  child: ProductBox(
                     name: "iPhone", 
                     description: "iPhone is the stylist phone ever", 
                     price: 1000, 
                     image: "iphone.png"
                  ), 
                  opacity: animation
               ), 
               MyAnimatedWidget(
                  child: ProductBox( 
                     name: "Pixel", 
                     description: "Pixel is the most featureful phone ever", 
                     price: 800, 
                     image: "pixel.png"
                  ), 
                  animation: animation
               ), 
               ProductBox( 
                  name: "Laptop", 
                  description: "Laptop is most productive development tool", 
                  price: 2000, 
                  image: "laptop.png"
               ), 
               ProductBox(
                  name: "Tablet",
                  description: "Tablet is the most useful device ever for meeting",
                  price: 1500, 
                  image: "tablet.png"
               ), 
               ProductBox(
                  name: "Pendrive", 
                  description: "Pendrive is useful storage medium", 
                  price: 100, 
                  image: "pendrive.png"
               ), 
               ProductBox(
                  name: "Floppy Drive", 
                  description: "Floppy drive is useful rescue storage medium", 
                  price: 20, 
                  image: "floppy.png"
               ), 
            ], 
         )
      ); 
   } 
} 
class ProductBox extends StatelessWidget { 
   ProductBox({Key key, this.name, this.description, this.price, this.image}) :
      super(key: key);
   final String name; 
   final String description; 
   final int price; 
   final String image; 
   Widget build(BuildContext context) {
      return Container(
         padding: EdgeInsets.all(2), 
         height: 140, 
         child: Card(
            child: Row(
               mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
               children: <Widget>[ 
                  Image.asset("assets/appimages/" + image), 
                  Expanded(
                     child: Container( 
                        padding: EdgeInsets.all(5), 
                        child: Column( 
                           mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
                           children: <Widget>[ 
                              Text(
                                 this.name, style: TextStyle(
                                    fontWeight: FontWeight.bold
                                 )
                              ), 
                              Text(this.description), Text(
                                 "Price: " + this.price.toString()
                              ), 
                           ], 
                        )
                     )
                  ) 
               ]
            )
         )
      ); 
   } 
}
class MyAnimatedWidget extends StatelessWidget { 
   MyAnimatedWidget({this.child, this.animation}); 
   final Widget child; 
   final Animation<double> animation; 
 
   Widget build(BuildContext context) => Center( 
      child: AnimatedBuilder(
         animation: animation, 
         builder: (context, child) => Container( 
            child: Opacity(opacity: animation.value, child: child), 
         ), 
         child: child
      ), 
   ); 
}
  • Biên dịch và chạy ứng dụng để xem kết quả. Phiên bản đầu tiên và phiên bản cuối cùng của ứng dụng như sau: