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

الرسوم المتحركة المتدرجة: التأثيرات المتتالية

16 دقيقة الدرس 8 من 13

الرسوم المتحركة المتدرجة: التأثيرات المتتالية

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

المفاهيم الأساسية: الفترات الزمنية ووحدة التحكم الواحدة

سرّ الرسوم المتحركة المتدرجة في Flutter هو استخدام AnimationController واحد يعمل من 0.0 إلى 1.0 على مدار المدة الإجمالية، مقترناً بمنحنيات Interval متعددة تنشط كل منها خلال نطاق فرعي مختلف من ذلك الجدول الزمني. تقوم فئة Interval(begin, end, curve) بتعيين جزء من تقدم وحدة التحكم إلى رسم متحرك فرعي، حيث تبقى عند 0.0 قبل نافذتها الزمنية وعند 1.0 بعدها.

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

إعداد وحدة تحكم الرسوم المتحركة

ابدأ بـ StatefulWidget يدمج SingleTickerProviderStateMixin. أنشئ وحدة التحكم في initState، واضبط المدة الإجمالية لتغطية جميع المراحل (مثلاً 800 ميلي ثانية لخمسة عناصر)، واستدعِ forward() فوراً لبدء تسلسل الدخول:

AnimationController مع فترات متدرجة

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

  @override
  State<StaggeredListDemo> createState() => _StaggeredListDemoState();
}

class _StaggeredListDemoState extends State<StaggeredListDemo>
    with SingleTickerProviderStateMixin {

  late final AnimationController _controller;

  // خمسة عناصر، كل منها يشغل نافذة 0.20 من الجدول الزمني،
  // مع تداخل طفيف بنسبة 0.05 لإبقاء الشعور حيوياً.
  static const int _itemCount = 5;
  late final List<Animation<double>> _fadeAnims;
  late final List<Animation<Offset>> _slideAnims;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 900),
    );

    _fadeAnims = List.generate(_itemCount, (i) {
      final start = i * 0.15;          // كل عنصر يبدأ بتأخير 15٪ إضافية
      final end   = start + 0.40;      // كل عنصر يتلاشى خلال 40٪ من الجدول الزمني
      return Tween<double>(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(start, end.clamp(0.0, 1.0), curve: Curves.easeIn),
        ),
      );
    });

    _slideAnims = List.generate(_itemCount, (i) {
      final start = i * 0.15;
      final end   = start + 0.45;
      return Tween<Offset>(
        begin: const Offset(0.0, 0.4),  // يبدأ بمقدار 40٪ أسفل موضعه الطبيعي
        end: Offset.zero,
      ).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(start, end.clamp(0.0, 1.0), curve: Curves.easeOutCubic),
        ),
      );
    });

    // ابدأ التشغيل فور إدراج الودجت في الشجرة.
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('قائمة متدرجة')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: _itemCount,
        itemBuilder: (context, index) {
          return FadeTransition(
            opacity: _fadeAnims[index],
            child: SlideTransition(
              position: _slideAnims[index],
              child: Card(
                margin: const EdgeInsets.symmetric(vertical: 8),
                child: ListTile(
                  leading: CircleAvatar(child: Text('${index + 1}')),
                  title: Text('عنصر ${index + 1}'),
                  subtitle: const Text('يظهر بانزلاق وتلاشٍ متدرج'),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

اختيار الفترات الزمنية المناسبة

تعتمد جودة الرسوم المتحركة المتدرجة كلياً على مقدار التداخل بين الفترات الزمنية. ثلاثة قواعد عملية:

  • بدون تداخل — تنتظر كل عنصر حتى ينتهي السابق. يشعر بالبطء والآلية.
  • تداخل صغير (10–20 %) — يبدأ كل عنصر قبيل انتهاء السابق. سلس واحترافي — النقطة المثالية لمعظم واجهات المستخدم.
  • تداخل كبير (> 40 %) — تظهر العناصر معاً تقريباً. استخدمه فقط عندما تريد ظهوراً متفجراً وليس تتابعاً.
نصيحة: اجعل المدة الإجمالية لوحدة التحكم أقل من 1000 ميلي ثانية لرسوم الدخول. التسلسلات الأطول تبدو ثقيلة عند الزيارات المتكررة. إذا كان بمقدور المستخدمين إعادة تشغيل الرسوم المتحركة (مثل السحب للتحديث)، استدعِ _controller.reset() قبل _controller.forward().

الجمع بين FadeTransition و SlideTransition

تداخل SlideTransition داخل FadeTransition (أو العكس) ينتج تأثير "الارتفاع والتلاشي" الكلاسيكي. كلا الودجتين يعيدان البناء فقط عند تغير قيم الرسوم المتحركة الخاصة بهما، لذا فإن العرض فعّال للغاية — لا حاجة لاستدعاء setState أثناء الرسوم المتحركة.

إعادة التشغيل عند الطلب: إعادة الضبط والتقديم

// أضف FloatingActionButton لإعادة تشغيل تسلسل الرسوم المتدرجة:
floatingActionButton: FloatingActionButton(
  onPressed: () {
    _controller.reset();    // إعادة جميع العناصر إلى حالة الإخفاء/الإزاحة
    _controller.forward();  // تشغيل التتابع من جديد
  },
  child: const Icon(Icons.replay),
),

استخراج ودجت StaggeredItem قابل لإعادة الاستخدام

في قواعد الكود الحقيقية، تجنب بناء جميع الرسوم المتحركة داخل فئة حالة ضخمة واحدة. بدلاً من ذلك، أنشئ ودجت StaggeredItem صغيراً يقبل Animation جاهزاً ويلفّ محتواه. هذا يفصل تكوين الرسوم المتحركة عن المحتوى المتحرك، مما يجعل كليهما أسهل في الاختبار والصيانة.

تحذير: لا تنشئ AnimationController أبداً داخل طريقة build(). وحدات التحكم كائنات قابلة للتخلص منها — إنشاؤها في كل عملية بناء يتسبب في تسرب الذاكرة وتشوهات بصرية. أنشئها دائماً في initState وتخلص منها في dispose.

الخلاصة

تُبنى الرسوم المتحركة المتدرجة في Flutter من وحدة تحكم واحدة + رسوم متحركة فرعية متعددة محددة النطاق بـ Interval. تنشط كل رسوم متحركة فرعية خلال نافذتها الزمنية الخاصة من نطاق 0.0–1.0 لوحدة التحكم، مما ينتج تأثير التتابع دون أي تعقيد إضافي. اجمع FadeTransition مع SlideTransition للحصول على تأثير دخول القائمة الكلاسيكي، واختر تداخلاً بنسبة 10–20 % للحصول على إحساس سلس، وأبقِ المدة الإجمالية أقل من ثانية، وتخلص دائماً من وحدة التحكم. هذا النمط يتوسع بسلاسة من عنصرين إلى عشرين دون أي تدهور في الأداء.