Ả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 trong ứ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ế hơn cũng như mang lại trải nghiệm 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 được tạo sẵn và các API khác để giúp bạn dễ dàng tạo và kết hợp ảnh động.

  • Ảnh động ngầm là các hiệu ứng ảnh động được tạo sẵn và tự động chạy toàn bộ ảnh động. Khi giá trị target 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 ẩn bao gồm AnimatedSize, AnimatedScaleAnimatedPositioned.
  • Ảnh động tường minh cũng là các hiệu ứng ảnh động được 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 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 một thời điểm bất kỳ. Đây là một lớp con của Listenable và thông báo cho các trình nghe khi trạng thái thay đổi trong khi hiệu ứng chuyển động đang chạy.
  • AnimationController là một cách để tạo Animation và kiểm soát trạng thái của Animation. Bạn có thể dùng các phương thức của lớp này, chẳng hạn 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ị mọi loại, chẳng hạn như double, 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, bạn thường áp dụng một đường cong gia 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 trong khoảng từ 0 đến 1 và trả về một giá trị đầu ra trong khoảng 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 trắc nghiệm có nhiều hiệu ứng và kỹ thuật hoạt hoạ.

3026390ad413769c.gif

Bạn sẽ thấy cách...

  • Tạo một tiện ích có ảnh động về 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 đẹp 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 có trong 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 hiệu ứng hoạt hoạ ngầm để tạo ra các ảnh động đẹp mắt mà không cần nhiều mã.
  • Cách sử dụng hiệu ứng động rõ ràng để định cấu hình hiệu ứng của riêng bạn bằng cách sử dụng các tiện ích động được 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 động bắt mắt với chế độ thiết lập tối thiểu.

Bạn cần có

  • Flutter SDK
  • 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ó 2 phần mềm để hoàn thành bài thực hành này: Flutter SDKmộ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:

  • Một thiết bị Android thực (nên dùng để 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à được đặt ở chế độ Nhà phát triển.
  • Trình mô phỏng iOS (bạn cần cài đặt các công cụ Xcode).
  • Trình mô phỏng Android (cần thiết lập trong Android Studio).
  • Một 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 một ứng dụng máy tính cho Windows, bạn phải phát triển trên Windows để truy cập vào chuỗi bản dựng thích hợp. Có những yêu cầu cụ thể theo hệ điều hành được trình bày chi tiết trên docs.flutter.dev/desktop.

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

Để xác minh rằng Flutter SDK được định cấu hình đúng cách và bạn đã cài đặt ít nhất một trong các nền tảng mục tiêu nê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 đầu

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

Sử dụng git để nhân bản ứng dụng bắt đầu 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 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 của Flutter để biết thêm thông tin.

Khám phá đoạn mã

Ứng dụng khởi đầu là một trò chơi trắc nghiệm có nhiều lựa chọn, bao gồm 2 màn hình tuân theo mẫu thiết kế mô hình-khung hiển thị-mô hình khung hiển thị (MVVM). QuestionScreen (View) sử dụng lớp QuizViewModel (View-Model) để hỏi người dùng các câu hỏi trắc nghiệm trong lớp QuestionBank (Model).

  • home_screen.dart – Hiển thị màn hình có nút New Game
  • 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 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ợ bất kỳ hiệu ứng độ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 vào nút New Game (Trò chơi mới).

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

Ảnh động ngầm là lựa chọn phù hợp trong nhiều trường hợp vì không yêu cầu cấu hình đặc biệt. Trong phần này, bạn sẽ cập nhật tiện ích StatusBar để tiện ích này hiển thị một bảng điểm có hiệu ứng động. Để tìm các hiệu ứng hoạt ảnh ngầm định thường gặp, 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 đoạn 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 vào các phần tử con của tiện ích StatusBar, thay thế các tiện ích Text trước đây hiển thị điểm số và tổng số câu hỏi. Trình chỉnh sửa của bạn sẽ tự động thêm import "scoreboard.dart" bắt buộc vào đầ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 một câu hỏi được trả lời đúng, một ngôi sao khác sẽ sáng lên ngay lập tức mà không có hiệu ứng động. Trong các bước sau, bạn sẽ giúp thông báo cho người dùng rằng đ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 hoạt ảnh ngầm

Tạo một tiện ích mới có tên là AnimatedStar dùng tiện ích AnimatedScale để thay đổi số tiền scale từ 0.5 thành 1.0 khi ngôi sao 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(isActive: score > i),                 // Edit this line.
        ],
      ),
    );
  }
}

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 một câu hỏi, tiện ích AnimatedStar sẽ cập nhật kích thước bằng một ảnh động ngầm. color của Icon không được tạo hiệu ứng độ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 sắc có ảnh động, bạn có thể thử dùng tiện ích AnimatedContainer (đây 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 của tiện ích, bao gồm cả màu sắc. Rất tiếc, tiện ích của chúng tôi 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, phương thức này 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ó phương thức triển khai mặc định của biểu tượng ngôi sao trong lớp AnimatedIcons.

Thay vào đó, chúng ta sẽ dùng một lớp con khác của ImplicitlyAnimatedWidget có tên là TweenAnimationBuilder. Lớp này lấy Tween làm tham số. Tween là một lớp lấy 2 giá trị (beginend) rồi 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 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" (Bọc bằng Trình tạo) trong IDE của bạn, 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);     // And modify this line.
        },
      ),
    );
  }
}

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 sẽ 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ị cuối mới, cho phép bạn thay đổi màu 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 hoạt ảnh này đều chạy ở tốc độ không đổi, nhưng hoạt ảnh thường thú vị và giàu thông tin hơn về mặt thị giác khi tăng hoặc giảm tốc độ.

Curve áp dụng một hàm gia tốc, 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 tập hợp các đường cong gia tốc được tạo sẵn trong lớp Curves, chẳng hạn như easeIn hoặc easeOut.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

Những 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 một giá trị đầu vào từ 0,0 đến 1,0 (hiển thị trên trục x) thành một giá trị đầu ra từ 0,0 đến 1,0 (hiển thị trên trục y). Các sơ đồ này cũng cho thấy bản xem trước về hình dạng của nhiều hiệu ứng hoạt ảnh khi chúng sử dụng đường cong gia tốc.

Tạo một trường mới trong AnimatedStar có tên là _curve và truyền trường đó làm tham số cho 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 mang đến hiệu ứng lò xo được phóng đại, bắt đầu bằng chuyển động lò xo và cân bằng về cuối.

8f84142bff312373.gif

Tải lại nhanh ứ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 cho mọi hiệu ứng hoạt ảnh, Flutter DevTools cung cấp một cách để làm chậm tất cả hoạt ảnh trong ứng dụng của bạn, nhờ đó bạn có thể xem hoạt ảnh 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 thanh công cụ Gỡ lỗi trong VSCode hoặc bằng cách chọn nút Open Flutter DevTools (Mở Flutter DevTools) trong cửa sổ công cụ Gỡ lỗi trong IntelliJ / Android Studio.

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

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

adea0a16d01127ad.png

5. Sử dụng hiệu ứng ảnh động rõ ràng

Tương tự 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ị đích, chúng lấy đối tượng Animation làm tham số. Điều này giúp chúng hữu ích trong những trường hợp mà ảnh động đã được xác định bằng một hiệu ứng chuyển đổi điều hướng, chẳng hạn như AnimatedSwitcher hoặc AnimationController.

Sử dụng hiệu ứng ảnh động rõ ràng

Để bắt đầu với hiệu ứng hoạt ảnh rõ ràng, hãy bao bọc tiện ích Card bằng một 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 mờ dần theo mặc định, nhưng bạn có thể ghi đè hiệu ứng này bằng cách sử dụng tham số transitionBuilder. Trình tạo hiệu ứng 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 tường minh.

Đố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ẽ dùng là SlideTransition. Ảnh động này lấy một Animation<Offset> xác định độ lệch bắt đầu và kết thúc mà các tiện ích đến và đi sẽ di chuyển giữa các độ lệch đó.

Tween có một hàm trợ giúp là animate(), hàm này chuyển đổi mọi Animation thành một Animation khác có tween được áp dụng. Điều này có nghĩa là bạn có thể dùng Tween để chuyển đổi Animation do AnimatedSwitcher cung cấp thành Animation, sau đó 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 điều này sử dụng Tween.animate để áp dụng Curve cho Animation, sau đó chuyển đổi Animation từ Tween có phạm vi từ 0,0 đến 1,0 thành Tween chuyển đổi từ -0,1 đến 0,0 trên trục x.

Ngoài ra, lớp Animation có một hàm drive() nhận bất kỳ Tween nào (hoặc Animatable) và chuyển đổi thành Animation mới. Điều này cho phép các tween được "liên kết", giúp mã kết quả 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 khi sử dụng ảnh động rõ ràng là bạn có thể kết hợp chú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 bao bọc 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 QuestionCard chuyển sang một câu hỏi mới, câu hỏi đó sẽ được bố trí ở giữa khoảng trống có sẵn trong khi ảnh động đang chạy. Tuy nhiên, khi ảnh động dừng, tiện ích sẽ chuyển nhanh lên đầu màn hình. Điều này gây ra ảnh động bị 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 mà bạn 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ới đầ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 trong lớp AnimatedSwitcher, nhưng sử dụng Alignment.topCenter thay vì Alignment.center.

Tóm tắt

  • Ảnh động rõ ràng là hiệu ứng ảnh động lấy đối tượng Animation (khác với ImplicitlyAnimatedWidgets, lấy mục tiêu valueduration)
  • Lớp Animation đại diện cho một ả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 TweensCurves (bằng CurveTween) cho một ảnh động.
  • Dùng tham 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 một ảnh động

Cho đến nay, mọi ảnh động đều được khung hình chạy tự động. Ảnh động ngầm chạy tự động và hiệu ứng ảnh động rõ ràng cần có Animation để hoạt động đúng cách. Trong phần này, bạn sẽ tìm hiểu cách tạo các đối tượng Animation của riêng mình bằng cách sử dụ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 một ảnh động bằng AnimationController, bạn cần làm theo các bước sau:

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

Tạo một tệp mới flip_effect.dart rồi sao chép và 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 các trình nghe của nó và tiện ích Transform được dùng để áp dụng hiệu ứng xoay 3D nhằm mô phỏng một thẻ bị lật.

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

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 bằng cách sử dụng tiện ích CardFlipEffect.

5455def725b866f6.gif

Bạn có thể nhận thấy rằng lớp này trông giống như một hiệu ứng hoạt ảnh 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 State, 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 hoạt hoạ 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 một độ trễ vào tiện ích CardFlipEffect để mỗi thẻ lật một lần. Để 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 đó, hãy 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 Animation mới áp dụng độ trễ bằng cách sử dụ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à thứ mà tiện ích kiểm soát một cách 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 hơn khi bật ảnh động chậm trong Công cụ cho nhà phát triển, vì hiệu ứng này sử dụng cùng một TickerProvider.

Để sử dụng TweenSequence, hãy tạo 2 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 chuyển từ 0.0 sang 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>([              // Add from here...
      if (widget.delayAmount > 0)
        TweenSequenceItem(
          tween: ConstantTween<double>(0.0),
          weight: widget.delayAmount,
        ),
      TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
    ]).animate(_animationController);                          // To here.
  }

Cuối cùng, hãy thay thế hiệu ứng chuyển động của AnimationController bằng hiệu ứng chuyển động mới có độ trễ trong phương thức build.

lib/flip_effect.dart

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _animationWithDelay,                            // Modify this line
    builder: (context, child) {
      return Transform(
        alignment: Alignment.center,
        transform: Matrix4.identity()
          ..rotateX(_animationWithDelay.value * math.pi),      // And this line
        child: _animationController.isAnimating
            ? _animationWithDelay.value < 0.5                  // And this one.
                  ? _previousChild
                  : Transform.flip(flipY: true, child: child)
            : child,
      );
    },
    child: widget.child,
  );
}

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, hãy thử nghiệm bằng cách thay đổi góc nhìn 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 đã thấy cách tuỳ chỉnh hiệu ứng trên một màn hình. Tuy nhiên, một cách khác để sử dụng ảnh động là 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 cảnh màn hình bằng cách sử dụng hiệu ứng ảnh động tích hợp và hiệu ứng ảnh động dựng sẵn đẹp mắt do gói animations chính thức cung cấp trên pub.dev.

Tạo ảnh động cho hiệu ứng 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. Bạn có thể ghi đè lệnh gọi lại transitionBuilder của thành phần này. Lệnh gọi lại này cung cấp 2 đối tượng Animation, đại diện cho ảnh động đến và đi do Navigator 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 chuyển từ HomeScreen sang QuestionScreen. Sử dụng FadeTransition (một tiện ích hoạt ảnh rõ ràng) để màn hình mới mờ dần ở trên cùng của màn hình trước.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(                                         // Add from here...
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
              return FadeTransition(
                opacity: animation,
                child: child,
              );
            },
      ),                                                        // To here.
    );
  },
  child: Text('New Game'),
),

Gói ảnh động cung cấp các hiệu ứng ảnh động dựng sẵn đẹp 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(                     // Add from here...
                animation: animation,
                secondaryAnimation: secondaryAnimation,
                child: child,
              );                                                // To here.
            },
      ),
    );
  },
  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 nhanh phía sau tuyến đường hoặc ứng dụng hiện tại để biết nội dung phía sau trước khi điều hướng. Ảnh động xem trước được điều khiển bằng vị trí ngón tay của người dùng khi họ kéo ngược lại 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 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 ứ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 điều hướng giữa các tuyến đường trong một ứ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ẽ chuyển về hành vi ban đầu của cử chỉ quay lại 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 của bạn, hãy định cấu hình PageTransitionsTheme để dùng PredictiveBack trên Android và hiệu ứng chuyển đổi mờ dần trong 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),
        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(),
    );
  }
}

Giờ đây, bạn có thể thay đổi lệnh gọi lại Navigator.push() thành MaterialPageRoute.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      MaterialPageRoute(                                        // Add from here...
        builder: (context) {
          return const QuestionScreen();
        },
      ),                                                        // To here.
    );
  },
  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 Animation 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 một 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(                              // Add from here...
      layoutBuilder: (entries) {
        return Stack(alignment: Alignment.topCenter, children: entries);
      },
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          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,
          ),
        ),
      ),
    );
  }
}

Sử dụng OpenContainer

77358e5776eb104c.png

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

Tiện ích do closedBuilder trả về sẽ xuất hiện ban đầu và mở rộng thành tiện ích do openBuilder trả về khi người dùng 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 gọi lại mới, truyền viewModel vào tiện ích QuestionCard và lưu trữ một lệnh gọi lại sẽ được dùng để hiện màn hình "Game Over":

lib/question_screen.dart

class QuestionScreen extends StatefulWidget {
  const QuestionScreen({super.key});

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  late final QuizViewModel viewModel = QuizViewModel(
    onGameOver: _handleGameOver,
  );
  VoidCallback? _showGameOverScreen;                                    // NEW

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        return Scaffold(
          appBar: AppBar(
            actions: [
              TextButton(
                onPressed:
                    viewModel.hasNextQuestion && viewModel.didAnswerQuestion
                    ? () {
                        viewModel.getNextQuestion();
                      }
                    : null,
                child: const Text('Next'),
              ),
            ],
          ),
          body: Center(
            child: Column(
              children: [
                QuestionCard(                                           // NEW
                  onChangeOpenContainer: _handleChangeOpenContainer,    // NEW
                  question: viewModel.currentQuestion?.question,        // NEW
                  viewModel: viewModel,                                 // NEW
                ),                                                      // NEW
                Spacer(),
                AnswerCards(
                  onTapped: (index) {
                    viewModel.checkAnswer(index);
                  },
                  answers: viewModel.currentQuestion?.possibleAnswers ?? [],
                  correctAnswer: viewModel.didAnswerQuestion
                      ? viewModel.currentQuestion?.correctAnswer
                      : null,
                ),
                StatusBar(viewModel: viewModel),
              ],
            ),
          ),
        );
      },
    );
  }

  void _handleChangeOpenContainer(VoidCallback openContainer) {        // NEW
    _showGameOverScreen = openContainer;                               // NEW
  }                                                                    // NEW

  void _handleGameOver() {                                             // NEW
    if (_showGameOverScreen != null) {                                 // NEW
      _showGameOverScreen!();                                          // NEW
    }                                                                  // NEW
  }                                                                    // NEW
}

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ế Card bằng tiện ích OpenContainer trong gói animations, thêm 2 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 các hiệu ứng hoạt ảnh vào một ứ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 hoạt ảnh của 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 CurvesTweens cho một ảnh động
  • Cách sử dụng các tiện ích chuyển cảnh dựng sẵn như AnimatedSwitcher hoặc PageRouteBuilder
  • Cách sử dụng các hiệu ứng ảnh động dựng sẵn đẹp mắt từ gói animations, chẳng hạn như FadeThroughTransitionOpenContainer
  • 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ợ Xem trước thao tác quay lại trên Android.

3026390ad413769c.gif

Tiếp theo là gì?

Hãy xem một số lớp học lập trình này:

Hoặc tải ứng dụng mẫu ảnh động xuống. Ứng dụng này minh hoạ nhiều kỹ thuật tạo ảnh động.

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

Bạn có thể tìm thêm tài nguyên về ả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