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 มิติ - วิธีใช้แพ็กเกจ
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
แอปยังไม่รองรับเอฟเฟกต์ภาพเคลื่อนไหวใดๆ ยกเว้นการเปลี่ยนมุมมองเริ่มต้นที่แสดงโดยคลาส 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( // 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
ใช้ 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.
),
);
}
}
จากนั้นโหลดแอปซ้ำขณะทำงานเพื่อดูภาพเคลื่อนไหวใหม่
โปรดทราบว่าค่า 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
ให้เอฟเฟกต์สปริงที่เกินจริงซึ่งเริ่มต้นด้วยการเคลื่อนไหวแบบสปริงและสมดุลกันในตอนท้าย
โหลดแอปซ้ำแบบ Hot Reload เพื่อดูว่าเส้นโค้งนี้มีผลกับ 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<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 เมื่อการ์ดคำถามเปลี่ยนไปใช้คำถามใหม่ การ์ดจะวางคำถามไว้ตรงกลางพื้นที่ว่างขณะที่ภาพเคลื่อนไหวทำงาน แต่เมื่อภาพเคลื่อนไหวหยุดลง วิดเจ็ตจะย้ายไปอยู่ที่ด้านบนของหน้าจอ ซึ่งทำให้ภาพเคลื่อนไหวกระตุกเนื่องจากตำแหน่งสุดท้ายของการ์ดคำถามไม่ตรงกับตำแหน่งขณะที่ภาพเคลื่อนไหวทำงาน
วิธีแก้ไขคือ 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 คุณจะต้องทําตามขั้นตอนต่อไปนี้
- สร้าง StatefulWidget
- ใช้มิกซ์อิน SingleTickerProviderStateMixin ในคลาส State เพื่อระบุ Ticker ให้กับ AnimationController
- เริ่มต้น AnimationController ในเมธอดวงจรชีวิตของ initState โดยระบุออบเจ็กต์สถานะปัจจุบันให้กับพารามิเตอร์
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
ให้กับวิดเจ็ตการ์ด
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 โดยตรงเพื่อใช้เวอร์ชันของคุณเอง แต่เนื่องจากคลาสนี้จำเป็นต้องจัดเก็บวิดเจ็ตก่อนหน้าไว้ในสถานะ จึงต้องใช้ 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
มีให้
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'),
),
ปรับแต่งภาพเคลื่อนไหวย้อนกลับแบบคาดเดา
การย้อนกลับแบบคาดการณ์เป็นฟีเจอร์ใหม่ของ 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
วิดเจ็ต 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
),
);
}
}
8. ขอแสดงความยินดี
ขอแสดงความยินดี คุณได้เพิ่มเอฟเฟกต์ภาพเคลื่อนไหวลงในแอป Flutter และเรียนรู้เกี่ยวกับคอมโพเนนต์หลักของระบบภาพเคลื่อนไหวของ Flutter เรียบร้อยแล้ว โดยเฉพาะอย่างยิ่ง คุณได้เรียนรู้สิ่งต่อไปนี้
- วิธีใช้ ImplicitlyAnimatedWidget
- วิธีใช้ ExplicitlyAnimatedWidget
- วิธีใช้เส้นโค้งและอินเทอร์โพเลชันกับภาพเคลื่อนไหว
- วิธีใช้วิดเจ็ตการเปลี่ยนที่สร้างไว้ล่วงหน้า เช่น AnimatedSwitcher หรือ PageRouteBuilder
- วิธีใช้เอฟเฟกต์ภาพเคลื่อนไหวที่สร้างไว้ล่วงหน้าอย่างสวยงามจากแพ็กเกจ
animations
เช่น FadeThroughTransition และ OpenContainer - วิธีปรับแต่งภาพเคลื่อนไหวการเปลี่ยนเริ่มต้น รวมถึงการเพิ่มการรองรับการย้อนกลับแบบคาดเดาใน Android
ขั้นตอนถัดไป
ลองดู Codelab ต่อไปนี้
- การสร้างเลย์เอาต์แอปที่ปรับเปลี่ยนตามพื้นที่โฆษณาแบบเคลื่อนไหวด้วย Material 3
- การสร้างทรานซิชันที่สวยงามด้วย Material Motion สำหรับ Flutter
- เปลี่ยนแอป Flutter ของคุณจากน่าเบื่อให้สวยงาม
หรือดาวน์โหลดแอปตัวอย่างภาพเคลื่อนไหวซึ่งแสดงเทคนิคภาพเคลื่อนไหวต่างๆ
อ่านเพิ่มเติม
ดูแหล่งข้อมูลภาพเคลื่อนไหวเพิ่มเติมได้ใน flutter.dev
- ข้อมูลเบื้องต้นเกี่ยวกับภาพเคลื่อนไหว
- บทแนะนำภาพเคลื่อนไหว (บทแนะนำ)
- ภาพเคลื่อนไหวแบบไม่เจาะจง (บทแนะนำ)
- ทำให้พร็อพเพอร์ตี้ของคอนเทนเนอร์เคลื่อนไหว (ตำรา)
- ทำให้วิดเจ็ตปรากฏขึ้นและจางหายไป (ตำราอาหาร)
- ภาพเคลื่อนไหวของรูปภาพหลัก
- ทำให้การเปลี่ยนเส้นทางหน้าเว็บเคลื่อนไหว (ตำรา)
- สร้างภาพเคลื่อนไหวของวิดเจ็ตโดยใช้การจำลองฟิสิกส์ (ตำรา)
- ภาพเคลื่อนไหวแบบสลับ
- วิดเจ็ตภาพเคลื่อนไหวและการเคลื่อนไหว (แคตตาล็อกวิดเจ็ต)
หรืออ่านบทความเหล่านี้ใน Medium
- เจาะลึกภาพเคลื่อนไหว
- ภาพเคลื่อนไหวโดยนัยที่กําหนดเองใน Flutter
- การจัดการภาพเคลื่อนไหวด้วย Flutter และ Flux / Redux
- วิธีเลือกวิดเจ็ตภาพเคลื่อนไหว Flutter ที่เหมาะกับคุณ
- ภาพเคลื่อนไหวตามทิศทางที่มีภาพเคลื่อนไหวที่ชัดเจนในตัว
- ข้อมูลเบื้องต้นเกี่ยวกับภาพเคลื่อนไหว Flutter ด้วยภาพเคลื่อนไหวโดยนัย
- ฉันควรใช้ AnimatedBuilder หรือ AnimatedWidget เมื่อใด