الرسوم المتحركة وتصميم الحركة

الرسوم المتحركة الضمنية: AnimatedContainer و AnimatedPadding

15 دقيقة الدرس 2 من 13

الرسوم المتحركة الضمنية: AnimatedContainer و AnimatedPadding

يوفر Flutter فئة قوية من الودجات تُعرف بـ ودجات الرسوم المتحركة الضمنية. تُحرِّك هذه الودجات تغييرات خصائصها تلقائياً في كل مرة تُحدَّث فيها تلك الخصائص داخل setState(). لا تحتاج إلى إدارة AnimationController أو Tween أو Ticker على الإطلاق — فـ Flutter يتولى الاستيفاء (interpolation) خلف الكواليس.

أكثر ودجتي الرسوم المتحركة الضمنية استخداماً هما AnimatedContainer و AnimatedPadding. تقبل كلتاهما معامل duration الذي يتحكم في مدة الانتقال، ومعامل curve الاختياري الذي يُشكِّل تسارع الحركة.

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

كيف تعمل الرسوم المتحركة الضمنية

تتبع كل ودجت رسوم متحركة ضمنية النمط ذاته:

  • تُعلن الودجت ببعض قيم الخصائص الأولية.
  • عندما يُسبِّب تغيير الحالة تلقي خاصية واحدة أو أكثر لقيم جديدة، تكتشف الودجت الفرق.
  • خلال duration المحددة، تستوفي الودجت بسلاسة بين القيمة القديمة والجديدة.
  • تحترم الرسوم المتحركة Curve المختارة التي تتحكم في ملف سرعة الانتقال (مثل التسارع التدريجي، الارتداد، المرونة).

AnimatedContainer

AnimatedContainer هي النسخة المتحركة من Container. يمكنها تحريك أي خاصية تقريباً تدعمها Container: العرض والارتفاع واللون وانحناء الحافة والمحاذاة والحشو والهامش والزينة. هذا يجعلها واحدة من أكثر ودجات الرسوم المتحركة الضمنية تنوعاً في Flutter.

AnimatedContainer — تحريك الحجم واللون

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

  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _expanded = !_expanded;
        });
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 400),
        curve: Curves.easeInOut,
        width: _expanded ? 280.0 : 120.0,
        height: _expanded ? 280.0 : 120.0,
        decoration: BoxDecoration(
          color: _expanded ? Colors.deepPurple : Colors.teal,
          borderRadius: BorderRadius.circular(_expanded ? 40.0 : 8.0),
        ),
        alignment: Alignment.center,
        child: Text(
          _expanded ? 'اضغط للتصغير' : 'اضغط للتوسيع',
          style: const TextStyle(color: Colors.white, fontSize: 14),
          textAlign: TextAlign.center,
        ),
      ),
    );
  }
}

عندما يضغط المستخدم على الصندوق، يقلب setState قيمة _expanded. تستوفي AnimatedContainer تلقائياً قيم width و height و color و borderRadius خلال 400 ميلي ثانية باستخدام منحنى easeInOut. لا حاجة لأي كود نمطي للرسوم المتحركة.

نصيحة: يمكنك تحريك decoration و color في آنٍ واحد، لكن لا تضبط كلاً من color و BoxDecoration التي تحتوي لوناً في الوقت ذاته. سيُطلق Flutter خطأ تأكيد (assertion error). ضع اللون داخل BoxDecoration عندما تحتاج أيضاً إلى انحناء الحافة أو الظلال.

تحريك خصائص متعددة في آنٍ واحد

إحدى نقاط قوة AnimatedContainer هي أن كل خاصية تُغيِّرها في استدعاء setState واحد تُحرَّك في آنٍ واحد. يمكنك تحريك العرض والارتفاع واللون وانحناء الحافة والحشو والمحاذاة كلها في انتقال واحد.

AnimatedContainer — تأثير تمييز البطاقة

class HighlightCard extends StatefulWidget {
  final String label;
  const HighlightCard({super.key, required this.label});

  @override
  State<HighlightCard> createState() => _HighlightCardState();
}

class _HighlightCardState extends State<HighlightCard> {
  bool _selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _selected = !_selected),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.fastOutSlowIn,
        margin: EdgeInsets.all(_selected ? 4.0 : 12.0),
        padding: EdgeInsets.symmetric(
          vertical: _selected ? 24.0 : 16.0,
          horizontal: _selected ? 32.0 : 16.0,
        ),
        decoration: BoxDecoration(
          color: _selected ? Colors.indigo : Colors.white,
          borderRadius: BorderRadius.circular(_selected ? 20.0 : 4.0),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(_selected ? 0.25 : 0.08),
              blurRadius: _selected ? 16.0 : 4.0,
              offset: const Offset(0, 4),
            ),
          ],
        ),
        child: Text(
          widget.label,
          style: TextStyle(
            color: _selected ? Colors.white : Colors.black87,
            fontWeight:
                _selected ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }
}

AnimatedPadding

AnimatedPadding هي ودجت رسوم متحركة ضمنية متخصصة تُحرِّك فقط الحشو حول ودجتها الفرعية. إنها أخف وزناً من AnimatedContainer عندما تحتاج حصراً إلى نقل قيم الحشو — مثلاً، تمرير المحتوى للداخل أو الخارج استجابةً لتفاعل المستخدم أو ظهور لوحة المفاتيح.

AnimatedPadding — إزاحة المحتوى عند ظهور لوحة المفاتيح

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

  @override
  State<FocusableForm> createState() => _FocusableFormState();
}

class _FocusableFormState extends State<FocusableForm> {
  bool _focused = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedPadding(
          duration: const Duration(milliseconds: 350),
          curve: Curves.easeOut,
          padding: EdgeInsets.only(top: _focused ? 8.0 : 60.0),
          child: const FlutterLogo(size: 80),
        ),
        Focus(
          onFocusChange: (hasFocus) {
            setState(() => _focused = hasFocus);
          },
          child: const TextField(
            decoration: InputDecoration(
              labelText: 'اسم المستخدم',
              border: OutlineInputBorder(),
            ),
          ),
        ),
      ],
    );
  }
}

عندما يكتسب حقل النص التركيز، ينزلق الشعار بسلاسة للأعلى بالانتقال من top: 60.0 إلى top: 8.0 — محاكياً نوع إزاحة المحتوى الشائعة في شاشات تسجيل الدخول على الجوّال.

اختيار المنحنى المناسب

يتحكم معامل curve في كيفية تقدم الرسوم المتحركة بمرور الوقت. يشحن Flutter العديد من المنحنيات المدمجة في فئة Curves:

  • Curves.linear — سرعة ثابتة، إحساس ميكانيكي.
  • Curves.easeIn — بداية بطيئة ونهاية سريعة.
  • Curves.easeOut — بداية سريعة وتباطؤ طبيعي.
  • Curves.easeInOut — بداية ونهاية بطيئتان ووسط سريع. الأكثر طبيعية لانتقالات واجهة المستخدم.
  • Curves.fastOutSlowIn — منحنى الحركة القياسي لـ Material Design.
  • Curves.bounceOut — يتجاوز الهدف ويرتد عند النهاية.
  • Curves.elasticOut — تأثير ارتداد نابضي مرن.
نصيحة: لمعظم انتقالات واجهة المستخدم، يبدو Curves.easeInOut أو Curves.fastOutSlowIn الأكثر صقلاً ويتبع إرشادات حركة Material Design. احتفظ بمنحنيات الارتداد والمرونة للتفاعلات المرحة الشبيهة بالألعاب.

الخلاصة

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

النقطة الرئيسية: في أي وقت يبدأ فيه اسم ودجت Flutter بـ Animated ويوجد نظيرها غير المتحرك (مثل ContainerAnimatedContainer، و OpacityAnimatedOpacity)، فإنها تتبع نمط الرسوم المتحركة الضمنية: غيِّر قيمة الخاصية داخل setState() وسيُحرِّك Flutter الانتقال تلقائياً.