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

คุณจะเห็นวิธีต่อไปนี้
- สร้างวิดเจ็ตที่มีภาพเคลื่อนไหวของขนาดและสี
- สร้างเอฟเฟกต์การพลิกการ์ด 3 มิติ
- ใช้เอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าจากแพ็กเกจภาพเคลื่อนไหว
- เพิ่มการรองรับท่าทางสัมผัสการย้อนกลับที่คาดการณ์ได้ใน Android เวอร์ชันล่าสุด
สิ่งที่คุณจะได้เรียนรู้
ในโค้ดแล็บนี้ คุณจะได้เรียนรู้สิ่งต่อไปนี้
- วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวโดยนัยเพื่อให้ได้ภาพเคลื่อนไหวที่ดูดีโดยไม่ต้องใช้โค้ดจำนวนมาก
- วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจนเพื่อกำหนดค่าเอฟเฟกต์ของคุณเองโดยใช้วิดเจ็ตภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้า เช่น
AnimatedSwitcherหรือAnimationController - วิธีใช้
AnimationControllerเพื่อกำหนดวิดเจ็ตของคุณเองซึ่งแสดงผลเป็นเอฟเฟกต์ 3 มิติ - วิธีใช้
animationspackage เพื่อแสดงเอฟเฟกต์ภาพเคลื่อนไหวแฟนซีด้วยการตั้งค่าเพียงเล็กน้อย
สิ่งที่คุณต้องมี
- 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

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

สร้างวิดเจ็ตตารางสรุปสถิติแบบไม่มีภาพเคลื่อนไหว
สร้างไฟล์ใหม่ 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(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.
ตอนนี้เมื่อผู้ใช้ตอบคำถามอย่างถูกต้อง วิดเจ็ต AnimatedStar จะอัปเดตขนาดโดยใช้ภาพเคลื่อนไหวโดยนัย color ของ Icon จะไม่เคลื่อนไหวที่นี่ มีเพียง scale เท่านั้นที่เคลื่อนไหว ซึ่งดำเนินการโดยวิดเจ็ต AnimatedScale

ใช้ Tween เพื่อหาค่าประมาณระหว่าง 2 ค่า
โปรดสังเกตว่าสีของวิดเจ็ต AnimatedStar จะเปลี่ยนทันทีหลังจากช่อง isActive เปลี่ยนเป็น "จริง"
หากต้องการสร้างเอฟเฟกต์สีแบบเคลื่อนไหว คุณอาจลองใช้วิดเจ็ต AnimatedContainer (ซึ่งเป็นคลาสย่อยอีกคลาสหนึ่งของ ImplicitlyAnimatedWidget) เนื่องจากวิดเจ็ตนี้สามารถทำให้แอตทริบิวต์ทั้งหมด รวมถึงสี เคลื่อนไหวได้โดยอัตโนมัติ ขออภัย วิดเจ็ตของเราต้องแสดงไอคอน ไม่ใช่คอนเทนเนอร์
คุณอาจลองใช้ AnimatedIcon ซึ่งใช้เอฟเฟกต์การเปลี่ยนระหว่างรูปร่างของไอคอน แต่ไม่มีการใช้ไอคอนดาวเริ่มต้นในคลาส AnimatedIcons
แต่เราจะใช้คลาสย่อยอีกคลาสหนึ่งของ ImplicitlyAnimatedWidget ที่เรียกว่า TweenAnimationBuilder ซึ่งใช้ Tween เป็นพารามิเตอร์แทน Tween คือคลาสที่ใช้ค่า 2 ค่า (begin และ end) และคำนวณค่าที่อยู่ตรงกลางเพื่อให้ภาพเคลื่อนไหวแสดงค่าเหล่านั้นได้ ในตัวอย่างนี้ เราจะใช้ ColorTween ซึ่งเป็นไปตามอินเทอร์เฟซ Tween ที่จําเป็นต่อการสร้างเอฟเฟกต์ภาพเคลื่อนไหว
เลือกวิดเจ็ต 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); // And modify this line.
},
),
);
}
}
จากนั้นโหลดแอปซ้ำขณะทำงานเพื่อดูภาพเคลื่อนไหวใหม่

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


แผนภาพเหล่านี้ (ดูได้ใน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 ให้เอฟเฟกต์สปริงที่เกินจริงซึ่งเริ่มต้นด้วยการเคลื่อนไหวแบบสปริงและสมดุลกันในตอนท้าย

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

ใช้เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์เพื่อเปิดใช้ภาพเคลื่อนไหวแบบช้า
หากต้องการแก้ไขข้อบกพร่องของเอฟเฟกต์ภาพเคลื่อนไหว Flutter DevTools มีวิธีทำให้ภาพเคลื่อนไหวทั้งหมดในแอปช้าลงเพื่อให้คุณเห็นภาพเคลื่อนไหวได้ชัดเจนยิ่งขึ้น
หากต้องการเปิด DevTools ให้ตรวจสอบว่าแอปทำงานอยู่ในโหมดแก้ไขข้อบกพร่อง และเปิดเครื่องมือตรวจสอบวิดเจ็ตโดยเลือกในแถบเครื่องมือแก้ไขข้อบกพร่องใน VSCode หรือเลือกปุ่มเปิด Flutter DevTools ในหน้าต่างเครื่องมือแก้ไขข้อบกพร่องใน IntelliJ / Android Studio


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

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 สามารถใช้เพื่อแปลง Animation ที่ AnimatedSwitcher ระบุเป็น Animation เพื่อส่งไปยังวิดเจ็ต 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 ที่มีช่วง 0.0 ถึง 1.0 เป็น Tween ที่เปลี่ยนจาก -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 เมื่อ QuestionCard เปลี่ยนไปใช้คำถามใหม่ ระบบจะวางคำถามนั้นไว้ตรงกลางพื้นที่ว่างขณะที่ภาพเคลื่อนไหวทำงาน แต่เมื่อภาพเคลื่อนไหวหยุดลง วิดเจ็ตจะย้ายไปอยู่ที่ด้านบนของหน้าจอ ซึ่งทำให้ภาพเคลื่อนไหวกระตุกเนื่องจากตำแหน่งสุดท้ายของการ์ดคำถามไม่ตรงกับตำแหน่งขณะที่ภาพเคลื่อนไหวทำงานอยู่

ในการแก้ไขปัญหานี้ 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ซึ่งใช้valueและdurationเป้าหมาย) - คลาส
Animationแสดงภาพเคลื่อนไหวที่ทำงานอยู่ แต่ไม่กำหนดเอฟเฟกต์ที่เฉพาะเจาะจง - ใช้
Tween().animateหรือAnimation.drive()เพื่อใช้TweensและCurves(โดยใช้CurveTween) กับภาพเคลื่อนไหว - ใช้พารามิเตอร์
layoutBuilderของAnimatedSwitcherเพื่อปรับวิธีจัดวางองค์ประกอบย่อย
6. ควบคุมสถานะของภาพเคลื่อนไหว
ที่ผ่านมาเฟรมเวิร์กจะเรียกใช้ภาพเคลื่อนไหวทุกรายการโดยอัตโนมัติ ภาพเคลื่อนไหวโดยนัยจะทำงานโดยอัตโนมัติ ส่วนเอฟเฟกต์ภาพเคลื่อนไหวโดยชัดแจ้งต้องใช้ Animation จึงจะทำงานได้อย่างถูกต้อง ในส่วนนี้ คุณจะได้เรียนรู้วิธีสร้างออบเจ็กต์ Animation ของคุณเองโดยใช้ AnimationController และใช้ TweenSequence เพื่อรวม Tween เข้าด้วยกัน
เรียกใช้ภาพเคลื่อนไหวโดยใช้ AnimationController
หากต้องการสร้างภาพเคลื่อนไหวโดยใช้ AnimationController คุณจะต้องทําตามขั้นตอนต่อไปนี้
- สร้าง
StatefulWidget - ใช้
SingleTickerProviderStateMixinmixin ในคลาสStateเพื่อระบุTickerให้กับAnimationController - เริ่มต้น
AnimationControllerในเมธอดวงจรชีวิตของinitStateโดยระบุออบเจ็กต์Stateปัจจุบันไปยังพารามิเตอร์vsync(TickerProvider) - ตรวจสอบว่าวิดเจ็ตจะสร้างใหม่ทุกครั้งที่
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 ให้กับวิดเจ็ต 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
);
}),
);
}
ตอนนี้ให้โหลดแอปซ้ำแบบ Hot Reload เพื่อดูการ์ดคำตอบพลิกกลับโดยใช้วิดเจ็ต CardFlipEffect

คุณอาจสังเกตเห็นว่าคลาสนี้มีลักษณะคล้ายกับเอฟเฟกต์ภาพเคลื่อนไหวที่อาจไม่เหมาะสม อันที่จริงแล้ว เราขอแนะนำให้ขยายคลาส AnimatedWidget โดยตรงเพื่อใช้เวอร์ชันของคุณเอง แต่เนื่องจากคลาสนี้จําเป็นต้องจัดเก็บวิดเจ็ตก่อนหน้าใน State จึงต้องใช้ 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 ให้สร้าง Animation ใหม่ที่ใช้การเลื่อนเวลาโดยใช้ 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>([ // 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.
}
สุดท้าย ให้แทนที่ภาพเคลื่อนไหวของ 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 มีให้

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( // 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'),
),
แพ็กเกจภาพเคลื่อนไหวมีเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าอย่าง 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( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
child: Text('New Game'),
),
ปรับแต่งภาพเคลื่อนไหวย้อนกลับแบบคาดเดา

การย้อนกลับแบบคาดการณ์เป็นฟีเจอร์ใหม่ของ 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),
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( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
ใช้ FadeThroughTransition เพื่อเปลี่ยนคำถามปัจจุบัน
วิดเจ็ต AnimatedSwitcher มี Animation เพียงรายการเดียวในการเรียกกลับของเครื่องมือสร้าง แพ็กเกจ 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( // 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,
),
),
),
);
}
}
ใช้ OpenContainer

วิดเจ็ต 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 ให้แทนที่ Card ด้วยวิดเจ็ต OpenContainer จากแพ็กเกจ animations โดยเพิ่ม 2 ช่องใหม่สําหรับ viewModel และเปิดการเรียกกลับคอนเทนเนอร์ ดังนี้
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
),
);
}
}

8. ขอแสดงความยินดี
ขอแสดงความยินดี คุณได้เพิ่มเอฟเฟกต์ภาพเคลื่อนไหวลงในแอป Flutter และเรียนรู้เกี่ยวกับคอมโพเนนต์หลักของระบบภาพเคลื่อนไหวของ Flutter เรียบร้อยแล้ว โดยเฉพาะอย่างยิ่ง คุณได้เรียนรู้สิ่งต่อไปนี้
- วิธีใช้
ImplicitlyAnimatedWidget - วิธีใช้
ExplicitlyAnimatedWidget - วิธีใช้
CurvesและTweensกับภาพเคลื่อนไหว - วิธีใช้วิดเจ็ตทรานซิชันที่สร้างไว้ล่วงหน้า เช่น
AnimatedSwitcherหรือPageRouteBuilder - วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าอย่างสวยงามจากแพ็กเกจ
animationsเช่นFadeThroughTransitionและOpenContainer - วิธีปรับแต่งภาพเคลื่อนไหวการเปลี่ยนเริ่มต้น รวมถึงการเพิ่มการรองรับการย้อนกลับแบบคาดเดาใน Android

ขั้นตอนถัดไปคือ
ลองดู Codelab ต่อไปนี้
- การสร้างเลย์เอาต์แอปที่ปรับเปลี่ยนตามพื้นที่โฆษณาแบบเคลื่อนไหวด้วย Material 3
- การสร้างทรานซิชันที่สวยงามด้วย Material Motion สำหรับ Flutter
- เปลี่ยนแอป Flutter ของคุณจากน่าเบื่อให้สวยงาม
หรือดาวน์โหลดแอปตัวอย่างภาพเคลื่อนไหวซึ่งแสดงเทคนิคภาพเคลื่อนไหวต่างๆ
อ่านเพิ่มเติม
ดูแหล่งข้อมูลภาพเคลื่อนไหวเพิ่มเติมได้ใน flutter.dev
- ข้อมูลเบื้องต้นเกี่ยวกับภาพเคลื่อนไหว
- บทแนะนำภาพเคลื่อนไหว (บทแนะนำ)
- ภาพเคลื่อนไหวแบบไม่เจาะจง (บทแนะนำ)
- ทำให้พร็อพเพอร์ตี้ของคอนเทนเนอร์เคลื่อนไหว (ตำรา)
- ทำให้วิดเจ็ตปรากฏขึ้นและจางหายไป (ตำราอาหาร)
- ภาพเคลื่อนไหวของรูปภาพหลัก
- ทำให้การเปลี่ยนเส้นทางหน้าเว็บเคลื่อนไหว (ตำรา)
- สร้างภาพเคลื่อนไหวของวิดเจ็ตโดยใช้การจำลองฟิสิกส์ (ตำรา)
- ภาพเคลื่อนไหวแบบสลับ
- วิดเจ็ตภาพเคลื่อนไหวและการเคลื่อนไหว (แคตตาล็อกวิดเจ็ต)
หรืออ่านบทความเหล่านี้ใน Medium
- เจาะลึกภาพเคลื่อนไหว
- ภาพเคลื่อนไหวโดยนัยที่กําหนดเองใน Flutter
- การจัดการภาพเคลื่อนไหวด้วย Flutter และ Flux / Redux
- วิธีเลือกวิดเจ็ตภาพเคลื่อนไหว Flutter ที่เหมาะกับคุณ
- ภาพเคลื่อนไหวตามทิศทางที่มีภาพเคลื่อนไหวที่ชัดเจนในตัว
- ข้อมูลเบื้องต้นเกี่ยวกับภาพเคลื่อนไหวของ Flutter ที่มีภาพเคลื่อนไหวโดยนัย
- ฉันควรใช้ AnimatedBuilder หรือ AnimatedWidget เมื่อใด