ภาพเคลื่อนไหวใน Flutter

1. บทนำ

ภาพเคลื่อนไหวเป็นวิธีที่ยอดเยี่ยมในการปรับปรุงประสบการณ์ของผู้ใช้ในแอป สื่อสารข้อมูลสำคัญกับผู้ใช้ และทำให้แอปดูดียิ่งขึ้นและน่าใช้

ภาพรวมของเฟรมเวิร์กภาพเคลื่อนไหวของ Flutter

Flutter แสดงเอฟเฟกต์ภาพเคลื่อนไหวโดยการสร้างบางส่วนของต้นไม้วิดเจ็ตขึ้นมาใหม่ในแต่ละเฟรม โดยจะมีเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าและ API อื่นๆ เพื่อช่วยให้คุณสร้างและประกอบภาพเคลื่อนไหวได้ง่ายขึ้น

  • ภาพเคลื่อนไหวโดยนัยคือเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าซึ่งจะแสดงภาพเคลื่อนไหวทั้งหมดโดยอัตโนมัติ เมื่อค่า target ของภาพเคลื่อนไหวเปลี่ยนแปลง ระบบจะแสดงภาพเคลื่อนไหวจากค่าปัจจุบันไปยังค่าเป้าหมาย และแสดงค่าแต่ละค่าในระหว่างนั้นเพื่อให้วิดเจ็ตเคลื่อนไหวอย่างราบรื่น ตัวอย่างภาพเคลื่อนไหวโดยนัย ได้แก่ AnimatedSize, AnimatedScale และ AnimatedPositioned
  • ภาพเคลื่อนไหวที่ชัดเจนก็เป็นเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าเช่นกัน แต่ต้องใช้ออบเจ็กต์ Animation จึงจะใช้งานได้ เช่น SizeTransition, ScaleTransition หรือ PositionedTransition
  • Animation คือคลาสที่แสดงภาพเคลื่อนไหวที่ทำงานอยู่หรือหยุดทำงานแล้ว และประกอบด้วย value ที่แสดงค่าเป้าหมายที่ภาพเคลื่อนไหวกำลังทำงานอยู่ และ status ที่แสดงค่าปัจจุบันที่ภาพเคลื่อนไหวแสดงบนหน้าจอ ณ เวลาหนึ่งๆ ซึ่งเป็นคลาสย่อยของ Listenable และจะแจ้งให้ผู้ฟังทราบเมื่อสถานะมีการเปลี่ยนแปลงขณะที่ภาพเคลื่อนไหวกำลังทำงาน
  • AnimationController เป็นวิธีสร้างภาพเคลื่อนไหวและควบคุมสถานะ เมธอดต่างๆ เช่น forward(), reset(), stop() และ repeat() สามารถใช้เพื่อควบคุมภาพเคลื่อนไหวได้โดยไม่ต้องกำหนดเอฟเฟกต์ภาพเคลื่อนไหวที่แสดง เช่น สเกล ขนาด หรือตำแหน่ง
  • Tween ใช้เพื่อหาค่าเฉลี่ยระหว่างค่าเริ่มต้นและค่าสิ้นสุด และสามารถแสดงค่าประเภทใดก็ได้ เช่น เลขทศนิยม, Offset หรือ Color
  • เส้นโค้งใช้เพื่อปรับอัตราการเปลี่ยนแปลงของพารามิเตอร์เมื่อเวลาผ่านไป เมื่อภาพเคลื่อนไหวทำงาน โดยทั่วไปจะใช้เส้นโค้งการผ่อนปรนเพื่อทำให้อัตราการเปลี่ยนแปลงเร็วขึ้นหรือช้าลงในช่วงต้นหรือท้ายของภาพเคลื่อนไหว เส้นโค้งใช้ค่าอินพุตระหว่าง 0.0 ถึง 1.0 และแสดงผลค่าเอาต์พุตระหว่าง 0.0 ถึง 1.0

สิ่งที่คุณจะสร้าง

ในโค้ดแล็บนี้ คุณจะได้สร้างเกมแบบตอบคำถามแบบหลายตัวเลือกที่มีเอฟเฟกต์และเทคนิคภาพเคลื่อนไหวต่างๆ

3026390ad413769c.gif

คุณจะเห็นวิธีต่อไปนี้

  • สร้างวิดเจ็ตที่แสดงผลขนาดและสีแบบเคลื่อนไหว
  • สร้างเอฟเฟกต์การพลิกการ์ด 3 มิติ
  • ใช้เอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าจากแพ็กเกจภาพเคลื่อนไหว
  • เพิ่มการรองรับท่าทางสัมผัสการย้อนกลับที่คาดการณ์ได้ใน Android เวอร์ชันล่าสุด

สิ่งที่คุณจะได้เรียนรู้

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้สิ่งต่อไปนี้

  • วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวโดยนัยเพื่อให้ได้ภาพเคลื่อนไหวที่ดูดีโดยไม่ต้องใช้โค้ดจำนวนมาก
  • วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจนเพื่อกำหนดค่าเอฟเฟกต์ของคุณเองโดยใช้วิดเจ็ตภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้า เช่น AnimatedSwitcher หรือ AnimationController
  • วิธีใช้ AnimationController เพื่อกำหนดวิดเจ็ตของคุณเองซึ่งแสดงผลเป็นเอฟเฟกต์ 3 มิติ
  • วิธีใช้แพ็กเกจ animations เพื่อแสดงเอฟเฟกต์ภาพเคลื่อนไหวที่ดูน่าสนใจด้วยการตั้งค่าเพียงเล็กน้อย

สิ่งที่ต้องมี

  • Flutter SDK
  • IDE เช่น VSCode หรือ Android Studio / IntelliJ

2. ตั้งค่าสภาพแวดล้อมการพัฒนา Flutter

คุณต้องใช้ซอฟต์แวร์ 2 อย่างในการฝึกนี้ ได้แก่ Flutter SDK และเครื่องมือแก้ไข

คุณเรียกใช้โค้ดแล็บได้โดยใช้อุปกรณ์ต่อไปนี้

  • อุปกรณ์ Android (แนะนำสำหรับการใช้การกดย้อนกลับแบบคาดการณ์ในขั้นตอนที่ 7) หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาซอฟต์แวร์
  • โปรแกรมจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
  • โปรแกรมจำลอง Android (ต้องมีการตั้งค่าใน Android Studio)
  • เบราว์เซอร์ (ต้องใช้ Chrome สำหรับการแก้ไขข้อบกพร่อง)
  • คอมพิวเตอร์เดสก์ท็อปที่ใช้ Windows, Linux หรือ macOS คุณต้องพัฒนาในแพลตฟอร์มที่คุณวางแผนจะใช้งาน ดังนั้น หากต้องการพัฒนาแอปเดสก์ท็อป Windows คุณต้องพัฒนาใน Windows เพื่อเข้าถึงเชนการบิลด์ที่เหมาะสม มีข้อกำหนดเฉพาะสำหรับระบบปฏิบัติการที่อธิบายไว้อย่างละเอียดใน docs.flutter.dev/desktop

ยืนยันการติดตั้ง

หากต้องการตรวจสอบว่า Flutter SDK ได้รับการกําหนดค่าอย่างถูกต้องและคุณได้ติดตั้งแพลตฟอร์มเป้าหมายอย่างน้อย 1 รายการข้างต้นแล้ว ให้ใช้เครื่องมือ 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. เรียกใช้แอปเริ่มต้น

ดาวน์โหลดแอปเริ่มต้น

ใช้ git เพื่อโคลนแอปเริ่มต้นจากที่เก็บ flutter/samples ใน GitHub

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

หรือจะดาวน์โหลดซอร์สโค้ดเป็นไฟล์ .zip ก็ได้

เรียกใช้แอป

หากต้องการเรียกใช้แอป ให้ใช้คำสั่ง flutter run และระบุอุปกรณ์เป้าหมาย เช่น android, ios หรือ chrome ดูรายการแพลตฟอร์มทั้งหมดที่รองรับได้ที่หน้าแพลตฟอร์มที่รองรับ

$ flutter run -d android

นอกจากนี้ คุณยังเรียกใช้และแก้ไขข้อบกพร่องของแอปโดยใช้ IDE ที่ต้องการได้ด้วย ดูข้อมูลเพิ่มเติมในเอกสารประกอบอย่างเป็นทางการของ Flutter

ทัวร์ชมโค้ด

แอปเริ่มต้นเป็นเกมแบบทดสอบแบบหลายตัวเลือกที่มี 2 หน้าจอตามรูปแบบการออกแบบ Model-View-View-Model หรือ MVVM QuestionScreen (มุมมอง) ใช้คลาส QuizViewModel (มุมมอง-โมเดล) เพื่อถามคำถามแบบหลายตัวเลือกจากคลาส QuestionBank (โมเดล) แก่ผู้ใช้

  • home_screen.dart - แสดงหน้าจอที่มีปุ่มเกมใหม่
  • main.dart - กําหนดค่า MaterialApp ให้ใช้ Material 3 และแสดงหน้าจอหลัก
  • model.dart - กำหนดคลาสหลักที่ใช้ทั่วทั้งแอป
  • question_screen.dart - แสดง UI สำหรับเกมคำถาม
  • view_model.dart - จัดเก็บสถานะและตรรกะสำหรับเกมแบบทดสอบที่แสดงโดย QuestionScreen

fbb1e1f7b6c91e21.png

แอปยังไม่รองรับเอฟเฟกต์ภาพเคลื่อนไหวใดๆ ยกเว้นการเปลี่ยนมุมมองเริ่มต้นที่แสดงโดยคลาส Navigator ของ Flutter เมื่อผู้ใช้กดปุ่มเกมใหม่

4. ใช้เอฟเฟกต์ภาพเคลื่อนไหวโดยนัย

ภาพเคลื่อนไหวโดยนัยเป็นตัวเลือกที่ยอดเยี่ยมในหลายสถานการณ์ เนื่องจากไม่ต้องมีการกําหนดค่าพิเศษใดๆ ในส่วนนี้ คุณจะอัปเดตวิดเจ็ต StatusBar เพื่อให้แสดงตารางสรุปผลแบบเคลื่อนไหว หากต้องการดูเอฟเฟกต์ภาพเคลื่อนไหวโดยนัยทั่วไป ให้เรียกดูเอกสารประกอบของ ImplicitlyAnimatedWidget API

206dd8d9c1fae95.gif

สร้างวิดเจ็ตตารางสรุปสถิติแบบไม่มีภาพเคลื่อนไหว

สร้างไฟล์ใหม่ lib/scoreboard.dart ด้วยโค้ดต่อไปนี้

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

จากนั้นเพิ่มวิดเจ็ต Scoreboard ในรายการย่อยของวิดเจ็ต StatusBar โดยแทนที่วิดเจ็ต Text ที่ก่อนหน้านี้แสดงคะแนนและจำนวนคำถามทั้งหมด เครื่องมือแก้ไขควรเพิ่ม import "scoreboard.dart" ที่จำเป็นที่ด้านบนของไฟล์โดยอัตโนมัติ

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

วิดเจ็ตนี้จะแสดงไอคอนดาวสำหรับแต่ละคำถาม เมื่อตอบคำถามถูกต้องแล้ว ดาวอีกดวงจะสว่างขึ้นทันทีโดยไม่มีภาพเคลื่อนไหว ในขั้นตอนต่อไปนี้ คุณจะแจ้งให้ผู้ใช้ทราบว่าคะแนนมีการเปลี่ยนแปลงโดยแสดงผลขนาดและสีของคะแนนเป็นภาพเคลื่อนไหว

ใช้เอฟเฟกต์ภาพเคลื่อนไหวโดยนัย

สร้างวิดเจ็ตใหม่ชื่อ AnimatedStar ที่ใช้วิดเจ็ต AnimatedScale เพื่อเปลี่ยนจำนวน scale จาก 0.5 เป็น 1.0 เมื่อดาวทำงานอยู่ โดยทำดังนี้

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.

ตอนนี้เมื่อผู้ใช้ตอบคำถามอย่างถูกต้อง วิดเจ็ต AnimatedStar จะอัปเดตขนาดโดยใช้ภาพเคลื่อนไหวโดยนัย color ของ Icon จะไม่เคลื่อนไหวที่นี่ มีเพียง scale เท่านั้นที่เคลื่อนไหว ซึ่งดำเนินการโดยวิดเจ็ต AnimatedScale

84aec4776e70b870.gif

ใช้ Tween เพื่อหาค่าประมาณระหว่าง 2 ค่า

โปรดสังเกตว่าสีของวิดเจ็ต AnimatedStar จะเปลี่ยนทันทีหลังจากช่อง isActive เปลี่ยนเป็น "จริง"

หากต้องการสร้างเอฟเฟกต์สีแบบเคลื่อนไหว คุณอาจลองใช้วิดเจ็ต AnimatedContainer (ซึ่งเป็นคลาสย่อยอีกคลาสหนึ่งของ ImplicitlyAnimatedWidget) เนื่องจากวิดเจ็ตนี้สามารถทำให้แอตทริบิวต์ทั้งหมด รวมถึงสี เคลื่อนไหวได้โดยอัตโนมัติ ขออภัย วิดเจ็ตของเราต้องแสดงไอคอน ไม่ใช่คอนเทนเนอร์

นอกจากนี้ คุณยังลองใช้ AnimatedIcon ได้ด้วย ซึ่งจะใช้เอฟเฟกต์การเปลี่ยนระหว่างรูปร่างของไอคอน แต่ไม่มีการใช้งานไอคอนดาวเริ่มต้นในคลาส AnimatedIcons

แต่เราจะใช้คลาสย่อยอีกคลาสหนึ่งของ ImplicitlyAnimatedWidget ที่เรียกว่า TweenAnimationBuilder ซึ่งใช้ Tween เป็นพารามิเตอร์แทน Tween คือคลาสที่ใช้ค่า 2 ค่า (begin และ end) และคำนวณค่าที่อยู่ตรงกลางเพื่อให้ภาพเคลื่อนไหวแสดงค่าเหล่านั้นได้ ในตัวอย่างนี้ เราจะใช้ ColorTween ซึ่งเป็นไปตามอินเทอร์เฟซ Tween<Color> ที่จําเป็นต่อการสร้างเอฟเฟกต์ภาพเคลื่อนไหว

เลือกวิดเจ็ต Icon และใช้การดำเนินการด่วน "ตัดกับโปรแกรมสร้าง" ใน IDE แล้วเปลี่ยนชื่อเป็น TweenAnimationBuilder จากนั้นระบุระยะเวลาและ 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.
      ),
    );
  }
}

จากนั้นโหลดแอปซ้ำขณะทำงานเพื่อดูภาพเคลื่อนไหวใหม่

8b0911f4af299a60.gif

โปรดทราบว่าค่า end ของ ColorTween จะเปลี่ยนแปลงตามค่าของพารามิเตอร์ isActive เนื่องจาก TweenAnimationBuilder จะเรียกใช้ภาพเคลื่อนไหวซ้ำทุกครั้งที่ค่า Tween.end เปลี่ยนแปลง เมื่อเกิดกรณีนี้ขึ้น ภาพเคลื่อนไหวใหม่จะทำงานจากค่าภาพเคลื่อนไหวปัจจุบันไปยังค่าสิ้นสุดใหม่ ซึ่งจะช่วยให้คุณเปลี่ยนสีได้ทุกเมื่อ (แม้ในขณะที่ภาพเคลื่อนไหวกำลังทำงานอยู่) และแสดงเอฟเฟกต์ภาพเคลื่อนไหวที่ราบรื่นด้วยค่ากลางที่ถูกต้อง

ใช้เส้นโค้ง

เอฟเฟกต์ภาพเคลื่อนไหวทั้ง 2 ประเภทนี้ทำงานในอัตราที่คงที่ แต่ภาพเคลื่อนไหวมักจะน่าสนใจและให้ข้อมูลมากกว่าเมื่อเล่นเร็วขึ้นหรือช้าลง

Curve ใช้ฟังก์ชันการลดลง ซึ่งกําหนดอัตราการเปลี่ยนแปลงของพารามิเตอร์เมื่อเวลาผ่านไป Flutter มาพร้อมกับคอลเล็กชันเส้นโค้งการผ่อนคลายที่สร้างไว้ล่วงหน้าในคลาส Curves เช่น easeIn หรือ easeOut

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

แผนภาพเหล่านี้ (ดูได้ในCurvesหน้าเอกสารประกอบเกี่ยวกับ API) จะให้คําแนะนําเกี่ยวกับวิธีการทำงานของเส้นโค้ง เส้นโค้งจะแปลงค่าอินพุตระหว่าง 0.0 ถึง 1.0 (แสดงบนแกน x) เป็นค่าเอาต์พุตระหว่าง 0.0 ถึง 1.0 (แสดงบนแกน y) แผนภาพเหล่านี้ยังแสดงตัวอย่างลักษณะของเอฟเฟกต์ภาพเคลื่อนไหวต่างๆ เมื่อใช้เส้นโค้งการผ่อนคลายด้วย

สร้างช่องใหม่ใน AnimatedStar ชื่อ _curve แล้วส่งเป็นพารามิเตอร์ไปยังวิดเจ็ต AnimatedScale และ TweenAnimationBuilder

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

ในตัวอย่างนี้ เส้นโค้ง elasticOut ให้เอฟเฟกต์สปริงที่เกินจริงซึ่งเริ่มต้นด้วยการเคลื่อนไหวแบบสปริงและสมดุลกันในตอนท้าย

8f84142bff312373.gif

โหลดแอปซ้ำแบบ Hot Reload เพื่อดูว่าเส้นโค้งนี้มีผลกับ AnimatedSize และ TweenAnimationBuilder หรือไม่

206dd8d9c1fae95.gif

ใช้เครื่องมือสำหรับนักพัฒนาเว็บเพื่อเปิดใช้ภาพเคลื่อนไหวแบบช้า

หากต้องการแก้ไขข้อบกพร่องของเอฟเฟกต์ภาพเคลื่อนไหว Flutter DevTools มีวิธีทำให้ภาพเคลื่อนไหวทั้งหมดในแอปช้าลงเพื่อให้คุณเห็นภาพเคลื่อนไหวได้ชัดเจนยิ่งขึ้น

หากต้องการเปิด DevTools ให้ตรวจสอบว่าแอปทำงานอยู่ในโหมดแก้ไขข้อบกพร่อง และเปิดเครื่องมือตรวจสอบวิดเจ็ตโดยเลือกในแถบเครื่องมือแก้ไขข้อบกพร่องใน VSCode หรือเลือกปุ่มเปิด Flutter DevTools ในหน้าต่างเครื่องมือแก้ไขข้อบกพร่องใน IntelliJ / Android Studio

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

เมื่อเครื่องมือตรวจสอบวิดเจ็ตเปิดขึ้น ให้คลิกปุ่มภาพเคลื่อนไหวช้าในแถบเครื่องมือ

adea0a16d01127ad.png

5. ใช้เอฟเฟกต์ภาพเคลื่อนไหวที่โจ่งแจ้ง

ภาพเคลื่อนไหวที่ชัดเจนเป็นเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าเช่นเดียวกับภาพเคลื่อนไหวโดยนัย แต่จะใช้ออบเจ็กต์ Animation เป็นพารามิเตอร์แทนที่จะใช้ค่าเป้าหมาย ซึ่งทำให้มีประโยชน์ในสถานการณ์ที่ภาพเคลื่อนไหวได้รับการกำหนดโดยการเปลี่ยนเส้นทางการนำทาง AnimatedSwitcher หรือ AnimationController อยู่แล้ว เป็นต้น

ใช้เอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจน

หากต้องการเริ่มต้นใช้งานเอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจน ให้ใส่วิดเจ็ต Card ใน 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 จะใช้เอฟเฟกต์การเฟดโดยค่าเริ่มต้น แต่คุณลบล้างค่านี้ได้โดยใช้พารามิเตอร์ transitionBuilder ตัวสร้างทรานซิชันจะระบุวิดเจ็ตย่อยที่ส่งไปยัง AnimatedSwitcher และออบเจ็กต์ Animation นี่เป็นโอกาสที่ดีในการใช้ภาพเคลื่อนไหวที่ชัดเจน

สําหรับโค้ดแล็บนี้ ภาพเคลื่อนไหวที่ชัดเจนรายการแรกที่เราจะใช้คือ SlideTransition ซึ่งใช้ Animation<Offset> ที่กําหนดระยะเริ่มต้นและระยะสิ้นสุดที่วิดเจ็ตขาเข้าและขาออกจะเคลื่อนไหวไปมา

Tween มีฟังก์ชันตัวช่วย animate() ซึ่งจะแปลง Animation ใดก็ได้เป็น Animation อื่นโดยใช้ Tween ซึ่งหมายความว่า Tween<Offset> สามารถใช้เพื่อแปลง Animation<double> ที่ AnimatedSwitcher ระบุเป็น Animation<Offset> เพื่อส่งไปยังวิดเจ็ต 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,
          ),
        ),
      ),
    );
  }
}

โปรดทราบว่าการดําเนินการนี้ใช้ Tween.animate เพื่อใช้ Curve กับ Animation จากนั้นแปลงจาก Tween<double> ที่มีช่วง 0.0 ถึง 1.0 เป็น Tween<Offset> ที่เปลี่ยนจาก -0.1 เป็น 0.0 ในแกน x

หรือคลาส Animation จะมีฟังก์ชัน drive() ที่รับ Tween (หรือ Animatable) ใดก็ได้และแปลงเป็น Animation ใหม่ ซึ่งช่วยให้ "เชน" Tween ได้ ทำให้โค้ดที่ได้กระชับยิ่งขึ้น

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

ข้อดีอีกอย่างหนึ่งของการใช้ภาพเคลื่อนไหวที่ชัดเจนคือสามารถประกอบเข้าด้วยกันได้อย่างง่ายดาย เพิ่มภาพเคลื่อนไหวที่ชัดเจนอีกรายการหนึ่งคือ FadeTransition ที่ใช้ภาพเคลื่อนไหวโค้งแบบเดียวกันโดยรวมวิดเจ็ต 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
  },

ปรับแต่ง layoutBuilder

คุณอาจสังเกตเห็นปัญหาเล็กน้อยเกี่ยวกับ AnimationSwitcher เมื่อการ์ดคำถามเปลี่ยนไปใช้คำถามใหม่ การ์ดจะวางคำถามไว้ตรงกลางพื้นที่ว่างขณะที่ภาพเคลื่อนไหวทำงาน แต่เมื่อภาพเคลื่อนไหวหยุดลง วิดเจ็ตจะย้ายไปอยู่ที่ด้านบนของหน้าจอ ซึ่งทำให้ภาพเคลื่อนไหวกระตุกเนื่องจากตำแหน่งสุดท้ายของการ์ดคำถามไม่ตรงกับตำแหน่งขณะที่ภาพเคลื่อนไหวทำงาน

d77de181bdde58f7.gif

วิธีแก้ไขคือ AnimatedSwitcher มีพารามิเตอร์ layoutBuilder ด้วย ซึ่งสามารถใช้เพื่อกําหนดเลย์เอาต์ ใช้ฟังก์ชันนี้เพื่อกําหนดค่าเครื่องมือสร้างเลย์เอาต์ให้จัดการ์ดให้อยู่ด้านบนของหน้าจอ

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

โค้ดนี้เป็นเวอร์ชันที่แก้ไขแล้วของ defaultLayoutBuilder จากคลาส AnimatedSwitcher แต่ใช้ Alignment.topCenter แทน Alignment.center

สรุป

  • ภาพเคลื่อนไหวที่ชัดเจนคือเอฟเฟกต์ภาพเคลื่อนไหวที่ใช้ออบเจ็กต์ Animation (ตรงข้ามกับ ImplicitlyAnimatedWidgets ที่ใช้ค่าเป้าหมายและระยะเวลา)
  • คลาส Animation แสดงภาพเคลื่อนไหวที่ทำงานอยู่ แต่ไม่กำหนดเอฟเฟกต์ที่เฉพาะเจาะจง
  • ใช้ Tween().animate หรือ Animation.drive() เพื่อใช้ Tween และ Curve (โดยใช้ CurveTween) กับภาพเคลื่อนไหว
  • ใช้พารามิเตอร์ layoutBuilder ของ AnimatedSwitcher เพื่อปรับวิธีจัดวางองค์ประกอบย่อย

6. ควบคุมสถานะของภาพเคลื่อนไหว

ที่ผ่านมาเฟรมเวิร์กจะเรียกใช้ภาพเคลื่อนไหวทุกรายการโดยอัตโนมัติ ภาพเคลื่อนไหวโดยนัยจะทำงานโดยอัตโนมัติ ส่วนเอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจนต้องใช้ภาพเคลื่อนไหวจึงจะทำงานได้อย่างถูกต้อง ในส่วนนี้ คุณจะได้เรียนรู้วิธีสร้างออบเจ็กต์ภาพเคลื่อนไหวของคุณเองโดยใช้ AnimationController และใช้ TweenSequence เพื่อรวม Tween เข้าด้วยกัน

เรียกใช้ภาพเคลื่อนไหวโดยใช้ AnimationController

หากต้องการสร้างภาพเคลื่อนไหวโดยใช้ AnimationController คุณจะต้องทําตามขั้นตอนต่อไปนี้

  1. สร้าง StatefulWidget
  2. ใช้มิกซ์อิน SingleTickerProviderStateMixin ในคลาส State เพื่อระบุ Ticker ให้กับ AnimationController
  3. เริ่มต้น AnimationController ในเมธอดวงจรชีวิตของ initState โดยระบุออบเจ็กต์สถานะปัจจุบันให้กับพารามิเตอร์ vsync (TickerProvider)
  4. ตรวจสอบว่าวิดเจ็ตจะสร้างใหม่ทุกครั้งที่ AnimationController แจ้งเตือนผู้ฟัง โดยใช้ AnimatedBuilder หรือเรียกฟังฟังฟังก์ชัน listen() และ setState ด้วยตนเอง

สร้างไฟล์ใหม่ชื่อ flip_effect.dart แล้วคัดลอกและวางโค้ดต่อไปนี้

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

คลาสนี้จะตั้งค่า AnimationController และเรียกใช้ภาพเคลื่อนไหวอีกครั้งทุกครั้งที่เฟรมเวิร์กเรียก didUpdateWidget เพื่อแจ้งให้ทราบว่ามีการเปลี่ยนแปลงการกำหนดค่าวิดเจ็ตและอาจมีวิดเจ็ตย่อยใหม่

AnimatedBuilder ช่วยให้มั่นใจได้ว่าระบบจะสร้างต้นไม้วิดเจ็ตขึ้นใหม่ทุกครั้งที่ AnimationController แจ้งเตือนผู้ฟัง และระบบจะใช้วิดเจ็ต Transform เพื่อใช้เอฟเฟกต์การหมุน 3 มิติเพื่อจำลองการพลิกไพ่

หากต้องการใช้วิดเจ็ตนี้ ให้รวมการ์ดคำตอบแต่ละใบไว้ในวิดเจ็ต CardFlipEffect อย่าลืมระบุ key ให้กับวิดเจ็ตการ์ด

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

ตอนนี้ให้โหลดแอปซ้ำแบบ Hot Reload เพื่อดูการ์ดคำตอบพลิกกลับโดยใช้วิดเจ็ต CardFlipEffect

5455def725b866f6.gif

คุณอาจสังเกตเห็นว่าคลาสนี้มีลักษณะคล้ายกับเอฟเฟกต์ภาพเคลื่อนไหวที่อาจไม่เหมาะสม อันที่จริงแล้ว เราขอแนะนำให้ขยายคลาส AnimatedWidget โดยตรงเพื่อใช้เวอร์ชันของคุณเอง แต่เนื่องจากคลาสนี้จำเป็นต้องจัดเก็บวิดเจ็ตก่อนหน้าไว้ในสถานะ จึงต้องใช้ StatefulWidget ดูข้อมูลเพิ่มเติมเกี่ยวกับการสร้างเอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจนของคุณเองได้ในเอกสารประกอบ API สําหรับ AnimatedWidget

เพิ่มการหน่วงเวลาโดยใช้ TweenSequence

ในส่วนนี้ คุณจะเพิ่มการหน่วงเวลาให้กับวิดเจ็ต CardFlipEffect เพื่อให้การ์ดแต่ละใบพลิกทีละใบ เริ่มต้นใช้งานโดยเพิ่มช่องใหม่ชื่อ 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();
}

จากนั้นเพิ่ม delayAmount ลงในเมธอดการสร้าง 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]),

จากนั้นสร้างภาพเคลื่อนไหวใหม่ใน _CardFlipEffectState โดยใช้ TweenSequence เพื่อใช้การเลื่อนเวลา โปรดทราบว่าการดำเนินการนี้ไม่ได้ใช้ยูทิลิตีใดๆ จากไลบรารี dart:async เช่น Future.delayed เนื่องจากความล่าช้าเป็นส่วนหนึ่งของภาพเคลื่อนไหวและไม่ใช่สิ่งที่วิดเจ็ตควบคุมอย่างชัดเจนเมื่อใช้ AnimationController วิธีนี้ช่วยให้แก้ไขข้อบกพร่องของเอฟเฟกต์ภาพเคลื่อนไหวได้ง่ายขึ้นเมื่อเปิดใช้ภาพเคลื่อนไหวช้าในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ เนื่องจากใช้ TickerProvider เดียวกัน

หากต้องการใช้ TweenSequence ให้สร้าง TweenSequenceItem 2 รายการ โดยรายการหนึ่งมี ConstantTween ที่ทำให้ภาพเคลื่อนไหวอยู่ที่ 0 เป็นเวลาตามระยะเวลาที่ต้องการ และ Tween ปกติที่วิ่งจาก 0.0 ถึง 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
  }

สุดท้าย ให้แทนที่ภาพเคลื่อนไหวของ AnimationController ด้วยภาพเคลื่อนไหวแบบเลื่อนเวลาใหม่ในเมธอด 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,
  );
}

ตอนนี้ให้โหลดแอปซ้ำแบบ Hot Reload แล้วดูการ์ดพลิกทีละใบ หากต้องการลองทำสิ่งท้าทาย ให้ลองเปลี่ยนมุมมองของเอฟเฟกต์ 3 มิติที่วิดเจ็ต Transform มีให้

28b5291de9b3f55f.gif

7. ใช้ทรานซิชันการนำทางที่กำหนดเอง

จนถึงตอนนี้ เราได้ดูวิธีปรับแต่งเอฟเฟกต์ในหน้าจอเดียวแล้ว แต่อีกวิธีในการใช้ภาพเคลื่อนไหวคือการใช้ภาพเคลื่อนไหวเพื่อเปลี่ยนระหว่างหน้าจอ ในส่วนนี้ คุณจะได้เรียนรู้วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวกับการเปลี่ยนหน้าจอโดยใช้เอฟเฟกต์ภาพเคลื่อนไหวในตัวและเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าซึ่งดูน่าสนใจจากแพ็กเกจ animations อย่างเป็นทางการใน pub.dev

ทำให้การเปลี่ยนการนำทางเคลื่อนไหว

คลาส PageRouteBuilder คือ Route ที่ช่วยให้คุณปรับแต่งภาพเคลื่อนไหวการเปลี่ยนได้ ซึ่งช่วยให้คุณลบล้างการเรียกกลับ transitionBuilder ของ Navigator ได้ ซึ่งจะสร้างออบเจ็กต์ Animation 2 รายการ ซึ่งแสดงภาพเคลื่อนไหวขาเข้าและขาออกที่ Navigator เรียกใช้

หากต้องการปรับแต่งภาพเคลื่อนไหวการเปลี่ยน ให้แทนที่ MaterialPageRoute ด้วย PageRouteBuilder และหากต้องการปรับแต่งภาพเคลื่อนไหวการเปลี่ยนเมื่อผู้ใช้ไปยัง HomeScreen จาก QuestionScreen ใช้ FadeTransition (วิดเจ็ตที่เคลื่อนไหวอย่างชัดแจ้ง) เพื่อทำให้หน้าจอใหม่ค่อยๆ ปรากฏขึ้นบนหน้าจอก่อนหน้า

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

แพ็กเกจภาพเคลื่อนไหวมีเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าอย่าง FadeThroughTransition นําเข้าแพ็กเกจภาพเคลื่อนไหวและแทนที่วิดเจ็ต FadeTransition ด้วยวิดเจ็ต 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'),
),

ปรับแต่งภาพเคลื่อนไหวย้อนกลับแบบคาดเดา

1c0558ffa3b76439.gif

การย้อนกลับแบบคาดการณ์เป็นฟีเจอร์ใหม่ของ Android ที่ช่วยให้ผู้ใช้ดูเส้นทางหรือแอปปัจจุบันเพื่อดูสิ่งที่อยู่เบื้องหลังได้ก่อนที่จะไปยังส่วนถัดไป ภาพเคลื่อนไหวของฟีเจอร์แสดงตัวอย่างจะอิงตามตำแหน่งของนิ้วของผู้ใช้ขณะที่ลากกลับบนหน้าจอ

Flutter รองรับการกดย้อนกลับแบบคาดการณ์ของระบบโดยเปิดใช้ฟีเจอร์นี้ที่ระดับระบบเมื่อ Flutter ไม่มีเส้นทางที่จะแสดงในกองการนำทาง หรือกล่าวคือ เมื่อการกดย้อนกลับจะออกจากแอป ระบบจะจัดการภาพเคลื่อนไหวนี้ ไม่ใช่ Flutter

Flutter ยังรองรับการย้อนกลับแบบคาดการณ์เมื่อไปยังส่วนต่างๆ ภายในแอป Flutter ด้วย PageTransitionsBuilder พิเศษชื่อ PredictiveBackPageTransitionsBuilder จะคอยฟังท่าทางสัมผัสการย้อนกลับแบบคาดการณ์ของระบบ และขับเคลื่อนการเปลี่ยนหน้าเว็บตามความคืบหน้าของท่าทางสัมผัส

การกดย้อนกลับแบบคาดการณ์ใช้ได้ใน Android U ขึ้นไปเท่านั้น แต่ Flutter จะเปลี่ยนกลับไปใช้ลักษณะการทำงานของท่าทางสัมผัสย้อนกลับเดิมและ ZoomPageTransitionBuilder อย่างราบรื่น ดูข้อมูลเพิ่มเติมได้ที่บล็อกโพสต์ของเรา รวมถึงส่วนเกี่ยวกับวิธีตั้งค่าในแอปของคุณเอง

ในการกำหนดค่า ThemeData สําหรับแอป ให้กําหนดค่า PageTransitionsTheme ให้ใช้ PredictiveBack ใน Android และเอฟเฟกต์การเปลี่ยนภาพแบบจางจากแพ็กเกจภาพเคลื่อนไหวในแพลตฟอร์มอื่นๆ ดังนี้

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

ตอนนี้คุณเปลี่ยนการเรียก Navigator.push() กลับเป็น 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'),
),

ใช้ FadeThroughTransition เพื่อเปลี่ยนคำถามปัจจุบัน

วิดเจ็ต AnimatedSwitcher มีภาพเคลื่อนไหวเพียงรายการเดียวในคอลแบ็กของโปรแกรมสร้าง แพ็กเกจ animations มี 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,
          ),
        ),
      ),
    );
  }
}

ใช้ OpenContainer

77358e5776eb104c.png

วิดเจ็ต OpenContainer จากแพ็กเกจ animations มีเอฟเฟกต์ภาพเคลื่อนไหวการเปลี่ยนรูปแบบคอนเทนเนอร์ที่ขยายออกเพื่อสร้างการเชื่อมต่อที่มองเห็นได้ระหว่างวิดเจ็ต 2 รายการ

วิดเจ็ตที่ closedBuilder แสดงผลในตอนแรกจะขยายเป็นวิดเจ็ตที่ openBuilder แสดงผลเมื่อมีการแตะคอนเทนเนอร์หรือเรียกใช้การเรียกกลับ openContainer

หากต้องการเชื่อมต่อการเรียกคืน openContainer กับวิวโมเดล ให้เพิ่มการผ่าน viewModel ใหม่ลงในวิดเจ็ต QuestionCard และจัดเก็บการเรียกคืนที่จะใช้เพื่อแสดงหน้าจอ "เกมจบแล้ว" ดังนี้

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
}

เพิ่มวิดเจ็ตใหม่ 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);
              },
            ),
          ],
        ),
      ),
    );
  }
}

ในวิดเจ็ต QuestionCard ให้แทนที่การ์ดด้วยวิดเจ็ต OpenContainer จากแพ็กเกจภาพเคลื่อนไหว โดยเพิ่ม 2 ฟิลด์ใหม่สําหรับ viewModel และการเรียกกลับของ Open Container ดังนี้

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. ขอแสดงความยินดี

ขอแสดงความยินดี คุณได้เพิ่มเอฟเฟกต์ภาพเคลื่อนไหวลงในแอป Flutter และเรียนรู้เกี่ยวกับคอมโพเนนต์หลักของระบบภาพเคลื่อนไหวของ Flutter เรียบร้อยแล้ว โดยเฉพาะอย่างยิ่ง คุณได้เรียนรู้สิ่งต่อไปนี้

  • วิธีใช้ ImplicitlyAnimatedWidget
  • วิธีใช้ ExplicitlyAnimatedWidget
  • วิธีใช้เส้นโค้งและอินเทอร์โพเลชันกับภาพเคลื่อนไหว
  • วิธีใช้วิดเจ็ตการเปลี่ยนที่สร้างไว้ล่วงหน้า เช่น AnimatedSwitcher หรือ PageRouteBuilder
  • วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าอย่างสวยงามจากแพ็กเกจ animations เช่น FadeThroughTransition และ OpenContainer
  • วิธีปรับแต่งภาพเคลื่อนไหวการเปลี่ยนเริ่มต้น รวมถึงการเพิ่มการรองรับการย้อนกลับแบบคาดเดาใน Android

3026390ad413769c.gif

ขั้นตอนถัดไป

ลองดู Codelab ต่อไปนี้

หรือดาวน์โหลดแอปตัวอย่างภาพเคลื่อนไหวซึ่งแสดงเทคนิคภาพเคลื่อนไหวต่างๆ

อ่านเพิ่มเติม

ดูแหล่งข้อมูลภาพเคลื่อนไหวเพิ่มเติมได้ใน flutter.dev

หรืออ่านบทความเหล่านี้ใน Medium

เอกสารอ้างอิง