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

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

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

إنشاء تطبيق لوحة النتائج المصغَّر غير المتحرّك
أنشئ ملفًا جديدًا باسم lib/scoreboard.dart باستخدام الرمز التالي:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color: score < i + 1
? Colors.grey.shade400
: Colors.yellow.shade700,
),
],
),
);
}
}
بعد ذلك، أضِفوا أداة Scoreboard إلى العناصر التابعة لأداة StatusBar، مع استبدال أدوات Text التي كانت تعرض النتيجة وإجمالي عدد الأسئلة. من المفترض أن يضيف المحرّر تلقائيًا import "scoreboard.dart" المطلوب في أعلى الملف.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
تعرض هذه الأداة رمز نجمة لكل سؤال. عند الإجابة عن سؤال بشكل صحيح، تضيء نجمة أخرى على الفور بدون أي تأثيرات مرئية. في الخطوات التالية، ستساعد في إعلام المستخدم بأنّ نتيجته قد تغيّرت من خلال تحريك حجمها ولونها.
استخدام تأثير صورة متحركة ضمني
أنشئ تطبيقًا مصغّرًا جديدًا باسم AnimatedStar يستخدم تطبيقًا مصغّرًا AnimatedScale لتغيير قيمة scale من 0.5 إلى 1.0 عندما يصبح النجم نشطًا:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar(isActive: score > i), // Edit this line.
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
الآن، عندما يجيب المستخدم عن سؤال بشكل صحيح، يغيّر التطبيق المصغّر AnimatedStar حجمه باستخدام رسم متحرك ضمني. لا يتم تحريك color في Icon هنا، بل يتم تحريك scale فقط، ويتم ذلك من خلال أداة AnimatedScale.

استخدام Tween لاحتساب معدّل التغيّر بين قيمتين
لاحظ أنّ لون الأداة AnimatedStar يتغيّر فورًا بعد تغيير الحقل isActive إلى "صحيح".
لتحقيق تأثير الألوان المتحركة، يمكنك محاولة استخدام أداة 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.
},
),
);
}
}
الآن، أعِد تحميل التطبيق سريعًا للاطّلاع على الصورة المتحركة الجديدة.

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


تقدّم هذه المخططات (المتاحة في صفحة مستندات Curves API) فكرة عن طريقة عمل المنحنيات. تحوّل المنحنيات قيمة إدخال تتراوح بين 0.0 و1.0 (معروضة على المحور x) إلى قيمة إخراج تتراوح بين 0.0 و1.0 (معروضة على المحور y). تعرض هذه المخططات البيانية أيضًا معاينة لشكل تأثيرات الرسوم المتحركة المختلفة عند استخدام منحنى تغيير السرعة.
أنشِئ حقلًا جديدًا في AnimatedStar باسم _curve ومرِّره كمعلَمة إلى أداتَي AnimatedScale وTweenAnimationBuilder.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(Icons.star, size: 50, color: value);
},
),
);
}
}
في هذا المثال، يوفّر المنحنى elasticOut تأثيرًا مبالغًا فيه للنابض يبدأ بحركة نابضية وينتهي بشكل متوازن.

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

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


بعد فتح أداة فحص التطبيقات المصغّرة، انقر على الزر الرسوم المتحركة البطيئة في شريط الأدوات.

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

لحلّ هذه المشكلة، تحتوي 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، عليك اتّباع الخطوات التالية:
- إنشاء
StatefulWidget - استخدِم mixin
SingleTickerProviderStateMixinفي فئةStateلتوفيرTickerإلىAnimationController - ابدأ
AnimationControllerفي طريقة مراحل النشاطinitState، وقدِّم عنصرStateالحالي إلى المَعلمةvsync(TickerProvider). - تأكَّد من إعادة إنشاء التطبيق المصغّر كلما أرسل
AnimationControllerإشعارًا إلى المستمعين، إما باستخدامAnimatedBuilderأو عن طريق استدعاءlisten()وsetStateيدويًا.
أنشئ ملفًا جديدًا باسم flip_effect.dart وانسخ الرمز التالي والصقه:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
تُعدّ هذه الفئة AnimationController وتعيد تشغيل الحركة كلما استدعى إطار العمل didUpdateWidget لإعلامها بأنّ إعدادات التطبيق المصغّر قد تغيّرت، وقد يكون هناك تطبيق مصغّر جديد تابع.
تضمن AnimatedBuilder إعادة إنشاء شجرة عناصر واجهة المستخدم كلما أرسل AnimationController إشعارات إلى المستمعين، ويتم استخدام عنصر واجهة المستخدم Transform لتطبيق تأثير دوران ثلاثي الأبعاد لمحاكاة قلب البطاقة.
لاستخدام هذه الأداة، عليك تضمين كل بطاقة إجابة في CardFlipEffect أداة. احرص على تقديم key إلى التطبيق المصغّر Card:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
أعِد الآن تحميل التطبيق سريعًا لترى بطاقات الإجابات وهي تنقلب باستخدام الأداة CardFlipEffect.

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

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'),
),
تخصيص الصورة المتحركة لإيماءة إظهار شاشة الرجوع

"إيماءة إظهار شاشة الرجوع" هي ميزة جديدة في 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

توفّر الأداة 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
),
);
}
}

8. تهانينا
تهانينا، لقد نجحت في إضافة تأثيرات الصور المتحركة إلى تطبيق Flutter، وتعرّفت على المكوّنات الأساسية لنظام الصور المتحركة في Flutter. على وجه التحديد، تعلّمت ما يلي:
- كيفية استخدام
ImplicitlyAnimatedWidget - كيفية استخدام
ExplicitlyAnimatedWidget - كيفية تطبيق
CurvesوTweensعلى صورة متحركة - كيفية استخدام أدوات انتقال جاهزة، مثل
AnimatedSwitcherأوPageRouteBuilder - كيفية استخدام تأثيرات الصور المتحركة الجذابة المُعدّة مسبقًا من حزمة
animations، مثلFadeThroughTransitionوOpenContainer - كيفية تخصيص الصورة المتحركة التلقائية للانتقال، بما في ذلك إضافة إمكانية استخدام ميزة "الرجوع التوقّعي" على Android

ما هي الخطوات التالية؟
اطّلِع على بعض دروس الترميز التطبيقية هذه:
- تطوير تصميم لتطبيق سريع الاستجابة ومتحرّك باستخدام Material 3
- إنشاء تأثيرات انتقالية رائعة باستخدام Material Motion على Flutter
- تحويل تطبيق Flutter من ممل إلى جميل
يمكنك أيضًا تنزيل نموذج تطبيق الصور المتحركة الذي يعرض تقنيات مختلفة للصور المتحركة.
محتوى إضافي للقراءة
يمكنك العثور على المزيد من مراجع الصور المتحركة على flutter.dev:
- مقدمة عن الصور المتحركة
- برنامج تعليمي حول الصور المتحركة (برنامج تعليمي)
- الصور المتحركة الضمنية (برنامج تعليمي)
- تحريك خصائص حاوية (كتاب الطبخ)
- تلاشي ظهور التطبيق المصغّر (كتاب الطبخ)
- الصور المتحركة في الصفحة الرئيسية
- تحريك عملية الانتقال إلى مسار صفحة (كتاب الطبخ)
- تحريك أداة باستخدام محاكاة فيزيائية (كتاب الطبخ)
- الصور المتحركة المتداخلة
- التطبيقات المصغّرة للصور المتحركة (كتالوج التطبيقات المصغّرة)
أو يمكنك الاطّلاع على هذه المقالات على Medium:
- نظرة متعمّقة على الصور المتحركة
- الصور المتحركة الضمنية المخصّصة في Flutter
- إدارة الصور المتحركة باستخدام Flutter وFlux / Redux
- كيفية اختيار أداة الرسوم المتحركة المناسبة لك في Flutter
- الصور المتحركة الاتجاهية مع صور متحركة صريحة مدمجة
- أساسيات الصور المتحركة في Flutter باستخدام الصور المتحركة الضمنية
- متى يجب استخدام AnimatedBuilder أو AnimatedWidget؟