Ảnh động trong Flutter

1. Giới thiệu

Ảnh động là một cách tuyệt vời để cải thiện trải nghiệm người dùng của ứng dụng, truyền đạt thông tin quan trọng cho người dùng và giúp ứng dụng của bạn trở nên tinh tế và thú vị hơn khi sử dụng.

Tổng quan về khung ảnh động của Flutter

Flutter hiển thị hiệu ứng ảnh động bằng cách tạo lại một phần của cây tiện ích trên mỗi khung hình. Thư viện này cung cấp các hiệu ứng ảnh động tạo sẵn và các API khác để giúp việc tạo và soạn ảnh động trở nên dễ dàng hơn.

  • Ảnh động ngầm ẩn là các hiệu ứng ảnh động được tạo sẵn tự động chạy toàn bộ ảnh động. Khi giá trị mục tiêu của ảnh động thay đổi, ảnh động sẽ chạy từ giá trị hiện tại đến giá trị mục tiêu và hiển thị từng giá trị ở giữa để tiện ích tạo ảnh động một cách mượt mà. Ví dụ về ảnh động ngầm bao gồm AnimatedSize, AnimatedScaleAnimatedPositioned.
  • Ảnh động phản cảm cũng là hiệu ứng ảnh động tạo sẵn, nhưng cần có đối tượng Animation để hoạt động. Ví dụ: SizeTransition, ScaleTransition hoặc PositionedTransition.
  • Ảnh động là một lớp đại diện cho ảnh động đang chạy hoặc đã dừng và bao gồm một giá trị đại diện cho giá trị mục tiêu mà ảnh động đang chạy đến và trạng thái đại diện cho giá trị hiện tại mà ảnh động đang hiển thị trên màn hình tại bất kỳ thời điểm nào. Đây là lớp con của Listenable và thông báo cho trình nghe khi trạng thái thay đổi trong khi ảnh động đang chạy.
  • AnimationController là một cách để tạo Ảnh động và kiểm soát trạng thái của ảnh động đó. Bạn có thể sử dụng các phương thức của lớp này như forward(), reset(), stop()repeat() để kiểm soát ảnh động mà không cần xác định hiệu ứng ảnh động đang hiển thị, chẳng hạn như tỷ lệ, kích thước hoặc vị trí.
  • Tween được dùng để nội suy các giá trị giữa giá trị bắt đầu và giá trị kết thúc, đồng thời có thể biểu thị bất kỳ loại nào, chẳng hạn như một số thực, Offset hoặc Color.
  • Đường cong được dùng để điều chỉnh tốc độ thay đổi của một tham số theo thời gian. Khi một ảnh động chạy, thường thì bạn sẽ áp dụng đường cong giảm tốc để tăng hoặc giảm tốc độ thay đổi ở đầu hoặc cuối ảnh động. Đường cong lấy một giá trị đầu vào từ 0 đến 1 và trả về một giá trị đầu ra từ 0 đến 1.

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ xây dựng một trò chơi đố vui có nhiều hiệu ứng và kỹ thuật ảnh động.

3026390ad413769c.gif

Bạn sẽ tìm hiểu cách...

  • Tạo một tiện ích tạo ảnh động cho kích thước và màu sắc
  • Tạo hiệu ứng lật thẻ 3D
  • Sử dụng các hiệu ứng ảnh động tạo sẵn bắt mắt trong gói ảnh động
  • Thêm tính năng hỗ trợ cử chỉ xem trước thao tác quay lại trên phiên bản Android mới nhất

Kiến thức bạn sẽ học được

Trong lớp học lập trình này, bạn sẽ tìm hiểu:

  • Cách sử dụng các hiệu ứng ảnh động ngầm ẩn để tạo ra ảnh động trông tuyệt vời mà không cần nhiều mã.
  • Cách sử dụng các hiệu ứng ảnh động rõ ràng để định cấu hình hiệu ứng của riêng bạn bằng các tiện ích ảnh động tạo sẵn như AnimatedSwitcher hoặc AnimationController.
  • Cách sử dụng AnimationController để xác định tiện ích của riêng bạn hiển thị hiệu ứng 3D.
  • Cách sử dụng gói animations để hiển thị các hiệu ứng ảnh động bắt mắt với mức thiết lập tối thiểu.

Bạn cần có

  • SDK Flutter
  • Một IDE, chẳng hạn như VSCode hoặc Android Studio / IntelliJ

2. Thiết lập môi trường phát triển Flutter

Bạn cần có hai phần mềm để hoàn thành lớp học này — SDK Fluttermột trình chỉnh sửa.

Bạn có thể chạy lớp học lập trình này bằng bất kỳ thiết bị nào sau đây:

  • Thiết bị Android thực (nên triển khai tính năng xem trước thao tác quay lại ở bước 7) hoặc thiết bị iOS được kết nối với máy tính và đặt thành Chế độ nhà phát triển.
  • Trình mô phỏng iOS (yêu cầu cài đặt các công cụ Xcode).
  • Trình mô phỏng Android (yêu cầu thiết lập trong Android Studio).
  • Trình duyệt (bạn cần có Chrome để gỡ lỗi).
  • Máy tính để bàn chạy Windows, Linux hoặc macOS. Bạn phải phát triển trên nền tảng mà bạn dự định triển khai. Vì vậy, nếu muốn phát triển ứng dụng dành cho máy tính Windows, bạn phải phát triển trên Windows để truy cập vào chuỗi xây dựng thích hợp. Có các yêu cầu dành riêng cho hệ điều hành được đề cập chi tiết trên docs.flutter.dev/desktop.

Xác minh quá trình cài đặt

Để xác minh rằng SDK Flutter của bạn được định cấu hình chính xác và bạn đã cài đặt ít nhất một trong các nền tảng mục tiêu ở trên, hãy sử dụng công cụ 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. Chạy ứng dụng khởi động

Tải ứng dụng ban đầu xuống

Sử dụng git để sao chép ứng dụng khởi động từ kho lưu trữ flutter/samples trên GitHub.

$ git clone https://github.com/flutter/codelabs.git
$ cd codelabs/animations/step_01/

Ngoài ra, bạn có thể tải mã nguồn xuống dưới dạng tệp .zip.

Chạy ứng dụng

Để chạy ứng dụng, hãy sử dụng lệnh flutter run và chỉ định một thiết bị mục tiêu, chẳng hạn như android, ios hoặc chrome. Để xem danh sách đầy đủ các nền tảng được hỗ trợ, hãy xem trang Nền tảng được hỗ trợ.

$ flutter run -d android

Bạn cũng có thể chạy và gỡ lỗi ứng dụng bằng IDE mà bạn chọn. Hãy xem tài liệu chính thức về Flutter để biết thêm thông tin.

Tham quan mã

Ứng dụng khởi động là một trò chơi đố vui có nhiều lựa chọn, bao gồm hai màn hình tuân theo mẫu thiết kế mô hình-thành phần hiển thị-thành phần hiển thị-mô hình (model-view-view-model) hoặc MVVM. QuestionScreen (Thành phần hiển thị) sử dụng lớp QuizViewModel (Mô hình thành phần hiển thị) để đặt câu hỏi trắc nghiệm cho người dùng từ lớp QuestionBank (Mô hình).

  • home_screen.dart – Hiển thị màn hình có nút New Game (Trò chơi mới)
  • main.dart – Định cấu hình MaterialApp để sử dụng Material 3 và hiển thị màn hình chính
  • model.dart – Xác định các lớp cốt lõi được sử dụng trong toàn bộ ứng dụng
  • question_screen.dart – Hiển thị giao diện người dùng cho trò chơi đố vui
  • view_model.dart – Lưu trữ trạng thái và logic cho trò chơi đố vui, do QuestionScreen hiển thị

fbb1e1f7b6c91e21.png

Ứng dụng chưa hỗ trợ hiệu ứng ảnh động nào, ngoại trừ hiệu ứng chuyển đổi chế độ xem mặc định do lớp Navigator của Flutter hiển thị khi người dùng nhấn nút New Game (Trò chơi mới).

4. Sử dụng hiệu ứng ảnh động ngầm ẩn

Ảnh động ngầm ẩn là một lựa chọn tuyệt vời trong nhiều trường hợp vì không yêu cầu bất kỳ cấu hình đặc biệt nào. Trong phần này, bạn sẽ cập nhật tiện ích StatusBar để hiển thị một bảng điểm động. Để tìm các hiệu ứng ảnh động ngầm ẩn phổ biến, hãy duyệt xem tài liệu về API ImplicitlyAnimatedWidget.

206dd8d9c1fae95.gif

Tạo tiện ích bảng điểm không có ảnh động

Tạo một tệp mới, lib/scoreboard.dart bằng mã sau:

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,
            )
        ],
      ),
    );
  }
}

Sau đó, hãy thêm tiện ích Scoreboard trong các tiện ích con của tiện ích StatusBar, thay thế các tiện ích Text trước đó hiển thị điểm số và tổng số câu hỏi. Trình chỉnh sửa sẽ tự động thêm import "scoreboard.dart" bắt buộc ở đầu tệp.

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
            ),
          ],
        ),
      ),
    );
  }
}

Tiện ích này hiển thị biểu tượng ngôi sao cho từng câu hỏi. Khi bạn trả lời đúng một câu hỏi, một ngôi sao khác sẽ sáng lên ngay lập tức mà không có bất kỳ ảnh động nào. Trong các bước sau, bạn sẽ thông báo cho người dùng về việc điểm số của họ đã thay đổi bằng cách tạo ảnh động cho kích thước và màu sắc của điểm số.

Sử dụng hiệu ứng ảnh động ngầm ẩn

Tạo một tiện ích mới có tên là AnimatedStar. Tiện ích này sử dụng tiện ích AnimatedScale để thay đổi số lượng scale từ 0.5 thành 1.0 khi ngôi sao trở nên hoạt động:

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.

Giờ đây, khi người dùng trả lời đúng câu hỏi, tiện ích AnimatedStar sẽ cập nhật kích thước bằng ảnh động ngầm ẩn. color của Icon không được tạo ảnh động ở đây, chỉ có scale được thực hiện bằng tiện ích AnimatedScale.

84aec4776e70b870.gif

Sử dụng Tween để nội suy giữa hai giá trị

Lưu ý rằng màu của tiện ích AnimatedStar sẽ thay đổi ngay sau khi trường isActive thay đổi thành true.

Để đạt được hiệu ứng màu động, bạn có thể thử sử dụng tiện ích AnimatedContainer (là một lớp con khác của ImplicitlyAnimatedWidget), vì tiện ích này có thể tự động tạo ảnh động cho tất cả các thuộc tính, bao gồm cả màu sắc. Rất tiếc, tiện ích của chúng ta cần hiển thị một biểu tượng, chứ không phải một vùng chứa.

Bạn cũng có thể thử AnimatedIcon để triển khai hiệu ứng chuyển đổi giữa các hình dạng của biểu tượng. Tuy nhiên, không có cách triển khai mặc định cho biểu tượng dấu sao trong lớp AnimatedIcons.

Thay vào đó, chúng ta sẽ sử dụng một lớp con khác của ImplicitlyAnimatedWidget có tên là TweenAnimationBuilder. Lớp con này lấy Tween làm tham số. Tween là một lớp lấy hai giá trị (beginend) và tính toán các giá trị ở giữa để ảnh động có thể hiển thị các giá trị đó. Trong ví dụ này, chúng ta sẽ sử dụng ColorTween, đáp ứng giao diện Tween<Color> cần thiết để tạo hiệu ứng ảnh động.

Chọn tiện ích Icon và sử dụng thao tác nhanh "Wrap with Builder" (Gói bằng Trình tạo) trong IDE, thay đổi tên thành TweenAnimationBuilder. Sau đó, hãy cung cấp thời lượng và 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.
      ),
    );
  }
}

Bây giờ, hãy tải lại nóng ứng dụng để xem ảnh động mới.

8b0911f4af299a60.gif

Lưu ý rằng giá trị end của ColorTween thay đổi dựa trên giá trị của tham số isActive. Nguyên nhân là do TweenAnimationBuilder chạy lại ảnh động bất cứ khi nào giá trị Tween.end thay đổi. Khi điều này xảy ra, ảnh động mới sẽ chạy từ giá trị ảnh động hiện tại đến giá trị kết thúc mới, cho phép bạn thay đổi màu sắc bất cứ lúc nào (ngay cả khi ảnh động đang chạy) và hiển thị hiệu ứng ảnh động mượt mà với các giá trị trung gian chính xác.

Áp dụng đường cong

Cả hai hiệu ứng ảnh động này đều chạy ở tốc độ không đổi, nhưng ảnh động thường thú vị và cung cấp nhiều thông tin hơn về mặt hình ảnh khi tăng tốc hoặc giảm tốc.

Curve áp dụng hàm làm dịu, xác định tốc độ thay đổi của một tham số theo thời gian. Flutter đi kèm với một bộ sưu tập các đường cong chuyển động được tạo sẵn trong lớp Curves, chẳng hạn như easeIn hoặc easeOut.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

Các sơ đồ này (có trên trang tài liệu về API Curves) cho biết cách hoạt động của các đường cong. Đường cong chuyển đổi giá trị đầu vào từ 0 đến 1 (hiển thị trên trục x) thành giá trị đầu ra từ 0 đến 1 (hiển thị trên trục y). Các sơ đồ này cũng cho thấy bản xem trước của nhiều hiệu ứng ảnh động khi sử dụng một đường cong làm dịu.

Tạo một trường mới trong AnimatedStar có tên là _curve và truyền trường đó dưới dạng tham số đến các tiện ích AnimatedScaleTweenAnimationBuilder.

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,
          );
        },
      ),
    );
  }
}

Trong ví dụ này, đường cong elasticOut tạo ra hiệu ứng mùa xuân phóng đại bắt đầu bằng chuyển động mùa xuân và cân bằng về cuối.

8f84142bff312373.gif

Tải lại nóng ứng dụng để xem đường cong này được áp dụng cho AnimatedSizeTweenAnimationBuilder.

206dd8d9c1fae95.gif

Sử dụng Công cụ cho nhà phát triển để bật ảnh động chậm

Để gỡ lỗi bất kỳ hiệu ứng ảnh động nào, Flutter DevTools cung cấp một cách để làm chậm tất cả ảnh động trong ứng dụng, nhờ đó bạn có thể xem ảnh động rõ ràng hơn.

Để mở DevTools, hãy đảm bảo ứng dụng đang chạy ở chế độ gỡ lỗi và mở Widget Inspector (Trình kiểm tra tiện ích) bằng cách chọn tiện ích này trong Debug toolbar (Thanh công cụ gỡ lỗi) trong VSCode hoặc bằng cách chọn nút Open Flutter DevTools (Mở DevTools Flutter) trong Debug tool window (Cửa sổ công cụ gỡ lỗi) trong IntelliJ / Android Studio.

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

Sau khi trình kiểm tra tiện ích mở ra, hãy nhấp vào nút Slow animations (Ảnh động chậm) trong thanh công cụ.

adea0a16d01127ad.png

5. Sử dụng hiệu ứng ảnh động phản cảm

Giống như ảnh động ngầm ẩn, ảnh động rõ ràng là các hiệu ứng ảnh động được tạo sẵn, nhưng thay vì lấy giá trị mục tiêu, ảnh động rõ ràng sẽ lấy đối tượng Animation làm tham số. Điều này giúp các lớp này hữu ích trong những trường hợp ảnh động đã được xác định bằng một hiệu ứng chuyển đổi điều hướng, AnimatedSwitcher hoặc AnimationController, chẳng hạn.

Sử dụng hiệu ứng ảnh động phản cảm

Để bắt đầu với hiệu ứng ảnh động rõ ràng, hãy gói tiện ích Card bằng 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 sử dụng hiệu ứng chuyển màu theo mặc định, nhưng bạn có thể ghi đè hiệu ứng này bằng tham số transitionBuilder. Trình tạo chuyển đổi cung cấp tiện ích con đã được truyền đến AnimatedSwitcher và một đối tượng Animation. Đây là cơ hội tuyệt vời để sử dụng ảnh động rõ ràng.

Đối với lớp học lập trình này, ảnh động rõ ràng đầu tiên mà chúng ta sẽ sử dụng là SlideTransition. Ảnh động này sẽ lấy Animation<Offset> xác định độ dời bắt đầu và kết thúc mà các tiện ích đến và đi sẽ di chuyển giữa.

Tween có một hàm trợ giúp, animate(), chuyển đổi mọi Animation thành một Animation khác khi áp dụng tween. Điều này có nghĩa là bạn có thể sử dụng Tween<Offset> để chuyển đổi Animation<double> do AnimatedSwitcher cung cấp thành Animation<Offset>, để cung cấp cho tiện ích 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,
          ),
        ),
      ),
    );
  }
}

Xin lưu ý rằng mã này sử dụng Tween.animate để áp dụng Curve cho Animation, sau đó chuyển đổi Tween<double> từ 0 đến 1 thành Tween<Offset> chuyển đổi từ -0, 1 đến 0 trên trục x.

Ngoài ra, lớp Ảnh động có một hàm drive() nhận bất kỳ Tween (hoặc Animatable) nào và chuyển đổi thành một Animation mới. Điều này cho phép "chuỗi" các tween, giúp mã thu được trở nên súc tích hơn:

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);
},

Một lợi thế khác của việc sử dụng ảnh động rõ ràng là bạn có thể dễ dàng kết hợp các ảnh động đó với nhau. Thêm một ảnh động rõ ràng khác, FadeTransition sử dụng cùng một ảnh động cong bằng cách gói tiện ích 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
  },

Tuỳ chỉnh layoutBuilder

Bạn có thể nhận thấy một vấn đề nhỏ với AnimationSwitcher. Khi chuyển sang một câu hỏi mới, QuestionCard sẽ hiển thị câu hỏi đó ở giữa không gian có sẵn trong khi ảnh động đang chạy, nhưng khi ảnh động dừng, tiện ích sẽ chuyển sang đầu màn hình. Điều này gây ra ảnh động giật vì vị trí cuối cùng của thẻ câu hỏi không khớp với vị trí trong khi ảnh động đang chạy.

d77de181bdde58f7.gif

Để khắc phục vấn đề này, AnimatedSwitcher cũng có một tham số layoutBuilder có thể dùng để xác định bố cục. Sử dụng hàm này để định cấu hình trình tạo bố cục nhằm căn chỉnh thẻ vào đầu màn hình:

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,
        ],
      );
    },

Mã này là phiên bản sửa đổi của defaultLayoutBuilder từ lớp AnimatedSwitcher, nhưng sử dụng Alignment.topCenter thay vì Alignment.center.

Tóm tắt

  • Ảnh động rõ ràng là các hiệu ứng ảnh động lấy đối tượng Ảnh động (tương phản với ImplicitlyAnimatedWidgets, lấy giá trị mục tiêu và thời lượng)
  • Lớp Ảnh động đại diện cho ảnh động đang chạy, nhưng không xác định hiệu ứng cụ thể.
  • Sử dụng Tween().animate hoặc Animation.drive() để áp dụng Tween và Curve (sử dụng CurveTween) cho ảnh động.
  • Sử dụng thông số layoutBuilder của AnimatedSwitcher để điều chỉnh cách bố trí các thành phần con.

6. Kiểm soát trạng thái của ảnh động

Cho đến nay, mọi ảnh động đều được khung chạy tự động. Ảnh động ngầm ẩn chạy tự động và hiệu ứng ảnh động rõ ràng yêu cầu Ảnh động hoạt động đúng cách. Trong phần này, bạn sẽ tìm hiểu cách tạo đối tượng Ảnh động của riêng mình bằng AnimationController và sử dụng TweenSequence để kết hợp các Tween với nhau.

Chạy ảnh động bằng AnimationController

Để tạo ảnh động bằng AnimationController, bạn cần làm theo các bước sau:

  1. Tạo StatefulWidget
  2. Sử dụng mixin SingleTickerProviderStateMixin trong lớp Trạng thái để cung cấp một Ticker cho AnimationController
  3. Khởi động AnimationController trong phương thức vòng đời initState, cung cấp đối tượng Trạng thái hiện tại cho tham số vsync (TickerProvider).
  4. Đảm bảo rằng tiện ích của bạn sẽ tạo lại bất cứ khi nào AnimationController thông báo cho trình nghe, bằng cách sử dụng AnimatedBuilder hoặc bằng cách gọi listen() và setState theo cách thủ công.

Tạo một tệp mới, flip_effect.dart và sao chép-dán mã sau:

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,
    );
  }
}

Lớp này thiết lập một AnimationController và chạy lại ảnh động bất cứ khi nào khung gọi didUpdateWidget để thông báo rằng cấu hình tiện ích đã thay đổi và có thể có một tiện ích con mới.

AnimatedBuilder đảm bảo rằng cây tiện ích được tạo lại bất cứ khi nào AnimationController thông báo cho trình nghe và tiện ích Transform được dùng để áp dụng hiệu ứng xoay 3D nhằm mô phỏng một thẻ đang được lật.

Để sử dụng tiện ích này, hãy gói từng thẻ câu trả lời bằng tiện ích CardFlipEffect. Hãy nhớ cung cấp key cho tiện ích Thẻ:

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
      );
    }),
  );
}

Bây giờ, hãy tải lại nóng ứng dụng để xem các thẻ câu trả lời lật qua lại bằng tiện ích CardFlipEffect.

5455def725b866f6.gif

Bạn có thể nhận thấy lớp này trông giống như một hiệu ứng ảnh động rõ ràng. Trên thực tế, bạn nên mở rộng trực tiếp lớp AnimatedWidget để triển khai phiên bản của riêng mình. Rất tiếc, vì lớp này cần lưu trữ tiện ích trước đó trong Trạng thái, nên lớp này cần sử dụng StatefulWidget. Để tìm hiểu thêm về cách tạo hiệu ứng ảnh động rõ ràng của riêng bạn, hãy xem tài liệu về API cho AnimatedWidget.

Thêm độ trễ bằng TweenSequence

Trong phần này, bạn sẽ thêm độ trễ vào tiện ích CardFlipEffect để mỗi thẻ lật lần lượt một thẻ. Để bắt đầu, hãy thêm một trường mới có tên là 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();
}

Sau đó, thêm delayAmount vào phương thức tạo 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]),

Sau đó, trong _CardFlipEffectState, hãy tạo một Ảnh động mới áp dụng độ trễ bằng TweenSequence. Xin lưu ý rằng phương thức này không sử dụng bất kỳ tiện ích nào trong thư viện dart:async, chẳng hạn như Future.delayed. Điều này là do độ trễ là một phần của ảnh động và không phải là điều mà tiện ích kiểm soát rõ ràng khi sử dụng AnimationController. Điều này giúp bạn dễ dàng gỡ lỗi hiệu ứng ảnh động khi bật ảnh động chậm trong DevTools, vì hiệu ứng này sử dụng cùng một TickerProvider.

Để sử dụng TweenSequence, hãy tạo hai TweenSequenceItem, một chứa ConstantTween giữ ảnh động ở 0 trong một khoảng thời gian tương đối và một Tween thông thường đi từ 0.0 đến 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
  }

Cuối cùng, hãy thay thế ảnh động của AnimationController bằng ảnh động bị trì hoãn mới trong phương thức bản dựng.

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,
  );
}

Bây giờ, hãy tải lại nóng ứng dụng và xem các thẻ lật lần lượt. Để thử thách bản thân, hãy thử thay đổi phối cảnh của hiệu ứng 3D do tiện ích Transform cung cấp.

28b5291de9b3f55f.gif

7. Sử dụng hiệu ứng chuyển đổi điều hướng tuỳ chỉnh

Cho đến nay, chúng ta đã xem cách tuỳ chỉnh hiệu ứng trên một màn hình, nhưng có một cách khác để sử dụng ảnh động là sử dụng ảnh động để chuyển đổi giữa các màn hình. Trong phần này, bạn sẽ tìm hiểu cách áp dụng hiệu ứng ảnh động cho các hiệu ứng chuyển đổi màn hình bằng cách sử dụng hiệu ứng ảnh động tích hợp sẵn và hiệu ứng ảnh động tạo sẵn bắt mắt do gói ảnh động chính thức cung cấp trên pub.dev

Tạo hiệu ứng động cho quá trình chuyển đổi điều hướng

Lớp PageRouteBuilder là một Route cho phép bạn tuỳ chỉnh ảnh động chuyển đổi. Phương thức này cho phép bạn ghi đè lệnh gọi lại transitionBuilder, cung cấp hai đối tượng Ảnh động, đại diện cho ảnh động đến và đi do Trình điều hướng chạy.

Để tuỳ chỉnh ảnh động chuyển đổi, hãy thay thế MaterialPageRoute bằng PageRouteBuilder và để tuỳ chỉnh ảnh động chuyển đổi khi người dùng di chuyển từ HomeScreen đến QuestionScreen. Sử dụng FadeTransition (một tiện ích động rõ ràng) để làm cho màn hình mới mờ dần trên màn hình trước.

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'),
),

Gói ảnh động cung cấp các hiệu ứng ảnh động tạo sẵn bắt mắt, chẳng hạn như FadeThroughTransition. Nhập gói ảnh động và thay thế FadeTransition bằng tiện ích 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'),
),

Tuỳ chỉnh ảnh động xem trước thao tác quay lại

1c0558ffa3b76439.gif

Xem trước thao tác quay lại là một tính năng mới của Android cho phép người dùng xem trước tuyến hoặc ứng dụng hiện tại để biết nội dung đằng sau trước khi điều hướng. Ảnh động xem trước được điều khiển bởi vị trí ngón tay của người dùng khi họ kéo ngược trên màn hình.

Flutter hỗ trợ tính năng xem trước thao tác quay lại của hệ thống bằng cách bật tính năng này ở cấp hệ thống khi Flutter không có tuyến đường nào để bật lên trên ngăn xếp điều hướng, hay nói cách khác, khi thao tác quay lại sẽ thoát khỏi ứng dụng. Ảnh động này do hệ thống xử lý chứ không phải do chính Flutter xử lý.

Flutter cũng hỗ trợ tính năng xem trước thao tác quay lại khi di chuyển giữa các tuyến trong ứng dụng Flutter. Một PageTransitionsBuilder đặc biệt có tên là PredictiveBackPageTransitionsBuilder sẽ theo dõi các cử chỉ xem trước thao tác quay lại của hệ thống và điều khiển quá trình chuyển đổi trang bằng tiến trình của cử chỉ.

Tính năng xem trước thao tác quay lại chỉ được hỗ trợ trong Android U trở lên, nhưng Flutter sẽ quay lại hành vi cử chỉ quay lại ban đầu và ZoomPageTransitionBuilder một cách linh hoạt. Hãy xem bài đăng trên blog của chúng tôi để biết thêm thông tin, bao gồm cả phần về cách thiết lập tính năng này trong ứng dụng của riêng bạn.

Trong cấu hình ThemeData cho ứng dụng, hãy định cấu hình PageTransitionsTheme để sử dụng tính năng PredictiveBack trên Android và hiệu ứng chuyển đổi mờ từ gói ảnh động trên các nền tảng khác:

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(),
    );
  }
}

Bây giờ, bạn có thể thay đổi lệnh gọi Navigator.push() trở lại 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'),
),

Sử dụng FadeThroughTransition để thay đổi câu hỏi hiện tại

Tiện ích AnimatedSwitcher chỉ cung cấp một Ảnh động trong lệnh gọi lại trình tạo. Để giải quyết vấn đề này, gói animations cung cấp 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,
          ),
        ),
      ),
    );
  }
}

Sử dụng OpenContainer

77358e5776eb104c.png

Tiện ích OpenContainer trong gói animations cung cấp hiệu ứng ảnh động biến đổi vùng chứa mở rộng để tạo kết nối trực quan giữa hai tiện ích.

Tiện ích do closedBuilder trả về sẽ hiển thị ban đầu và mở rộng thành tiện ích do openBuilder trả về khi nhấn vào vùng chứa hoặc khi lệnh gọi lại openContainer được gọi.

Để kết nối lệnh gọi lại openContainer với mô hình khung hiển thị, hãy thêm một lệnh truyền mới vào viewModel vào tiện ích QuestionCard và lưu trữ lệnh gọi lại sẽ được dùng để hiển thị màn hình "Game Over" (Kết thúc trò chơi):

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
}

Thêm một tiện ích mới, 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);
              },
            ),
          ],
        ),
      ),
    );
  }
}

Trong tiện ích QuestionCard, hãy thay thế Thẻ bằng tiện ích OpenContainer từ gói ảnh động, thêm hai trường mới cho viewModel và lệnh gọi lại vùng chứa mở:

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
      ),
    );
  }
}

4120f9395857d218.gif

8. Xin chúc mừng

Xin chúc mừng! Bạn đã thêm thành công hiệu ứng ảnh động vào ứng dụng Flutter và tìm hiểu về các thành phần cốt lõi của hệ thống ảnh động Flutter. Cụ thể, bạn đã tìm hiểu:

  • Cách sử dụng ImplicitlyAnimatedWidget
  • Cách sử dụng ExplicitlyAnimatedWidget
  • Cách áp dụng Đường cong và Tween cho ảnh động
  • Cách sử dụng các tiện ích chuyển đổi tạo sẵn như AnimatedSwitcher hoặc PageRouteBuilder
  • Cách sử dụng các hiệu ứng ảnh động tạo sẵn bắt mắt từ gói animations, chẳng hạn như FadeThroughTransition và OpenContainer
  • Cách tuỳ chỉnh ảnh động chuyển đổi mặc định, bao gồm cả việc thêm tính năng hỗ trợ cho tính năng Xem trước thao tác quay lại trên Android.

3026390ad413769c.gif

Tiếp theo là gì?

Hãy tham khảo một số lớp học lập trình sau:

Hoặc tải ứng dụng mẫu ảnh động xuống để xem các kỹ thuật ảnh động

Tài liệu đọc thêm

Bạn có thể tìm thấy thêm tài nguyên ảnh động trên flutter.dev:

Hoặc xem các bài viết sau trên Medium:

Tài liệu tham khảo