الصور المتحركة في Flutter

1. مقدمة

إنّ الرسوم المتحرّكة هي طريقة رائعة لتحسين تجربة المستخدم في تطبيقك، ونقل معلومات مهمة إليه، وجعل تطبيقك أكثر رونقًا وسهولة في الاستخدام.

نظرة عامة على إطار عمل الرسوم المتحركة في Flutter

يعرض Flutter تأثيرات الرسوم المتحركة من خلال إعادة إنشاء جزء من شجرة التطبيقات المصغّرة في كل إطار. وتوفّر هذه الواجهة تأثيرات مُعدّة مسبقًا وواجهات برمجة تطبيقات أخرى لتسهيل إنشاء الصور المتحركة وتركيبها.

  • الصور المتحركة الضمنية هي تأثيرات صور متحركة مُنشأة مسبقًا تعمل على تشغيل الصورة المتحركة بالكامل تلقائيًا. عندما تتغيّر قيمة الهدف للحركة، يتم تشغيل الحركة من القيمة الحالية إلى القيمة المستهدَفة، وعرض كل قيمة بينهما لكي تظهر الحركة السلسة للتطبيق المصغّر. تشمل أمثلة الصور المتحركة الضمنية AnimatedSize وAnimatedScale وAnimatedPositioned.
  • الرسوم المتحركة الواضحة هي أيضًا تأثيرات رسوم متحركة مُعدّة مسبقًا، ولكنها تتطلّب عنصرًا Animation لكي تعمل. تشمل الأمثلة SizeTransition أو ScaleTransition أو PositionedTransition.
  • الحركة هي فئة تمثّل حركة معروضة أو متوقفة، وتتألّف من قيمة تمثّل القيمة المستهدَفة التي يتم تشغيل الحركة إليها، والحالة التي تمثّل القيمة الحالية التي تعرضها الحركة على الشاشة في أي وقت. وهي فئة فرعية من Listenable، وتُعلم المستمعين بها عند تغيير الحالة أثناء تشغيل الصورة المتحركة.
  • AnimationController هي طريقة لإنشاء صورة متحركة والتحكّم في حالتها. ويمكن استخدام طرقه، مثل forward() وreset() وstop() وrepeat() للتحكّم في الصورة المتحركة بدون الحاجة إلى تحديد تأثير الصورة المتحركة المعروض، مثل الحجم أو المقياس أو الموضع.
  • تُستخدَم القيم البينية لإدراج قيم بين قيمة البداية والنهاية، ويمكن أن تمثّل أي نوع، مثل القيمة المزدوجة أو Offset أو Color.
  • تُستخدَم الخطوط المنحنية لتعديل معدّل تغيُّر مَعلمة معيّنة بمرور الوقت. عند تشغيل صورة متحركة، من الشائع تطبيق منحنى التخفيف لجعل معدّل التغيير أسرع أو أبطأ في بداية الصورة المتحركة أو نهايتها. تأخذ المنحنيات قيمة إدخال تتراوح بين 0.0 و1.0 وتُعرِض قيمة ناتجة تتراوح بين 0.0 و1.0.

التطبيق الذي ستصممه

في هذا الدليل التعليمي حول البرمجة، ستنشئ لعبة اختبار تتضمّن أسئلة خيارات متعدّدة وتوفّر تأثيرات وتقنيات متحركة متنوعة.

3026390ad413769c.gif

ستتعرّف على كيفية إجراء ما يلي:

  • إنشاء تطبيق مصغّر يغيّر حجمه ولونه بشكل متحرك
  • إنشاء تأثير قلب بطاقة ثلاثي الأبعاد
  • استخدام تأثيرات رسوم متحركة رائعة ومصمّمة مسبقًا من حزمة الرسوم المتحركة
  • إضافة إيماءة إظهار شاشة الرجوع المتاحة في أحدث إصدار من Android

ما ستتعرّف عليه

في هذا الدليل التعليمي للترميز، ستتعرّف على ما يلي:

  • كيفية استخدام التأثيرات المتحرّكة الضمنية لإنشاء صور متحركة رائعة بدون الحاجة إلى الكثير من الرموز البرمجية
  • كيفية استخدام التأثيرات المتحركة الواضحة لضبط تأثيراتك الخاصة باستخدام التطبيقات المصغّرة المتحركة المُنشأة مسبقًا، مثل AnimatedSwitcher أو AnimationController
  • كيفية استخدام AnimationController لتحديد التطبيق المصغّر الذي يعرض تأثيرًا ثلاثي الأبعاد
  • كيفية استخدام حزمة animations لعرض تأثيرات رسوم متحركة رائعة باستخدام الحد الأدنى من الإعدادات

المتطلبات

  • حزمة تطوير البرامج (SDK) من Flutter
  • بيئة تطوير متكاملة، مثل VSCode أو Android Studio / IntelliJ

2. إعداد بيئة تطوير Flutter

تحتاج إلى برنامجَين لإكمال هذا الدرس التطبيقي، وهما حزمة تطوير البرامج (SDK) من Flutter ومحرِّر.

يمكنك تشغيل ورشة التعلم البرمجي باستخدام أيّ من الأجهزة التالية:

  • جهاز Android (ننصح باستخدامه لتنفيذ ميزة "الترجيع التوقّعي" في الخطوة 7) أو iOS متصل بالكمبيوتر ومُعدّ للاستخدام في وضع المطوّر
  • محاكي iOS (يتطلب تثبيت أدوات Xcode)
  • محاكي Android (يتطلب الإعداد في "استوديو Android")
  • متصفّح (يجب استخدام Chrome لتصحيح الأخطاء)
  • جهاز كمبيوتر مكتبي يعمل بنظام التشغيل Windows أو Linux أو macOS يجب إجراء عملية التطوير على النظام الأساسي الذي تنوي نشر التطبيق عليه. لذلك، إذا أردت تطوير تطبيق متوافق مع أجهزة الكمبيوتر المكتبي التي تعمل بنظام التشغيل Windows، عليك إجراء عملية التطوير على نظام التشغيل Windows للوصول إلى سلسلة الإنشاء المناسبة. هناك متطلبات خاصة بنظام التشغيل يتم تناولها بالتفصيل على docs.flutter.dev/desktop.

التحقّق من عملية التثبيت

للتأكّد من ضبط إعدادات حزمة تطوير البرامج (SDK) من Flutter بشكلٍ صحيح، ومن تثبيت واحد على الأقل من الأنظمة الأساسية المستهدفة أعلاه، استخدِم أداة 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

يمكنك أيضًا تشغيل التطبيق وتصحيح أخطاءه باستخدام بيئة تطوير البرامج المفضّلة لديك. يمكنك الاطّلاع على مستندات 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 عندما يضغط المستخدم على الزر لعبة جديدة.

4. استخدام تأثيرات الصور المتحركة الضمنية

إنّ الرسوم المتحرّكة الضمنية هي خيار رائع في العديد من الحالات، لأنّها لا تتطلّب أيّ إعدادات خاصة. في هذا القسم، ستُعدّل التطبيق المصغّر StatusBar لكي يعرض لوحة نتائج متحركة. للعثور على تأثيرات الصور المتحركة الضمنية الشائعة، تصفَّح مستندات واجهة برمجة التطبيقات 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 كمَعلمة. الفاصل الزمني هو فئة تأخذ قيمتَين (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.
      ),
    );
  }
}

الآن، أعِد تحميل التطبيق بسرعة للاطّلاع على الصورة المتحركة الجديدة.

8b0911f4af299a60.gif

يُرجى العِلم أنّ قيمة end في ColorTween تتغيّر استنادًا إلى قيمة المَعلمة isActive. ويرجع ذلك إلى أنّ TweenAnimationBuilder يعيد تشغيل الرسوم المتحركة كلما تغيّرت قيمة Tween.end. عند حدوث ذلك، يتم تشغيل الصورة المتحركة الجديدة من قيمة الصورة المتحركة الحالية إلى القيمة النهائية الجديدة، ما يتيح لك تغيير اللون في أي وقت (حتى أثناء تشغيل الصورة المتحركة) وعرض تأثير صورة متحركة سلس بالقيم الصحيحة بين القيم.

تطبيق منحنى

يتم تشغيل هذين التأثيرَين للرسوم المتحركة بمعدّل ثابت، ولكن غالبًا ما تكون الرسوم المتحركة أكثر تشويقًا وإفادة من الناحية المرئية عند تسريعها أو إبطاءها.

يطبّق Curve دالة تمويه، والتي تحدّد معدّل تغيُّر مَعلمة معيّنة بمرور الوقت. يتضمّن Flutter مجموعة من منحنيات التخفيف المُنشأة مسبقًا في فئة Curves، مثل easeIn أو easeOut.

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

تقدّم هذه المخطّطات البيانية (المتوفّرة في 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 طريقة لإبطاء جميع الصور المتحركة في تطبيقك، حتى تتمكّن من رؤية الصورة المتحركة بوضوح أكبر.

لفتح "أدوات المطوّر"، تأكَّد من تشغيل التطبيق في وضع تصحيح الأخطاء، وافتح أداة فحص التطبيقات المصغّرة من خلال اختيارها في شريط أدوات تصحيح الأخطاء في VSCode أو من خلال اختيار الزر فتح أدوات المطوّر في Flutter في نافذة أداة تصحيح الأخطاء في 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. يقدّم أداة إنشاء الانتقالات التطبيق المصغّر الثانوي الذي تم تمريره إلى AnimatedSwitcher وعنصر Animation. هذه فرصة رائعة لاستخدام صورة متحركة واضحة.

في هذا الدرس التطبيقي حول الترميز، أول حركة معروضة بوضوح سنستخدمها هي SlideTransition، التي تأخذ Animation<Offset> الذي يحدّد المدة الزمنية التي ستتحرك خلالها التطبيقات المصغّرة الواردة والصادرة بين بداية ونهاية الفاصل الزمني.

تتضمّن العناصر الانتقالية دالة مساعدة، وهي animate()، التي تحوّل أي Animation إلى Animation آخر مع تطبيق العنصر الانتقالي. وهذا يعني أنّه يمكن استخدام 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 جديد. يتيح ذلك "تسلسل" العناصر الانتقالية، ما يجعل الرمز الناتج أكثر إيجازًا:

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.

ملخّص

  • الرسوم المتحركة الصريحة هي تأثيرات للحركة تأخذ عنصرًا للحركة (على عكس ImplicitlyAnimatedWidgets التي تأخذ قيمة ومدة مستهدَفَين).
  • تمثّل فئة Animation صورة متحركة قيد التشغيل، ولكنها لا تحدّد تأثيرًا معيّنًا.
  • استخدِم Tween().animate أو Animation.drive() لتطبيق Tweens وCurves (باستخدام CurveTween) على صورة متحركة.
  • استخدِم مَعلمة layoutBuilder في AnimatedSwitcher لتعديل طريقة عرض عناصره الفرعية.

6- التحكّم في حالة صورة متحركة

حتى الآن، كان إطار العمل يشغّل كلّ صورة متحركة تلقائيًا. يتم تشغيل الصور المتحركة الضمنية تلقائيًا، وتتطلّب تأثيرات الصور المتحركة الصريحة استخدام صورة متحركة حتى تعمل بشكل صحيح. في هذا القسم، ستتعرّف على كيفية إنشاء عناصر متحركة باستخدام AnimationController، واستخدام TweenSequence لدمج Tweens معًا.

تشغيل صورة متحركة باستخدام AnimationController

لإنشاء صورة متحركة باستخدام AnimationController، عليك اتّباع الخطوات التالية:

  1. إنشاء عنصر تحكم StatefulWidget
  2. استخدِم عنصر 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 مباشرةً لتنفيذ إصدارك الخاص. بما أنّ هذه الفئة تحتاج إلى تخزين التطبيق المصغّر السابق في حالته، يجب استخدام StatefulWidget. للتعرّف على مزيد من المعلومات عن إنشاء تأثيرات متحركة صريحة، يمكنك الاطّلاع على مستندات واجهة برمجة التطبيقات 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، أحدهما يحتوي على 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 بالصورة المتحركة الجديدة التي تتضمّن تأخيرًا في طريقة الإنشاء.

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 تتيح لك تخصيص الرسوم المتحركة للانتقال. يتيح لك هذا الإجراء إلغاء طلب الاستدعاء transitionBuilder الذي يقدّم عنصرَي Animation يمثّلان الصورة المتحركة الواردة والصادرة التي يشغّلها المُستكشف.

لتخصيص الرسم المتحرك للانتقال، استبدِل 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 تتيح للمستخدم الاطّلاع على المسار أو التطبيق الحاليَين لمعرفة ما يليهما قبل الانتقال إليهما. يتم تشغيل الرسوم المتحركة للاطّلاع من خلال موقع إصبع المستخدم أثناء سحبه للخلف على الشاشة.

يتيح Flutter ميزة "الرجوع التوقّعي" للنظام من خلال تفعيل الميزة على مستوى النظام عندما لا يتوفّر لفلاتر مسارات لعرضها في حزمة التنقّل، أو بعبارة أخرى، عندما يؤدي الرجوع إلى الخروج من التطبيق. يعالج النظام هذا التأثير المتحرك وليس Flutter نفسه.

تتيح Flutter أيضًا ميزة "الرجوع التوقّعي" عند الانتقال بين المسارات داخل تطبيق Flutter. يرصد عنصر PageTransitionsBuilder الخاص المُسمى PredictiveBackPageTransitionsBuilder إيماءات الرجوع التوقّعي في النظام ويشغّل عملية انتقال الصفحة وفقًا لتقدّم الإيماءة.

لا تتوفّر ميزة "الرجوع التوقّعي" إلا في الإصدار U من Android والإصدارات الأحدث، ولكن سيستخدم 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

77358e5776eb104c.png

يقدّم التطبيق المصغّر OpenContainer من حزمة animations تأثيرًا متحركًا لتحويل الحاوية يتوسّع لإنشاء رابط مرئي بين تطبيقَين مصغّرَين.

يتم عرض التطبيق المصغّر الذي يعرضه closedBuilder في البداية، ويتم توسيعه ليشمل التطبيق المصغّر الذي يعرضه openBuilder عند النقر على الحاوية أو عند استدعاء دالة الاستدعاء openContainer.

لربط دالة الاستدعاء openContainer بنموذج العرض، أضِف تمريرة جديدة لنموذج العرض في التطبيق المصغّر 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 من حزمة الرسوم المتحرّكة، مع إضافة حقلَين جديدَين لنموذج العرض وطريقة استدعاء الحاوية المفتوحة:

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

الخطوة التالية

اطّلِع على بعض هذه الدروس التطبيقية حول الترميز:

أو يمكنك تنزيل تطبيق نماذج الصور المتحركة الذي يعرض أساليب مختلفة للصور المتحركة.

مزيد من المعلومات

يمكنك العثور على المزيد من مراجع الرسوم المتحركة على flutter.dev:

يمكنك أيضًا الاطّلاع على المقالات التالية على Medium:

مستندات المرجع