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

1. مقدمة

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

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

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

  • الصور المتحركة الضمنية هي تأثيرات صور متحركة مُعدّة مسبقًا يتم تشغيل الصورة المتحركة بأكملها تلقائيًا. عندما تتغيّر قيمة الهدف للرسم المتحرّك، يتم تشغيل الرسم المتحرّك من القيمة الحالية إلى قيمة الهدف، ويتم عرض كل قيمة بينهما حتى يتحرّك العنصر بسلاسة. تشمل أمثلة الصور المتحركة الضمنية AnimatedSize وAnimatedScale وAnimatedPositioned.
  • الصور المتحركة الواضحة هي أيضًا تأثيرات صور متحركة مُعدّة مسبقًا، ولكنها تتطلّب عنصر Animation لكي تعمل. تشمل الأمثلة SizeTransition أو ScaleTransition أو PositionedTransition.
  • Animation هي فئة تمثّل رسومًا متحركة قيد التشغيل أو متوقفة، وتتألف من قيمة تمثّل القيمة المستهدَفة التي يتم تشغيل الرسوم المتحركة عليها، وحالة تمثّل القيمة الحالية التي تعرضها الرسوم المتحركة على الشاشة في أي وقت. وهي فئة فرعية من 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
  • بيئة تطوير متكاملة (IDE)، مثل VSCode أو "استوديو Android" أو IntelliJ

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

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

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

  • جهاز Android فعلي (يُنصح به لتنفيذ إيماءة إظهار شاشة الرجوع في الخطوة 7) أو جهاز iOS متصل بجهاز الكمبيوتر وتم ضبطه على وضع مطور البرامج
  • محاكي iOS (يتطلّب تثبيت أدوات Xcode)
  • Android Emulator (يتطلّب الإعداد في "استوديو 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 الرسمية للحصول على مزيد من المعلومات.

الاطّلاع على الرمز

التطبيق التجريبي هو لعبة اختبار من متعدد تتألف من شاشتين تتبعان نمط تصميم Model-View-View-Model أو MVVM. يستخدم QuestionScreen (العرض) فئة QuizViewModel (نموذج العرض) لطرح أسئلة متعدّدة الخيارات على المستخدم من فئة QuestionBank (النموذج).

  • home_screen.dart: تعرض شاشة تتضمّن زر لعبة جديدة
  • main.dart: يضبط MaterialApp لاستخدام التصميم المتعدد الأبعاد 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(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.

84aec4776e70b870.gif

استخدام Tween لاحتساب معدّل التغيّر بين قيمتين

لاحظ أنّ لون الأداة AnimatedStar يتغيّر فورًا بعد تغيير الحقل isActive إلى "صحيح".

لتحقيق تأثير الألوان المتحركة، يمكنك محاولة استخدام أداة AnimatedContainer (وهي فئة فرعية أخرى من ImplicitlyAnimatedWidget)، لأنّها يمكنها تحريك جميع سماتها تلقائيًا، بما في ذلك اللون. للأسف، يجب أن يعرض التطبيق المصغّر رمزًا وليس حاوية.

يمكنك أيضًا تجربة AnimatedIcon، الذي يطبّق تأثيرات انتقالية بين أشكال الرموز. ولكن لا يتوفّر تنفيذ تلقائي لرمز النجمة في فئة AnimatedIcons.

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

اختَر أداة Icon واستخدِم الإجراء السريع "التضمين في أداة إنشاء" في بيئة التطوير المتكاملة، ثم غيِّر الاسم إلى 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.
        },
      ),
    );
  }
}

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

8b0911f4af299a60.gif

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

تطبيق منحنى

يعمل كلا تأثيرَي الرسوم المتحركة هذين بمعدّل ثابت، ولكن غالبًا ما تكون الرسوم المتحركة أكثر جاذبية وإفادة من الناحية المرئية عندما تزداد سرعتها أو تنخفض.

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

5dabe68d1210b8a1.gif

3a9e7490c594279a.gif

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

8f84142bff312373.gif

أعِد تحميل التطبيق سريعًا للاطّلاع على هذا المنحنى المطبَّق على AnimatedSize وTweenAnimationBuilder.

206dd8d9c1fae95.gif

استخدام "أدوات مطوّري البرامج" لتفعيل الصور المتحركة البطيئة

لتصحيح أي خطأ في تأثير الصورة المتحركة، توفّر "أدوات مطوّري البرامج في Flutter" طريقة لإبطاء جميع الصور المتحركة في تطبيقك، ما يتيح لك رؤية الصورة المتحركة بشكل أكثر وضوحًا.

لفتح DevTools، تأكَّد من أنّ التطبيق يعمل في وضع تصحيح الأخطاء، وافتح أداة فحص العناصر من خلال اختيارها في شريط أدوات تصحيح الأخطاء في VSCode أو من خلال النقر على الزر فتح Flutter DevTools في نافذة أدوات تصحيح الأخطاء في IntelliJ / استوديو Android.

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 لتحويل Animation المقدَّم من AnimatedSwitcher إلى Animation، ليتم تقديمه إلى أداة SlideTransition.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

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

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

يُرجى العِلم أنّ هذا الإجراء يستخدم Tween.animate لتطبيق Curve على Animation، ثم لتحويله من Tween يتراوح بين 0.0 و1.0 إلى Tween ينتقل من -0.1 إلى 0.0 على المحور x.

بدلاً من ذلك، تحتوي فئة Animation على الدالة drive() التي تأخذ أي Tween (أو Animatable) وتحوّله إلى Animation جديد. يسمح ذلك "بربط" الصور المتحركة، ما يجعل الرمز الناتج أكثر اختصارًا:

lib/question_screen.dart

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

من المزايا الأخرى لاستخدام الرسوم المتحركة الواضحة أنّه يمكن دمجها معًا. أضِف حركة صريحة أخرى، FadeTransition تستخدم الحركة المنحنية نفسها من خلال تضمين أداة SlideTransition.

lib/question_screen.dart

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

تخصيص layoutBuilder

قد تلاحظ مشكلة بسيطة في AnimationSwitcher. عندما ينتقل QuestionCard إلى سؤال جديد، يعرضه في وسط المساحة المتاحة أثناء تشغيل الصورة المتحركة، ولكن عند إيقاف الصورة المتحركة، يتم تثبيت الأداة في أعلى الشاشة. يؤدي ذلك إلى حركة متقطّعة لأنّ الموضع النهائي لبطاقة السؤال لا يتطابق مع الموضع أثناء تشغيل الحركة.

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 التي تستخدم عنصرًا مستهدفًا value وduration)
  • يمثّل الصف Animation صورة متحركة قيد التشغيل، ولكنّه لا يحدّد تأثيرًا معيّنًا.
  • استخدِم Tween().animate أو Animation.drive() لتطبيق Tweens وCurves (باستخدام CurveTween) على صورة متحركة.
  • استخدِم المَعلمة layoutBuilder الخاصة بالعنصر AnimatedSwitcher لتعديل طريقة ترتيب العناصر الفرعية.

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

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

تشغيل صورة متحركة باستخدام 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 إلى التطبيق المصغّر 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.

5455def725b866f6.gif

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

28b5291de9b3f55f.gif

7. استخدام انتقالات مخصّصة للتنقّل

حتى الآن، رأينا كيفية تخصيص التأثيرات على شاشة واحدة، ولكن هناك طريقة أخرى لاستخدام الصور المتحركة وهي استخدامها للانتقال بين الشاشات. في هذا القسم، ستتعرّف على كيفية تطبيق تأثيرات الصور المتحركة على انتقالات الشاشة باستخدام تأثيرات الصور المتحركة المضمّنة وتأثيرات الصور المتحركة الجذابة المُعدّة مسبقًا والمقدَّمة من حزمة animations الرسمية على pub.dev.

إضافة مؤثرات حركة إلى عملية انتقال في التنقّل

الفئة PageRouteBuilder هي Route تتيح لك تخصيص الرسوم المتحركة للانتقال. تتيح لك هذه السمة إلغاء معاودة الاتصال 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'),
),

تخصيص الصورة المتحركة لإيماءة إظهار شاشة الرجوع

1c0558ffa3b76439.gif

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

يتوافق Flutter مع ميزة "إيماءة إظهار شاشة الرجوع" في النظام من خلال تفعيل الميزة على مستوى النظام عندما لا يتضمّن 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),
        pageTransitionsTheme: PageTransitionsTheme(
          builders: {
            TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),  // NEW
            TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(),         // NEW
            TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(),       // NEW
            TargetPlatform.windows: FadeThroughPageTransitionsBuilder(),     // NEW
            TargetPlatform.linux: FadeThroughPageTransitionsBuilder(),       // NEW
          },
        ),
      ),
      home: HomeScreen(),
    );
  }
}

يمكنك الآن تغيير Navigator.push() إلى MaterialPageRoute.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      MaterialPageRoute(                                        // Add from here...
        builder: (context) {
          return const QuestionScreen();
        },
      ),                                                        // To here.
    );
  },
  child: Text('New Game'),
),

استخدِم FadeThroughTransition لتغيير السؤال الحالي

لا يوفّر التطبيق المصغّر AnimatedSwitcher سوى Animation واحد في دالة رد الاتصال الخاصة بأداة الإنشاء. لحلّ هذه المشكلة، توفّر حزمة animations PageTransitionSwitcher.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

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

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(                              // Add from here...
      layoutBuilder: (entries) {
        return Stack(alignment: Alignment.topCenter, children: entries);
      },
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          child: child,
        );
      },                                                        // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

استخدام OpenContainer

77358e5776eb104c.png

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

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

لربط openContainer callback بـ View-Model، أضِف viewModel جديدًا إلى الأداة QuestionCard وخزِّن callback سيتم استخدامه لعرض شاشة "انتهت اللعبة":

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 ودالة معاودة الاتصال الخاصة بالحاوية المفتوحة:

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
  • كيفية تطبيق Curves وTweens على صورة متحركة
  • كيفية استخدام أدوات انتقال جاهزة، مثل AnimatedSwitcher أو PageRouteBuilder
  • كيفية استخدام تأثيرات الصور المتحركة الجذابة المُعدّة مسبقًا من حزمة animations، مثل FadeThroughTransition وOpenContainer
  • كيفية تخصيص الصورة المتحركة التلقائية للانتقال، بما في ذلك إضافة إمكانية استخدام ميزة "الرجوع التوقّعي" على Android

3026390ad413769c.gif

ما هي الخطوات التالية؟

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

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

محتوى إضافي للقراءة

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

أو يمكنك الاطّلاع على هذه المقالات على Medium:

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