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

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

لمحة عن هذا الدرس التطبيقي حول الترميز

subjectتاريخ التعديل الأخير: يونيو 3, 2025
account_circleتأليف: John Ryan, Justin McCandless

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 لعرض تأثيرات صور متحركة رائعة باستخدام الحد الأدنى من الإعدادات

المتطلبات

  • حزمة تطوير البرامج Flutter SDK
  • بيئة تطوير متكاملة، مثل 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

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

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 إلى true.

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

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

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

اختَر التطبيق المصغّر 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);     // And modify this line.
        },
      ),
    );
  }
}

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

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

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

لتخصيص الرسم المتحرك للانتقال، استبدِل 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. يرصد 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 بنموذج العرض، أضِف تمريرة جديدة viewModel إلى التطبيق المصغّر QuestionCard واحفظ طلب إعادة اتصال سيتم استخدامه لعرض شاشة "انتهت اللعبة":

lib/question_screen.dart

class QuestionScreen extends StatefulWidget {
  const QuestionScreen({super.key});

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  late final QuizViewModel viewModel = QuizViewModel(
    onGameOver: _handleGameOver,
  );
  VoidCallback? _showGameOverScreen;                                    // NEW

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        return Scaffold(
          appBar: AppBar(
            actions: [
              TextButton(
                onPressed:
                    viewModel.hasNextQuestion && viewModel.didAnswerQuestion
                    ? () {
                        viewModel.getNextQuestion();
                      }
                    : null,
                child: const Text('Next'),
              ),
            ],
          ),
          body: Center(
            child: Column(
              children: [
                QuestionCard(                                           // NEW
                  onChangeOpenContainer: _handleChangeOpenContainer,    // NEW
                  question: viewModel.currentQuestion?.question,        // NEW
                  viewModel: viewModel,                                 // NEW
                ),                                                      // NEW
                Spacer(),
                AnswerCards(
                  onTapped: (index) {
                    viewModel.checkAnswer(index);
                  },
                  answers: viewModel.currentQuestion?.possibleAnswers ?? [],
                  correctAnswer: viewModel.didAnswerQuestion
                      ? viewModel.currentQuestion?.correctAnswer
                      : null,
                ),
                StatusBar(viewModel: viewModel),
              ],
            ),
          ),
        );
      },
    );
  }

  void _handleChangeOpenContainer(VoidCallback openContainer) {        // NEW
    _showGameOverScreen = openContainer;                               // NEW
  }                                                                    // NEW

  void _handleGameOver() {                                             // NEW
    if (_showGameOverScreen != null) {                                 // NEW
      _showGameOverScreen!();                                          // NEW
    }                                                                  // NEW
  }                                                                    // NEW
}

أضِف تطبيقًا مصغّرًا جديدًا، GameOverScreen:

lib/question_screen.dart

class GameOverScreen extends StatelessWidget {
  final QuizViewModel viewModel;
  const GameOverScreen({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(automaticallyImplyLeading: false),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Scoreboard(
              score: viewModel.score,
              totalQuestions: viewModel.totalQuestions,
            ),
            Text('You Win!', style: Theme.of(context).textTheme.displayLarge),
            Text(
              'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
              style: Theme.of(context).textTheme.displaySmall,
            ),
            ElevatedButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.popUntil(context, (route) => route.isFirst);
              },
            ),
          ],
        ),
      ),
    );
  }
}

في التطبيق المصغّر QuestionCard، استبدِل Card بتطبيق مصغّر OpenContainer من حزمة animations، مع إضافة حقلَين جديدَين لدعوة viewModel وفتح الحاوية:

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:

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