אנימציות ב-Flutter

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.

מה תפַתחו

בקודלאב הזה תלמדו ליצור משחק חידון עם שאלות אמריקאיות, שכולל אפקטים ושיטות שונים של אנימציה.

3026390ad413769c.gif

כאן תלמדו איך...

  • יצירת ווידג'ט עם אנימציה של הגודל והצבע שלו
  • יצירת אפקט היפוך כרטיס תלת-ממדי
  • שימוש באפקטים מוכנים מראש של אנימציות מרהיבות מחבילת האנימציות
  • הוספת תמיכה בתנועת החזרה חזוי שזמינה בגרסה האחרונה של 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

fbb1e1f7b6c91e21.png

האפליקציה עדיין לא תומכת באפקטים מונפשים, מלבד מעבר התצוגה שמוגדר כברירת מחדל ומוצג על ידי הכיתה Navigator של Flutter כשהמשתמש לוחץ על הלחצן New Game (משחק חדש).

4. שימוש באפקטים מרומזים של אנימציה

אנימציות משתמעות הן בחירה מצוינת במצבים רבים, כי הן לא מחייבות הגדרה מיוחדת. בקטע הזה נעדכן את הווידג'ט StatusBar כך שיציג לוח מודעות מונפש. כדי למצוא אפקטים נפוצים של אנימציה משתמעת, אפשר לעיין במסמכי התיעוד של ה-API של ImplicitlyAnimatedWidget.

206dd8d9c1fae95.gif

יצירת ווידג'ט של לוח התוצאות ללא אנימציה

יוצרים קובץ חדש, lib/scoreboard.dart, עם הקוד הבא:

lib/scoreboard.dart

import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            Icon(
              Icons.star,
              size: 50,
              color:
                  score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
            )
        ],
      ),
    );
  }
}

לאחר מכן מוסיפים את הווידג'ט Scoreboard לילדים של הווידג'ט StatusBar, ומחליפים את הווידג'טים Text שקודם הוצגו בהם הציון ומספר השאלות הכולל. מערכת העריכה אמורה להוסיף את import "scoreboard.dart" הנדרש באופן אוטומטי בחלק העליון של הקובץ.

lib/question_screen.dart

class StatusBar extends StatelessWidget {
  final QuizViewModel viewModel;

  const StatusBar({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Padding(
        padding: EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Scoreboard(                                        // NEW
              score: viewModel.score,                          // NEW
              totalQuestions: viewModel.totalQuestions,        // NEW
            ),
          ],
        ),
      ),
    );
  }
}

בווידג'ט הזה מוצג סמל כוכב לכל שאלה. כשנותנים תשובה נכונה לשאלה, כוכב נוסף נדלק באופן מיידי ללא אנימציה. בשלבים הבאים נסביר איך להוסיף אנימציה לגודל ולצבע של הציון כדי להודיע למשתמש שהציון שלו השתנה.

שימוש באפקט אנימציה משתמע

יוצרים ווידג'ט חדש בשם AnimatedStar שמשתמש בווידג'ט AnimatedScale כדי לשנות את הערך של scale מ-0.5 ל-1.0 כשהכוכב הופך לפעיל:

lib/scoreboard.dart

​​import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            AnimatedStar(                                      // NEW
              isActive: score > i,                             // NEW
            )                                                  // NEW
        ],
      ),
    );
  }
}

class AnimatedStar extends StatelessWidget {                   // Add from here...
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: Icon(
        Icons.star,
        size: 50,
        color: isActive ? _activatedColor : _deactivatedColor,
      ),
    );
  }
}                                                              // To here.

עכשיו, כשהמשתמש עונה על שאלה בצורה נכונה, הווידג'ט AnimatedStar מעדכן את הגודל שלו באמצעות אנימציה משתמעת. ה-color של Icon לא נע באנימציה כאן, רק ה-scale, שמתבצע על ידי הווידג'ט AnimatedScale.

84aec4776e70b870.gif

שימוש ב-Tween כדי לבצע אינטרפולציה בין שני ערכים

שימו לב שהצבע של הווידג'ט AnimatedStar משתנה מיד אחרי שהשדה isActive משתנה ל-true.

כדי ליצור אפקט צבע מונפש, אפשר לנסות להשתמש בווידג'ט AnimatedContainer (שנמנה על קבוצת משנה אחרת של ImplicitlyAnimatedWidget), כי הוא יכול להפעיל אנימציה באופן אוטומטי לכל המאפיינים שלו, כולל הצבע. לצערנו, הווידג'ט שלנו צריך להציג סמל, ולא מאגר.

אפשר גם לנסות את AnimatedIcon, שמטמיע אפקטים של מעבר בין הצורות של הסמלים. אבל אין הטמעה שמוגדרת כברירת מחדל של סמל כוכב בכיתה AnimatedIcons.

במקום זאת, נשתמש במחלקת משנה אחרת של ImplicitlyAnimatedWidget שנקראת TweenAnimationBuilder, שמקבלת פרמטר מסוג Tween. tween הוא סוג שמקבל שני ערכים (begin ו-end) ומחשב את הערכים שביניהם, כדי שאפשר יהיה להציג אותם באנימציה. בדוגמה הזו נשתמש ב-ColorTween, שמתאים לממשק Tween<Color> הנדרש כדי ליצור את אפקט האנימציה.

בוחרים את הווידג'ט Icon ומשתמשים בפעולה המהירה 'עטיפה ב-Builder' בסביבת הפיתוח המשולבת, משנים את השם ל-TweenAnimationBuilder. לאחר מכן מציינים את משך הזמן ו-ColorTween.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: TweenAnimationBuilder(                            // Add from here...
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {                     // To here.
          return Icon(
            Icons.star,
            size: 50,
            color: value,                                      // Modify from here...
          );
        },                                                     // To here.
      ),
    );
  }
}

עכשיו צריך לטעון מחדש את האפליקציה כדי לראות את האנימציה החדשה.

8b0911f4af299a60.gif

שימו לב שהערך end של ColorTween משתנה בהתאם לערך של הפרמטר isActive. הסיבה לכך היא ש-TweenAnimationBuilder מפעיל מחדש את האנימציה שלו בכל פעם שערכו של Tween.end משתנה. במקרה כזה, האנימציה החדשה פועלת מערך האנימציה הנוכחי לערך הסיום החדש, כך שתוכלו לשנות את הצבע בכל שלב (גם כשהאנימציה פועלת) ולהציג אפקט אנימציה חלק עם הערכים המתאימים שביניהם.

החלת עקומה

שני אפקטים ההנפשה האלה פועלים בקצב קבוע, אבל לרוב ההנפשות מעניינות יותר מבחינה חזותית ומספקות יותר מידע כשהן מאיצות או מאטות.

Curve מחילה פונקציית העלאת עוצמה, שמגדירה את קצב השינוי של פרמטר לאורך זמן. ב-Flutter יש אוסף של עקומות יישור מוכנות מראש בכיתה Curves, כמו easeIn או easeOut.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

התרשימים האלה (שזמינים בדף המסמכים של API Curves) מספקים רמז לאופן שבו הפונקציות מעוקלות פועלות. עקומות ממירות ערך קלט בין 0.0 ל-1.0 (שמוצג בציר x) לערך פלט בין 0.0 ל-1.0 (שמוצג בציר y). בתרשים הזה מוצגת גם תצוגה מקדימה של איך נראים אפקטים שונים של אנימציה כשמשתמשים בעקומת השתנות הדרגתית.

יוצרים שדה חדש ב-AnimatedStar בשם _curve ומעבירים אותו כפרמטר לווידג'טים AnimatedScale ו-TweenAnimationBuilder.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;
  final Curve _curve = Curves.elasticOut;                       // NEW

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      curve: _curve,                                           // NEW
      duration: _duration,
      child: TweenAnimationBuilder(
        curve: _curve,                                         // NEW
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {
          return Icon(
            Icons.star,
            size: 50,
            color: value,
          );
        },
      ),
    );
  }
}

בדוגמה הזו, העקומה elasticOut מספקת אפקט קפיץ מוגזם שמתחיל בתנועה קפיצית ומאזן את עצמו לקראת הסוף.

8f84142bff312373.gif

כדי לראות את העקומה הזו חלה על AnimatedSize ו-TweenAnimationBuilder, צריך לטעון מחדש את האפליקציה.

206dd8d9c1fae95.gif

איך משתמשים בכלי הפיתוח כדי להפעיל אנימציות איטיות

כדי לנפות באגים באפקטים של אנימציה, Flutter DevTools מספק דרך להאט את כל האנימציות באפליקציה כדי שתוכלו לראות את האנימציה בצורה ברורה יותר.

כדי לפתוח את DevTools, מוודאים שהאפליקציה פועלת במצב ניפוי באגים ופותחים את Widget Inspector על ידי בחירה בו בDebug toolbar ב-VSCode, או על ידי לחיצה על הלחצן Open Flutter DevTools בDebug tool window ב-IntelliJ או ב-Android Studio.

3ce33dc01d096b14.png

363ae0fbcd0c2395.png

כשבודק הווידג'טים נפתח, לוחצים על הלחצן אנימציות איטיות בסרגל הכלים.

adea0a16d01127ad.png

5. שימוש באפקטים בוטים של אנימציה

בדומה לאנימציות משתמעות, אנימציות מפורשות הן אפקטים של אנימציה שנוצרו מראש, אבל במקום ערך יעד, הן מקבלות אובייקט Animation כפרמטר. לכן, הם שימושיים במצבים שבהם האנימציה כבר מוגדרת על ידי מעבר ניווט, למשל AnimatedSwitcher או AnimationController.

שימוש באפקט אנימציה בוטה

כדי להתחיל להשתמש באפקט אנימציה מפורש, צריך לעטוף את הווידג'ט Card ב-AnimatedSwitcher.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.question,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(                                 // NEW
      duration: const Duration(milliseconds: 300),           // NEW
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),                                                     // NEW
    );
  }
}

כברירת מחדל, ב-AnimatedSwitcher נעשה שימוש באפקט מעבר הדרגתי, אבל אפשר לשנות את ההגדרה הזו באמצעות הפרמטר transitionBuilder. ה-transition builder מספק את הווידג'ט הצאצא שהוענק ל-AnimatedSwitcher ואובייקט Animation. זו הזדמנות מצוינת להשתמש באנימציה מפורשת.

בסדנת הקוד הזו, האנימציה הראשונה שנשתמש בה היא SlideTransition, שמקבלת Animation<Offset> שמגדיר את ההיסט של ההתחלה והסיום שביןיהם יתבצעו תנועות של הווידג'טים הנכנסים והיוצאים.

ל-Tweens יש פונקציית עזר, animate(), שממירה כל Animation ל-Animation אחר עם ה-Tween שהוחל. המשמעות היא שאפשר להשתמש ב-Tween<Offset> כדי להמיר את ה-Animation<double> שסופק על ידי ה-AnimatedSwitcher ל-Animation<Offset>, כדי לספק אותו לווידג'ט SlideTransition.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.question,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      transitionBuilder: (child, animation) {               // Add from here...
        final curveAnimation =
            CurveTween(curve: Curves.easeInCubic).animate(animation);
        final offsetAnimation =
            Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
                .animate(curveAnimation);
        return SlideTransition(position: offsetAnimation, child: child);
      },                                                    // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

הערה: בנוסחה הזו נעשה שימוש ב-Tween.animate כדי להחיל Curve על Animation, ולאחר מכן כדי להמיר אותו מ-Tween<double> בטווח שבין 0.0 ל-1.0 ל-Tween<Offset> שמתבצעת בו מעבר מ--0.1 ל-0.0 על ציר ה-x.

לחלופין, בכיתה Animation יש פונקציה drive() שמקבלת כל Tween (או Animatable) וממירה אותו ל-Animation חדש. כך אפשר "לשרשר" טרנספורמציות Tween, וכך הקוד שייווצר יהיה תמציתי יותר:

lib/question_screen.dart

transitionBuilder: (child, animation) {
  var offsetAnimation = animation
      .drive(CurveTween(curve: Curves.easeInCubic))
      .drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
  return SlideTransition(position: offsetAnimation, child: child);
},

יתרון נוסף של שימוש באנימציות מפורטות הוא שקל לשלב ביניהן. מוסיפים אנימציה מפורשת נוספת, FadeTransition, שמשתמשת באותה אנימציה מעוקלת על ידי גלישת הווידג'ט SlideTransition.

lib/question_screen.dart

return AnimatedSwitcher(
  transitionBuilder: (child, animation) {
    final curveAnimation =
        CurveTween(curve: Curves.easeInCubic).animate(animation);
    final offsetAnimation =
        Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
            .animate(curveAnimation);
    final fadeInAnimation = curveAnimation;                            // NEW
    return FadeTransition(                                             // NEW
      opacity: fadeInAnimation,                                        // NEW
      child: SlideTransition(position: offsetAnimation, child: child), // NEW
    );                                                                 // NEW
  },

התאמה אישית של layoutBuilder

יכול להיות שתבחינו בבעיה קטנה ב-AnimationSwitcher. כשעוברים לשאלה חדשה בכרטיס השאלה, היא מוצגת במרכז המקום הזמין בזמן שהאנימציה פועלת, אבל כשהאנימציה מופסקת, הווידג'ט מוצמד לחלק העליון של המסך. כתוצאה מכך, האנימציה לא חלקה כי המיקום הסופי של כרטיס השאלה לא תואם למיקום שלו בזמן שהאנימציה פועלת.

d77de181bdde58f7.gif

כדי לפתור את הבעיה, ל-AnimatedSwitcher יש גם פרמטר layoutBuilder שאפשר להשתמש בו כדי להגדיר את הפריסה. משתמשים בפונקציה הזו כדי להגדיר את הכלי ליצירת פריסות כך שיציב את הכרטיס בחלק העליון של המסך:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return AnimatedSwitcher(
    layoutBuilder: (currentChild, previousChildren) {
      return Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          ...previousChildren,
          if (currentChild != null) currentChild,
        ],
      );
    },

הקוד הזה הוא גרסה שונה של defaultLayoutBuilder מהקלאס AnimatedSwitcher, אבל הוא משתמש ב-Alignment.topCenter במקום ב-Alignment.center.

סיכום

  • אנימציות מפורשות הן אפקטים של אנימציה שמקבלים אובייקט Animation (בניגוד ל-ImplicitlyAnimatedWidgets, שמקבלים ערך יעד ומשך זמן)
  • הכיתה Animation מייצגת אנימציה שפועלת, אבל לא מגדירה אפקט ספציפי.
  • משתמשים ב-Tween().animate או ב-Animation.drive() כדי להחיל Tweens ו-Curves (באמצעות CurveTween) על אנימציה.
  • משתמשים בפרמטר layoutBuilder של AnimatedSwitcher כדי לשנות את אופן הפריסה של הצאצאים שלו.

6. שליטה במצב של אנימציה

עד כה, כל אנימציה הופעל באופן אוטומטי על ידי המסגרת. אנימציות משתמעות פועלות באופן אוטומטי, ואפקטים של אנימציה מפורשת דורשים אנימציה כדי לפעול כראוי. בקטע הזה תלמדו איך ליצור אובייקטים של אנימציה משלכם באמצעות AnimationController, ואיך להשתמש ב-TweenSequence כדי לשלב בין Tweens.

הפעלת אנימציה באמצעות AnimationController

כדי ליצור אנימציה באמצעות AnimationController, צריך לבצע את השלבים הבאים:

  1. יצירת StatefulWidget
  2. משתמשים ב-mixin‏ SingleTickerProviderStateMixin בכיתה State כדי לספק Ticker ל-AnimationController
  3. מפעילים את AnimationController בשיטת מחזור החיים initState, ומספקים את אובייקט ה-State הנוכחי לפרמטר vsync‏ (TickerProvider).
  4. צריך לוודא שהווידג'ט נבנה מחדש בכל פעם ש-AnimationController שולח התראה למאזינים שלו, באמצעות AnimatedBuilder או באמצעות קריאה ל-listen() ול-setState באופן ידני.

יוצרים קובץ חדש, flip_effect.dart, ומעתיקים ומדביקים את הקוד הבא:

lib/flip_effect.dart

import 'dart:math' as math;

import 'package:flutter/widgets.dart';

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;

  @override
  void initState() {
    super.initState();

    _animationController =
        AnimationController(vsync: this, duration: widget.duration);

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });
  }

  @override
  void didUpdateWidget(covariant CardFlipEffect oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.child.key != oldWidget.child.key) {
      _handleChildChanged(widget.child, oldWidget.child);
    }
  }

  void _handleChildChanged(Widget newChild, Widget previousChild) {
    _previousChild = previousChild;
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      builder: (context, child) {
        return Transform(
          alignment: Alignment.center,
          transform: Matrix4.identity()
            ..rotateX(_animationController.value * math.pi),
          child: _animationController.isAnimating
              ? _animationController.value < 0.5
                  ? _previousChild
                  : Transform.flip(flipY: true, child: child)
              : child,
        );
      },
      child: widget.child,
    );
  }
}

הכיתה הזו מגדירה AnimationController ומריצה מחדש את האנימציה בכל פעם שהמסגרת קוראת ל-didUpdateWidget כדי להודיע לה שהגדרת הווידג'ט השתנתה ויכול להיות שיש ווידג'ט צאצא חדש.

ה-AnimatedBuilder מוודא שצירוף הווידג'טים ייבנה מחדש בכל פעם ש-AnimationController מודיע למאזינים שלו, והווידג'ט Transform משמש כדי להחיל אפקט של סיבוב תלת-ממדי כדי לדמות כרטיס שמתהפך.

כדי להשתמש בווידג'ט הזה, צריך לעטוף כל כרטיס תשובה בווידג'ט 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
      );
    }),
  );
}

עכשיו אפשר לטעון מחדש את האפליקציה כדי לראות את כרטיסי התשובות מתהפכים באמצעות הווידג'ט CardFlipEffect.

5455def725b866f6.gif

יכול להיות שתבחינו שהקלאס הזה דומה מאוד לאפקט אנימציה מפורש. למעשה, בדרך כלל מומלץ להרחיב את המחלקה 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, יוצרים אנימציה חדשה שמחילה את העיכוב באמצעות 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>([   // 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,
  );
}

עכשיו צריך לטעון מחדש את האפליקציה ולראות איך הכרטיסים הופכים אחד אחרי השני. כדי להתנסות, אפשר לנסות לשנות את נקודת המבט של האפקט התלת-ממדי שמוצג בווידג'ט Transform.

28b5291de9b3f55f.gif

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(                                         // NEW
        pageBuilder: (context, animation, secondaryAnimation) { // NEW
          return QuestionScreen();                              // NEW
        },                                                      // NEW
        transitionsBuilder:                                     // NEW
            (context, animation, secondaryAnimation, child) {   // NEW
          return FadeTransition(                                // NEW
            opacity: animation,                                 // NEW
            child: child,                                       // NEW
          );                                                    // NEW
        },                                                      // NEW
      ),                                                        // NEW
    );
  },
  child: Text('New Game'),
),

חבילת האנימציות מספקת אפקטים מוכנים מראש של אנימציות מתוחכמות, כמו FadeThroughTransition. מייבאים את חבילת האנימציות ומחליפים את הווידג'ט FadeTransition בווידג'ט FadeThroughTransition:

lib/home_screen.dart

import 'package;animations/animations.dart';

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
          return FadeThroughTransition(                          // NEW
            animation: animation,                                // NEW
            secondaryAnimation: secondaryAnimation,              // NEW
            child: child,                                        // NEW
          );                                                     // NEW
        },
      ),
    );
  },
  child: Text('New Game'),
),

התאמה אישית של האנימציה של תנועת החזרה

1c0558ffa3b76439.gif

'חזרה חזותית חזותית' היא תכונה חדשה ב-Android שמאפשרת למשתמש להציץ מאחורי המסלול או האפליקציה הנוכחיים כדי לראות מה נמצא מאחוריהם לפני שהוא מנווט. אנימציית ה-peek מופעלת על סמך המיקום של האצבע של המשתמש בזמן שהוא גורר אותה בחזרה על המסך.

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 מספק רק אנימציה אחת בקריאה החוזרת (callback) של ה-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(                                          // NEW
      layoutBuilder: (entries) {                                            // NEW
        return Stack(                                                       // NEW
          alignment: Alignment.topCenter,                                   // NEW
          children: entries,                                                // NEW
        );                                                                  // NEW
      },                                                                    // NEW
      transitionBuilder: (child, animation, secondaryAnimation) {           // NEW
        return FadeThroughTransition(                                       // NEW
          animation: animation,                                             // NEW
          secondaryAnimation: secondaryAnimation,                           // NEW
          child: child,                                                     // NEW
        );                                                                  // NEW
      },                                                                    // NEW
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

שימוש ב-OpenContainer

77358e5776eb104c.png

הווידג'ט OpenContainer מחבילת animations מספק אפקט אנימציה של טרנספורמציה של קונטיינר שמתרחב כדי ליצור חיבור חזותי בין שני ווידג'טים.

הווידג'ט שהחזיר closedBuilder מוצג בהתחלה, ומתרחב לווידג'ט שהחזיר openBuilder כשמקישים על המאגר או כשמתבצעת קריאה ל-callback של openContainer.

כדי לחבר את הפונקציה הלא סטטית (callback) openContainer ל-view-model, מוסיפים העברה חדשה של ה-viewModel לווידג'ט QuestionCard ושומרים פונקציית callback שתשמש להצגת המסך 'Game Over':

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 מחבילת האנימציות, מוסיפים שני שדות חדשים ל-viewModel ול-open container 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
      ),
    );
  }
}

4120f9395857d218.gif

8. מזל טוב

מצוין, הוספת אפקטים של אנימציה לאפליקציה ב-Flutter, ולמדת על הרכיבים המרכזיים של מערכת האנימציה של Flutter. באופן ספציפי, למדתם:

  • איך משתמשים ב-ImplicitlyAnimatedWidget
  • איך משתמשים ב-ExplicitlyAnimatedWidget
  • איך מחילים עקומות ותנועה מדורגת על אנימציה
  • איך משתמשים בווידג'טים מוכנים מראש של מעברים, כמו AnimatedSwitcher או PageRouteBuilder
  • איך משתמשים באפקטים מוכנים מראש של אנימציות מהחבילה animations, כמו FadeThroughTransition ו-OpenContainer
  • איך להתאים אישית את אנימציית המעבר שמוגדרת כברירת מחדל, כולל הוספת תמיכה ב'חזרה חזותית חזותית' ב-Android.

3026390ad413769c.gif

מה השלב הבא?

כדאי לעיין בחלק מהקורסים הבאים ב-Codelab:

לחלופין, אפשר להוריד את האפליקציה לדוגמה של אנימציות, שבה מוצגות טכניקות אנימציה שונות.

מקורות מידע נוספים

מקורות מידע נוספים בנושא אנימציות זמינים ב-flutter.dev:

אפשר גם לעיין במאמרים הבאים ב-Medium:

מסמכי עזרה