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

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

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

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

عند بناء واجهات Flutter متقنة، كثيرًا ما تحتاج إلى تحريك عدة خصائص بصرية — الشفافية، والموضع، والحجم، واللون — بتسلسل محكم أو بتداخل مدروس. قيادة كل منها بـ AnimationController مستقل أمر مُكلف وصعب المزامنة. الحل الاحترافي هو استخدام متحكم واحد وتقسيم الجدول الزمني 0.0 → 1.0 إلى نطاقات فرعية مسمّاة باستخدام CurvedAnimations مقيّدة بـ Interval.

ملاحظة: Interval هو صنف فرعي من Curve. عند تغليفه داخل CurvedAnimation، فإنه يُعيّن شريحةً من الجدول الزمني للمتحكم إلى المدخل 0.0–1.0 لأي Tween. خارج تلك الشريحة تُثبَّت القيمة عند 0.0 أو 1.0، فتبقى الخاصية ساكنة بينما تعمل رسوم متحركة أخرى.

كيف يعمل Interval

Interval(begin, end, curve: ...) يقبل موضعين معيارَيْن على الجدول الزمني للمتحكم الأب (كلاهما بين 0.0 و1.0). داخل هذه النافذة يُطبَّق المنحنى الداخلي (الافتراضي: Curves.linear). خارجها تُثبَّت القيمة:

  • قبل begin ← الخرج 0.0
  • بعد end ← الخرج 1.0
  • بين begin وend ← يُعيّن المنحنى الداخلي التقدم المحلي

مثال 1 — تلاشٍ + انزلاق متدرج من متحكم واحد

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

  @override
  State<StaggeredCard> createState() => _StaggeredCardState();
}

class _StaggeredCardState extends State<StaggeredCard>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  // الرسم المتحرك الفرعي 1: تلاشٍ خلال أول 40 % من الجدول الزمني
  late final Animation<double> _opacity;

  // الرسم المتحرك الفرعي 2: انزلاق للأعلى خلال 40–90 % من الجدول الزمني
  late final Animation<Offset> _slide;

  // الرسم المتحرك الفرعي 3: تكبير خلال 60–100 % من الجدول الزمني
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 1200),
      vsync: this,
    );

    _opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.4, curve: Curves.easeIn),
      ),
    );

    _slide = Tween<Offset>(
      begin: const Offset(0.0, 0.3),
      end: Offset.zero,
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.4, 0.9, curve: Curves.easeOut),
      ),
    );

    _scale = Tween<double>(begin: 0.8, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.6, 1.0, curve: Curves.elasticOut),
      ),
    );

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return FadeTransition(
          opacity: _opacity,
          child: SlideTransition(
            position: _slide,
            child: ScaleTransition(
              scale: _scale,
              child: child,
            ),
          ),
        );
      },
      child: const Card(
        child: Padding(
          padding: EdgeInsets.all(24.0),
          child: Text('مرحبًا بعالم الرسوم المتدرجة!'),
        ),
      ),
    );
  }
}
نصيحة: لاحظ أن الودجت الابن يُمرَّر كمعامل child إلى AnimatedBuilder. هذه الشجرة الفرعية تُبنى مرةً واحدة وتُعاد استخدامها بين الإطارات، مما يتجنب إعادة بناء المحتوى الثابت. فقط أغلفة الانتقال تُعاد رسمها في كل إطار.

الفترات المتتالية مقابل المتداخلة

يمكن ترتيب الفترات بثلاث طرق لتحقيق تأثيرات توجيه مختلفة:

  • متتالية (بلا تداخل): Interval(0.0, 0.5) ثم Interval(0.5, 1.0) — تنتهي إحداهن قبل أن تبدأ الأخرى.
  • متداخلة: Interval(0.0, 0.6) وInterval(0.4, 1.0) — تتشاركان نافذة تداخل 20 % لتأثير انتقال أكثر سلاسة.
  • بداية متأخرة: Interval(0.3, 1.0) — يظل الرسم المتحرك خاملًا طوال أول 30 % من الجدول الزمني، ثم ينطلق بأقصى سرعة.

حزمة SequenceAnimation

للأنماط المتدرجة المعقدة، توفر حزمة المجتمع sequence_animation واجهة برمجية بنّاءة سلسة مبنية على نفس مبدأ Interval. تدير النطاقات الزمنية نيابةً عنك وتُعيد AnimationMap مفاتيحها تسميات نصية. وإن لم تكن جزءًا من SDK الرسمي، فإن فهمها يُعزز إدراك سبب قوة آلية Interval الأساسية.

مثال 2 — تسلسل ألوان ثلاثي المراحل (SDK فقط، بلا حزمة)

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

  @override
  State<ColorSequence> createState() => _ColorSequenceState();
}

class _ColorSequenceState extends State<ColorSequence>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  late final Animation<Color?> _phase1; // أزرق  → أخضر  (0–33 %)
  late final Animation<Color?> _phase2; // أخضر  → برتقالي (33–66 %)
  late final Animation<Color?> _phase3; // برتقالي → أحمر    (66–100 %)

  Color get _color =>
      _phase3.value ?? _phase2.value ?? _phase1.value ?? Colors.blue;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    )..repeat(reverse: true);

    _phase1 = ColorTween(begin: Colors.blue, end: Colors.green).animate(
      CurvedAnimation(
        parent: _ctrl,
        curve: const Interval(0.0, 0.33, curve: Curves.linear),
      ),
    );

    _phase2 = ColorTween(begin: Colors.green, end: Colors.orange).animate(
      CurvedAnimation(
        parent: _ctrl,
        curve: const Interval(0.33, 0.66, curve: Curves.linear),
      ),
    );

    _phase3 = ColorTween(begin: Colors.orange, end: Colors.red).animate(
      CurvedAnimation(
        parent: _ctrl,
        curve: const Interval(0.66, 1.0, curve: Curves.linear),
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _ctrl,
      builder: (_, __) => Container(
        width: 120,
        height: 120,
        decoration: BoxDecoration(
          color: _color,
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}
تحذير: ColorTween يُعيد Color? قابلًا للقيمة الخالية. احرص دائمًا على توفير قيمة احتياطية (كما هو موضح في سلسلة null-coalescing أعلاه) أو استخدم ! فقط حين تكون واثقًا أن الرسم المتحرك قد بدأ. إغفال هذا مصدر شائع لأعطال null-dereference عند الإطار الأول.

اعتبارات الأداء

الرسوم المتحركة المتسلسلة بالفترات فعّالة لأنها تتشارك ticker واحدة. ضع هذه الممارسات الفضلى في الاعتبار:

  • استخدم AnimatedBuilder مع child ثابت لتجنب إعادة بناء الأشجار الفرعية غير المتغيرة.
  • فضّل FadeTransition وSlideTransition وScaleTransition على أغلفة Opacity وTransform — فودجات الانتقال مدركة لحدود إعادة الرسم.
  • قِسْ باستخدام لوحة Performance في Flutter DevTools لرصد الإطارات المتقطعة إذا كانت الفترات تُطلق عملًا بنائيًا ثقيلًا في كل إطار.

الخلاصة

تنظيم رسوم متحركة متعددة من AnimationController واحد هو النمط القياسي في Flutter للحركة المتدرجة المنسّقة. الخطوات الأساسية هي: (1) إنشاء متحكم واحد بمدة إجمالية، (2) تغليفه في CurvedAnimations متعددة مقيّدة كل منها بـ Interval، (3) تغذية كل CurvedAnimation إلى Tween الخاص به، و(4) العرض باستخدام AnimatedBuilder مع ودجات الانتقال. يمكن أن تكون الفترات متتالية أو متداخلة أو متأخرة البداية لتحقيق أي توجيه مطلوب دون الحاجة أبدًا إلى متحكم ثانٍ.