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

คุณจะเห็นวิธี...
- สร้างวิดเจ็ตที่เคลื่อนไหวขนาดและสี
- สร้างเอฟเฟกต์พลิกการ์ด 3 มิติ
- ใช้เอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าจากแพ็กเกจภาพเคลื่อนไหว
- เพิ่มการรองรับท่าทางสัมผัสการย้อนกลับที่คาดการณ์ได้ซึ่งมีให้บริการใน Android เวอร์ชันล่าสุด
สิ่งที่คุณจะได้เรียนรู้
ในโค้ดแล็บนี้ คุณจะได้เรียนรู้สิ่งต่อไปนี้
- วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวโดยนัยเพื่อให้ได้ภาพเคลื่อนไหวที่ดูดีโดยไม่ต้องเขียนโค้ดมาก
- วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวที่ชัดเจนเพื่อกำหนดค่าเอฟเฟกต์ของคุณเองโดยใช้วิดเจ็ตภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้า เช่น
AnimatedSwitcherหรือAnimationController - วิธีใช้
AnimationControllerเพื่อกำหนดวิดเจ็ตของคุณเองที่แสดงเอฟเฟกต์ 3 มิติ - วิธีใช้แพ็กเกจ
animationsเพื่อแสดงเอฟเฟกต์ภาพเคลื่อนไหวที่สวยงามโดยมีการตั้งค่าขั้นต่ำ
สิ่งที่คุณต้องมี
- Flutter SDK
- IDE เช่น VSCode หรือ Android Studio / IntelliJ
2. ตั้งค่าสภาพแวดล้อมในการพัฒนา Flutter
คุณต้องมีซอฟต์แวร์ 2 อย่างจึงจะทำแล็บนี้ให้เสร็จสมบูรณ์ ได้แก่ Flutter SDK และโปรแกรมแก้ไข
คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้
- อุปกรณ์ Android (แนะนำให้ใช้เพื่อติดตั้งใช้งานการย้อนกลับแบบคาดการณ์ในขั้นตอนที่ 7) หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาแอป
- โปรแกรมจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
- Android Emulator (ต้องตั้งค่าใน 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-ViewModel หรือ MVVM QuestionScreen (View) ใช้คลาส QuizViewModel (View-Model) เพื่อถามคำถามแบบหลายตัวเลือกจากคลาส QuestionBank (Model) แก่ผู้ใช้
- home_screen.dart - แสดงหน้าจอที่มีปุ่มเกมใหม่
- main.dart - กำหนดค่า
MaterialAppให้ใช้ Material 3 และแสดงหน้าจอหลัก - model.dart - กำหนดคลาสหลักที่ใช้ทั่วทั้งแอป
- question_screen.dart - แสดง UI สำหรับเกมแบบทดสอบ
- view_model.dart - จัดเก็บสถานะและตรรกะสำหรับเกมแบบทดสอบ ซึ่งแสดงโดย
QuestionScreen

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

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

ใช้ Tween เพื่อประมาณค่าระหว่าง 2 ค่า
โปรดสังเกตว่าสีของวิดเจ็ต AnimatedStar จะเปลี่ยนทันทีหลังจากฟิลด์ isActive เปลี่ยนเป็น "จริง"
หากต้องการสร้างเอฟเฟกต์สีแบบเคลื่อนไหว คุณอาจลองใช้วิดเจ็ต AnimatedContainer (ซึ่งเป็นคลาสย่อยอีกคลาสหนึ่งของ ImplicitlyAnimatedWidget) เนื่องจากวิดเจ็ตนี้สามารถเคลื่อนไหวแอตทริบิวต์ทั้งหมดได้โดยอัตโนมัติ รวมถึงสีด้วย ขออภัย วิดเจ็ตของเราต้องแสดงไอคอน ไม่ใช่คอนเทนเนอร์
คุณอาจลองใช้ AnimatedIcon ซึ่งจะใช้เอฟเฟกต์การเปลี่ยนระหว่างรูปร่างของไอคอน แต่ไม่มีการใช้งานไอคอนดาวเริ่มต้นในคลาส AnimatedIcons
แต่เราจะใช้คลาสย่อยอีกคลาสหนึ่งของ ImplicitlyAnimatedWidget ที่ชื่อ TweenAnimationBuilder ซึ่งใช้ Tween เป็นพารามิเตอร์ Tween คือคลาสที่รับค่า 2 ค่า (begin และ end) และคำนวณค่าระหว่างกลาง เพื่อให้ภาพเคลื่อนไหวแสดงค่าเหล่านั้นได้ ในตัวอย่างนี้ เราจะใช้ ColorTween ซึ่งเป็นไปตามอินเทอร์เฟซ Tween ที่จำเป็นต่อการสร้างเอฟเฟกต์ภาพเคลื่อนไหว
เลือกวิดเจ็ต Icon แล้วใช้การดำเนินการด่วน "Wrap with Builder" ใน 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 ให้ตรวจสอบว่าแอปกำลังทำงานในโหมดแก้ไขข้อบกพร่อง แล้วเปิด Widget Inspector โดยเลือกในแถบเครื่องมือแก้ไขข้อบกพร่องใน 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 นี่เป็นโอกาสที่ดีในการใช้ภาพเคลื่อนไหวที่ชัดเจน
สำหรับ Codelab นี้ แอนิเมชันที่ชัดเจนแรกที่เราจะใช้คือ 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 ใหม่ ซึ่งช่วยให้ "เชื่อมโยง" ทวีตได้ ทำให้โค้ดที่ได้กระชับมากขึ้น
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) กับภาพเคลื่อนไหว - ใช้พารามิเตอร์
AnimatedSwitcher'slayoutBuilderเพื่อปรับวิธีวางเลย์เอาต์ขององค์ประกอบย่อย
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 widget โปรดระบุ 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
);
}),
);
}
ตอนนี้ให้โหลดแอปซ้ำอย่างรวดเร็วเพื่อดูการ์ดคำตอบพลิกกลับโดยใช้วิดเจ็ต 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,
);
}
ตอนนี้ให้โหลดแอปซ้ำอย่างรวดเร็ว แล้วดูการ์ดพลิกทีละใบ หากต้องการความท้าทาย ลองเปลี่ยนมุมมองของเอฟเฟกต์ 3 มิติที่วิดเจ็ต Transform มีให้

7. ใช้การเปลี่ยนฉากการนำทางที่กำหนดเอง
ที่ผ่านมาเราได้เห็นวิธีปรับแต่งเอฟเฟกต์บนหน้าจอเดียวไปแล้ว แต่การใช้ภาพเคลื่อนไหวอีกวิธีหนึ่งคือการใช้เพื่อเปลี่ยนผ่านระหว่างหน้าจอ ในส่วนนี้ คุณจะได้เรียนรู้วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวกับการเปลี่ยนหน้าจอโดยใช้เอฟเฟกต์ภาพเคลื่อนไหวในตัวและเอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าอย่างสวยงามซึ่งมีให้ในแพ็กเกจ animations อย่างเป็นทางการบน pub.dev
สร้างภาพเคลื่อนไหวการเปลี่ยนการนำทาง
PageRouteBuilder คลาสคือ Route ที่ช่วยให้คุณปรับแต่งภาพเคลื่อนไหวการเปลี่ยนได้ ซึ่งจะช่วยให้คุณลบล้างtransitionBuilderการเรียกกลับของฟังก์ชันนี้ ซึ่งจะให้ออบเจ็กต์ 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 Callback
หากต้องการเชื่อมต่อopenContainerการเรียกกลับกับ View-Model ให้เพิ่ม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 เมื่อใด