מידע על Codelab זה
1. מבוא
אנימציות הן דרך מצוינת לשפר את חוויית המשתמש באפליקציה, להעביר מידע חשוב למשתמש ולהפוך את האפליקציה למלוטשת ומהנה יותר לשימוש.
סקירה כללית על מסגרת האנימציה של Flutter
כדי להציג אפקטים של אנימציה ב-Flutter, המערכת בונה מחדש חלק מעץ הווידג'טים בכל פריים. הוא מספק אפקטים מוכנים מראש של אנימציה וממשקי API אחרים, כדי שיהיה קל יותר ליצור אנימציות ולהלחין אותן.
- אנימציות משתמעות הן אפקטים של אנימציה מוכנים מראש שמפעילים את כל האנימציה באופן אוטומטי. כשערך היעד של האנימציה משתנה, האנימציה פועלת מהערך הנוכחי לערך היעד, ומציגה כל ערך בדרך כדי שהווידג'ט יתנועע בצורה חלקה. דוגמאות לאנימציות משתמעות:
AnimatedSize
,AnimatedScale
ו-AnimatedPositioned
. - אנימציות בוטה הן גם אפקטים של אנימציה מוכנים מראש, אבל כדי שהן יפעלו נדרש אובייקט
Animation
. דוגמאות:SizeTransition
, ScaleTransition
אוPositionedTransition
. - Animation היא סיווג שמייצג אנימציה שפועלת או מופסקת, והוא מורכב מערך שמייצג את ערך היעד שאליו פועלת האנימציה, ומסטטוס שמייצג את הערך הנוכחי שהאנימציה מציגה במסך בכל זמן נתון. זוהי קבוצת משנה של
Listenable
, והיא מעדכנת את המאזינים שלה כשהסטטוס משתנה בזמן שהאנימציה פועלת. - AnimationController הוא רכיב שמאפשר ליצור אנימציה ולשלוט במצב שלה. אפשר להשתמש בשיטות שלו, כמו
forward()
, reset()
, stop()
ו-repeat()
, כדי לשלוט באנימציה בלי צורך להגדיר את אפקט האנימציה שמוצג, כמו קנה המידה, הגודל או המיקום. - Tweens משמשים לאינטרפולציה של ערכים בין ערך התחלה לערך סיום, והם יכולים לייצג כל סוג, כמו double,
Offset
אוColor
. - עקומות משמשות לשינוי קצב השינוי של פרמטר לאורך זמן. כשאנימציה פועלת, מקובל להחיל עקומת האצה כדי להאיץ או להאט את קצב השינוי בתחילת האנימציה או בסופה. פונקציות עקומה מקבלות ערך קלט בין 0.0 ל-1.0 ומחזירות ערך פלט בין 0.0 ל-1.0.
מה תפַתחו
בקודלאב הזה תלמדו ליצור משחק חידון עם שאלות אמריקאיות, שכולל אפקטים ושיטות שונים של אנימציה.
כאן תלמדו איך...
- יצירת ווידג'ט עם אנימציה של הגודל והצבע שלו
- יצירת אפקט היפוך כרטיס תלת-ממדי
- שימוש באפקטים מוגדרים מראש של אנימציות מרהיבות מחבילת האנימציות
- הוספת תמיכה בתנועת החזרה חזוי שזמינה בגרסה האחרונה של Android
מה תלמדו
ב-Codelab הזה תלמדו:
- איך משתמשים באפקטים עם אנימציה משתמעת כדי ליצור אנימציות יפהפיות בלי צורך בכמות גדולה של קוד.
- איך משתמשים באפקטים עם אנימציה מפורשת כדי להגדיר אפקטים משלכם באמצעות ווידג'טים מונפשים מוכנים מראש, כמו
AnimatedSwitcher
אוAnimationController
. - איך משתמשים ב-
AnimationController
כדי להגדיר ווידג'ט משלכם שמוצג בו אפקט 3D. - איך משתמשים בחבילת
animations
כדי להציג אפקטים אנימציה מרשימים עם הגדרה מינימלית.
מה נדרש
- Flutter SDK
- סביבת פיתוח משולבת (IDE), כמו VSCode או Android Studio / IntelliJ
2. הגדרת סביבת הפיתוח ב-Flutter
כדי להשלים את שיעור ה-Lab הזה, תצטרכו שני תוכנות – Flutter SDK ועורך.
אפשר להריץ את הקודלאב בכל אחד מהמכשירים הבאים:
- מכשיר Android (מומלץ להטמעת חזרה חזותית בשלב 7) או מכשיר iOS פיזי שמחובר למחשב ומוגדר למצב פיתוח.
- סימולטור iOS (נדרשת התקנה של כלי Xcode).
- Android Emulator (נדרשת הגדרה ב-Android Studio).
- דפדפן (נדרש דפדפן Chrome לניפוי באגים).
- מחשב נייח עם מערכת Windows, Linux או macOS. עליכם לפתח בפלטפורמה שבה אתם מתכננים לפרוס. לכן, אם רוצים לפתח אפליקציה למחשב עם Windows, צריך לפתח ב-Windows כדי לגשת לרשת ה-build המתאימה. יש דרישות ספציפיות למערכות הפעלה שפורטו באתר docs.flutter.dev/desktop.
אימות ההתקנה
כדי לוודא ש-Flutter SDK מוגדר בצורה נכונה ושהתקנתם לפחות אחת מפלטפורמות היעד שלמעלה, משתמשים בכלי 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.
סיור בקוד
אפליקציית ההתחלה היא משחק חידון עם תשובות בחירה, שמורכב משני מסכים לפי תבנית העיצוב 'מודל-תצוגה-תצוגת-מודל' (MVVM). ה-QuestionScreen
(תצוגה) משתמש בכיתה QuizViewModel
(תצוגה-מודל) כדי לשאול את המשתמש שאלות אמריקאיות מהכיתה QuestionBank
(מודל).
- home_screen.dart – הצגת מסך עם הלחצן משחק חדש
- main.dart – קובץ שמגדיר את
MaterialApp
כך שישתמש ב-Material 3 ויציג את מסך הבית - model.dart – הגדרת הכיתות המרכזיות שבהן נעשה שימוש בכל האפליקציה
- question_screen.dart – הצגת ממשק המשתמש של משחק החידון
- view_model.dart – כאן מאוחסנים המצב והלוגיקה של משחק הטריוויה, שמוצגים על ידי
QuestionScreen
האפליקציה עדיין לא תומכת באפקטים מונפשים, מלבד מעבר התצוגה שמוגדר כברירת מחדל ומוצג על ידי הכיתה Navigator
של Flutter כשהמשתמש לוחץ על הלחצן New Game (משחק חדש).
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 כדי לבצע אינטרפולציה בין שני ערכים
שימו לב שהצבע של הווידג'ט AnimatedStar
משתנה מיד אחרי שהשדה isActive
משתנה לערך true.
כדי ליצור אפקט צבע מונפש, אפשר לנסות להשתמש בווידג'ט AnimatedContainer
(שנמנה כסוג משנה נוסף של ImplicitlyAnimatedWidget
), כי הוא יכול להנפיש באופן אוטומטי את כל המאפיינים שלו, כולל הצבע. לצערנו, הווידג'ט שלנו צריך להציג סמל, ולא מאגר.
אפשר גם לנסות את AnimatedIcon
, שמטמיע אפקטים של מעבר בין הצורות של הסמלים. אבל אין הטמעה שמוגדרת כברירת מחדל של סמל כוכב בכיתה AnimatedIcons
.
במקום זאת, נשתמש במחלקת משנה אחרת של ImplicitlyAnimatedWidget
שנקראת TweenAnimationBuilder
, שמקבלת פרמטר מסוג Tween
. tween הוא סוג שמקבל שני ערכים (begin
ו-end
) ומחשב את הערכים שביניהם, כדי שאפשר יהיה להציג אותם באנימציה. בדוגמה הזו נשתמש ב-ColorTween
, שמתאים לממשק Tween
הנדרש כדי ליצור את אפקט האנימציה.
בוחרים את הווידג'ט Icon
ומשתמשים בפעולה המהירה 'עטיפה ב-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
משתנה. במקרה כזה, האנימציה החדשה פועלת מערך האנימציה הנוכחי לערך הסיום החדש. כך תוכלו לשנות את הצבע בכל שלב (גם כשהאנימציה פועלת) ולהציג אפקט אנימציה חלק עם הערכים המתאימים שבין הערכים ההתחלתיים והסופיים.
החלת עקומה
שני אפקטים ההנפשה האלה פועלים בקצב קבוע, אבל לרוב ההנפשות מעניינות יותר מבחינה חזותית ומספקות יותר מידע כשהן מאיצות או מאטות.
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, מוודאים שהאפליקציה פועלת במצב ניפוי באגים ופותחים את Widget Inspector על ידי בחירה בו בDebug toolbar ב-VSCode, או על ידי לחיצה על הלחצן Open Flutter DevTools בDebug tool window ב-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
. ה-transition builder מספק את הווידג'ט הצאצא שהוענק ל-AnimatedSwitcher
ואובייקט Animation
. זו הזדמנות מצוינת להשתמש באנימציה מפורשת.
בסדנת הקוד הזו, האנימציה הראשונה שנשתמש בה היא SlideTransition
, שמקבלת Animation<Offset>
שמגדיר את ההיסט של ההתחלה והסיום שביןיהם יתבצעו תנועות של הווידג'טים הנכנסים והיוצאים.
ל-Tweens יש פונקציית עזר, 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
s.
הפעלת אנימציה באמצעות AnimationController
כדי ליצור אנימציה באמצעות AnimationController, צריך לבצע את השלבים הבאים:
- צור
StatefulWidget
- משתמשים ב-mixin
SingleTickerProviderStateMixin
בכיתה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
משמש כדי להחיל אפקט של סיבוב תלת-ממדי כדי לדמות כרטיס שמתהפך.
כדי להשתמש בווידג'ט הזה, צריך לעטוף כל כרטיס תשובה בווידג'ט 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
);
}),
);
}
עכשיו אפשר לטעון מחדש את האפליקציה כדי לראות את כרטיסי התשובות מתהפכים באמצעות הווידג'ט 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
לשיטת ה-build 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
. כך קל יותר לנפות באגים באפקט האנימציה כשמפעילים אנימציות איטיות ב-DevTools, כי הוא משתמש באותו TickerProvider
.
כדי להשתמש ב-TweenSequence
, יוצרים שני רכיבי TweenSequenceItem
, אחד שמכיל רכיב 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,
);
}
עכשיו צריך לטעון מחדש את האפליקציה ולראות איך הכרטיסים הופכים אחד אחרי השני. כדי להתנסות, אפשר לנסות לשנות את נקודת המבט של האפקט התלת-ממדי שמוצג בווידג'ט Transform
.
7. שימוש במעברי ניווט בהתאמה אישית
עד עכשיו ראינו איך להתאים אישית אפקטים במסך אחד, אבל אפשר גם להשתמש באנימציות כדי לעבור בין מסכים. בקטע הזה תלמדו איך להחיל אפקטים של אנימציה על מעברים בין מסכים באמצעות אפקטים מובנים של אנימציה ואפקטים מורכבים של אנימציה מוכנים מראש שזמינים בחבילה הרשמית animations ב-pub.dev.
אנימציה של מעבר ניווט
הכיתה PageRouteBuilder
היא Route
שמאפשרת להתאים אישית את אנימציית המעבר. היא מאפשרת לשנות את פונקציית ה-call back transitionBuilder
שלה, שמספקת שני אובייקטים מסוג Animation, שמייצגים את האנימציה הנכנסת והיוצאת שפועלת על ידי 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
אחד בקריאה החוזרת של ה-builder. כדי לטפל בבעיה הזו, החבילה 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
מספק אפקט אנימציה של טרנספורמציה של קונטיינר שמתרחב כדי ליצור חיבור חזותי בין שני ווידג'טים.
הווידג'ט שהחזיר closedBuilder
מוצג בהתחלה, ומתרחב לווידג'ט שהחזיר openBuilder
כשמקישים על המאגר או כשמתבצעת הקריאה החוזרת (callback) של openContainer
.
כדי לחבר את הפונקציה הלא סטטית 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
, מוסיפים שני שדות חדשים ל-viewModel
ולקריאה החוזרת (callback) של מאגר הנתונים הפתוח:
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.
מה השלב הבא?
כדאי לעיין בחלק מהcodelabs הבאים:
- יצירת פריסה רספונסיבית של אפליקציה עם אנימציה באמצעות Material 3
- יצירת מעברים יפים באמצעות Material Motion ל-Flutter
- איך הופכים אפליקציה ב-Flutter משעממת ליפה
אפשר גם להוריד את האפליקציה לדוגמה של אנימציות, שבה מוצגות טכניקות אנימציה שונות.
קריאה נוספת
מקורות מידע נוספים בנושא אנימציות זמינים ב-flutter.dev:
- מבוא לאנימציות
- מדריך בנושא אנימציות (מדריך)
- אנימציות מרומזות (מדריך)
- הוספת אנימציה למאפיינים של קונטיינר (ספר בישול)
- הוספת ווידג'ט והוצאה שלו (ספר המתכונים)
- אנימציות של תמונות ראשיות
- הוספת אנימציה למעבר בין נתיבי דפים (ספר בישול)
- אנימציה של ווידג'ט באמצעות סימולציית פיזיקה (ספר בישול)
- אנימציות מושהות
- ווידג'טים של אנימציה ותנועה (קטלוג ווידג'טים)
אפשר גם לעיין במאמרים הבאים ב-Medium:
- סקירה מפורטת על אנימציה
- אנימציות משתמעות בהתאמה אישית ב-Flutter
- ניהול אנימציה באמצעות Flutter ו-Flux / Redux
- איך בוחרים את ווידג'ט האנימציה המתאים ב-Flutter?
- אנימציות כיווניות עם אנימציות בוטה מובנות
- אנימציה בסיסית ב-Flutter עם אנימציות משתמעות
- מתי כדאי להשתמש ב-AnimatedBuilder או ב-AnimatedWidget?