تخطَّ إلى المحتوى

الحركة في Flutter: AnimatedContainer والحركات الجاهزة و Hero

الدرس 24 من 33 · Flutter

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

الحركات الضمنية: غيّر القيمة والباقي تلقائي

Section titled “الحركات الضمنية: غيّر القيمة والباقي تلقائي”

Flutter فيه مجموعة Widgets تبدأ أسماؤها بكلمة Animated. فكرتها واحدة: تعطيها القيمة الجديدة، وهي تتحرك إليها من القيمة القديمة وحدها.

تخيّل الفرق بين مصعد وانتقال فوري: في الحالتين تصل إلى الطابق الخامس، لكن المصعد يأخذك عبر الطوابق بسلاسة. أنت تضغط الزر فقط، والمصعد يتولى الرحلة.

نسخة متحركة من Container. أي خاصية تتغير فيه، مثل الحجم أو اللون أو الزوايا أو المسافات، تنتقل بنعومة:

AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
width: _big ? 220 : 120,
height: _big ? 220 : 120,
decoration: BoxDecoration(
color: _big ? Colors.indigo : Colors.amber,
borderRadius: BorderRadius.circular(_big ? 32 : 12),
),
)
  • duration: مدة الحركة، وهي إجبارية. 400 جزء من الألف من الثانية أقل من نصف ثانية.
  • curve: منحنى الحركة، وسنشرحه بعد قليل.
  • الباقي خصائص Container العادية، لكن قيمها تعتمد على المتغير _big.

عندما تستدعي setState وتغيّر _big، لا يقفز الصندوق إلى شكله الجديد، بل يكبر ويتلوّن وتتدور زواياه معاً خلال 400 ملّي ثانية.

المنحنى يحدد توزيع السرعة خلال الحركة. الأشياء في الواقع لا تتحرك بسرعة ثابتة: السيارة تتسارع ثم تتباطأ قبل أن تقف.

المنحنى الإحساس
Curves.linear سرعة ثابتة، تبدو آلية
Curves.easeInOut بداية هادئة ثم تسارع ثم نهاية هادئة. الاختيار الآمن
Curves.easeOut بداية سريعة ونهاية هادئة، مناسب لعنصر يدخل الشاشة
Curves.easeIn بداية هادئة ونهاية سريعة، مناسب لعنصر يخرج
Curves.bounceOut يرتد في النهاية مثل كرة
Curves.elasticOut يتجاوز الهدف ثم يرجع مثل زنبرك

AnimatedOpacity: الظهور والاختفاء

Section titled “AnimatedOpacity: الظهور والاختفاء”
AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _visible ? 1 : 0,
child: const Text('أظهر وأختفي بنعومة'),
)

opacity رقم من 0 (شفاف تماماً) إلى 1 (ظاهر تماماً). انتبه: العنصر الشفاف ما زال موجوداً ويحتل مكانه ويستجيب للضغط. إذا أردت أن يختفي ويترك مكانه لغيره، فهذا عمل AnimatedSwitcher.

AnimatedSwitcher: تبديل عنصر بآخر

Section titled “AnimatedSwitcher: تبديل عنصر بآخر”

عندما يُستبدل Widget بآخر، يجعل AnimatedSwitcher القديم يتلاشى والجديد يظهر:

AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
'$_count',
key: ValueKey(_count),
style: const TextStyle(fontSize: 48),
),
)

السطر key: ValueKey(_count) هو سر العمل. AnimatedSwitcher يتحرك فقط إذا رأى أن الابن تغيّر. والنص القديم والنص الجديد كلاهما من نوع Text، فيظنهما نفس العنصر. المفتاح المختلف يخبره أنهما عنصران مختلفان، فيشغّل الحركة.

الـ Widget ماذا يحرّك
AnimatedPadding المسافة الداخلية
AnimatedAlign المحاذاة: ينزلق العنصر من زاوية إلى أخرى
AnimatedPositioned الموضع داخل Stack
AnimatedDefaultTextStyle حجم الخط ولونه
AnimatedScale التكبير والتصغير
AnimatedRotation الدوران
AnimatedCrossFade التبديل بين عنصرين مع تغيّر الحجم

كلها تعمل بنفس القاعدة: duration، ثم القيمة التي تتغير مع setState.

Hero: عنصر يطير بين شاشتين

Section titled “Hero: عنصر يطير بين شاشتين”

تضغط على صورة صغيرة في قائمة، فتفتح شاشة التفاصيل، وتطير الصورة من مكانها الصغير إلى مكانها الكبير. هذا التأثير يحتاج فقط أن تلف العنصر في الشاشتين بـ Hero له نفس الوسم:

const Hero(
tag: 'product-7',
child: Icon(Icons.headphones, size: 48),
)

وفي شاشة التفاصيل:

const Hero(
tag: 'product-7',
child: Icon(Icons.headphones, size: 200),
)

عند التنقل بـ Navigator.push، يبحث Flutter عن Hero في الشاشتين له نفس tag، ويحرّك العنصر بينهما. القاعدة الوحيدة: الوسم يجب أن يكون فريداً داخل كل شاشة. في القوائم استخدم رقم العنصر التعريفي جزءاً من الوسم.

وماذا عن الحركات المعقدة؟

Section titled “وماذا عن الحركات المعقدة؟”

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

lib/animations_demo.dart
class AnimationsDemo extends StatefulWidget {
const AnimationsDemo({super.key});
@override
State<AnimationsDemo> createState() => _AnimationsDemoState();
}
class _AnimationsDemoState extends State<AnimationsDemo> {
bool _big = false;
bool _visible = true;
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('الحركة')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
GestureDetector(
onTap: () => setState(() => _big = !_big),
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
width: _big ? 220 : 120,
height: _big ? 160 : 120,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _big ? Colors.indigo : Colors.amber,
borderRadius: BorderRadius.circular(_big ? 32 : 12),
),
child: const Text('اضغطني'),
),
),
const SizedBox(height: 24),
AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: _visible ? 1 : 0,
child: const Text('نص يظهر ويختفي', style: TextStyle(fontSize: 20)),
),
TextButton(
onPressed: () => setState(() => _visible = !_visible),
child: Text(_visible ? 'إخفاء' : 'إظهار'),
),
const SizedBox(height: 24),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
'$_count',
key: ValueKey(_count),
style: const TextStyle(fontSize: 48),
),
),
FilledButton(
onPressed: () => setState(() => _count++),
child: const Text('زد العدّاد'),
),
],
),
),
);
}
}

ثلاث تجارب في شاشة واحدة:

  1. اضغط الصندوق الأصفر: يكبر، ويتحول إلى الأزرق، وتتدور زواياه، كل ذلك معاً.
  2. اضغط «إخفاء»: يتلاشى النص، ويتغير الزر إلى «إظهار».
  3. اضغط «زد العدّاد»: يتلاشى الرقم القديم ويظهر الجديد مكانه.

وكل هذا بدون متحكم حركة واحد. فقط setState وقيم تتغير.

  1. شغّل اللوحة وجرّب الحركات الثلاث.
  2. غيّر curve في الصندوق إلى Curves.bounceOut ثم Curves.elasticOut.
  3. اجعل المدة 3 ثوانٍ وراقب الحركة بالتصوير البطيء، ثم أعدها.
  4. تحدٍّ: أضف دائرة داخل Stack تنزلق بين اليمين واليسار بـ AnimatedAlign عند الضغط على زر.
  • نسيان key في ابن AnimatedSwitcher: يتغير النص بدون أي حركة، لأن المبدّل يظن أن العنصر لم يتغير.
  • مدة طويلة جداً: حركة تستغرق ثانيتين تبدو جميلة أول مرة ومملة في المرة العاشرة.
  • وسم Hero مكرر في نفس الشاشة: يتوقف التطبيق بخطأ «multiple heroes that share the same tag».

سؤال

كيف تبدأ الحركة في AnimatedContainer؟

سؤال

لماذا نعطي ابن AnimatedSwitcher مفتاحاً يتغير مع القيمة؟

  • الحركات الضمنية تبدأ باسم Animated: تغيّر القيمة بـ setState فتتحرك تلقائياً.
  • duration إجبارية، والأفضل بين 200 و 400 ملّي ثانية، و curve تحدد توزيع السرعة.
  • AnimatedContainer للحجم واللون والشكل، و AnimatedOpacity للشفافية.
  • AnimatedSwitcher لتبديل عنصر بآخر، ويحتاج key مختلفاً.
  • Hero بنفس tag في شاشتين يجعل العنصر يطير بينهما.