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

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

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

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

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

لماذا نستخدم الرسوم المتحركة الصريحة؟

الودجات الضمنية مريحة لكنها محدودة. تحتاج إلى رسوم متحركة صريحة عندما تريد:

  • تشغيل رسم متحرك للأمام ثم عكسه عند أحداث مستخدم مختلفة
  • تكرار رسم متحرك إلى ما لا نهاية أو عدداً محدداً من المرات
  • مزامنة رسوم متحركة متعددة على خط زمني واحد
  • التفاعل مع أحداث حالة الرسوم المتحركة (بدأ، اكتمل، عكس، رُفض)
  • قيادة محاكاة فيزيائية أو منحنى قائم على السرعة

الـ TickerProviderStateMixin

يتطلب AnimationController وجود Ticker — وهو آلية تستدعي callback في كل تحديث للشاشة (عادةً 60 أو 120 مرة في الثانية). يجعل mixin الخاص بـ TickerProviderStateMixin فئة State الخاصة بك TickerProvider صالحاً. إذا كنت تنشئ متحكماً واحداً فقط، يمكنك استخدام SingleTickerProviderStateMixin بدلاً من ذلك لأنه أكثر كفاءة قليلاً.

ملاحظة: استخدم دائماً SingleTickerProviderStateMixin عندما يكون لديك AnimationController واحد. استخدم TickerProviderStateMixin عندما تدير متحكمَين أو أكثر في نفس State.

إنشاء AnimationController

AnimationController هو الساعة الرئيسية لرسم متحرك. تنشئه في initState، وتوفر له duration، وتمرر vsync: this لربطه بـ ticker الشاشة.

إعداد AnimationController البسيط

import 'package:flutter/material.dart';

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

  @override
  State<FadeBox> createState() => _FadeBoxState();
}

class _FadeBoxState extends State<FadeBox>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this, // يربط المتحكم بـ Ticker الخاص بهذه الحالة
    );
  }

  @override
  void dispose() {
    _controller.dispose(); // تخلص دائماً لمنع تسرب الذاكرة
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(width: 100, height: 100, color: Colors.blue),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => _controller.forward(),
              child: const Text('تشغيل'),
            ),
          ],
        ),
      ),
    );
  }
}
تحذير: نسيان استدعاء _controller.dispose() في dispose() يتسبب في تسرب ذاكرة — يستمر Ticker في العمل حتى بعد إزالة الودجت من الشجرة. سيؤدي ذلك في نهاية المطاف إلى تعطل تطبيقك في وضع debug برسالة "ticker was still active".

إرفاق Tween

يُنتج AnimationController وحده قيماً بين 0.0 و1.0 فقط. يقوم Tween بتعيين هذا النطاق على أي فاصل مكتوب تحتاجه — double أو Color أو Offset أو Size وما إلى ذلك. تربطه باستخدام .animate() لإنتاج Animation<T> مكتوب:

Tween ينتج Animation مكتوباً

// ينتج Animation<double> يتراوح من 0.0 إلى 300.0
final Animation<double> _widthAnimation = Tween<double>(
  begin: 0.0,
  end: 300.0,
).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Curves.easeInOut, // تطبيق منحنى تخفيف
  ),
);

// ينتج Animation<Color?> ينتقل بين لونين
final Animation<Color?> _colorAnimation = ColorTween(
  begin: Colors.blue,
  end: Colors.orange,
).animate(_controller);

// استخدام القيمة المتحركة في build():
@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Container(
        width: _widthAnimation.value,
        height: 80,
        color: _colorAnimation.value,
        child: child, // child لا يُعاد بناؤه في كل إطار
      );
    },
    child: const Center(child: Text('حرّكني')),
  );
}

قيادة الأمام والعكس والتكرار

بمجرد أن يكون لديك متحكم، تقوده بثلاث طرق رئيسية:

  • _controller.forward() — يشغل من القيمة الحالية نحو 1.0
  • _controller.reverse() — يشغل من القيمة الحالية عائداً نحو 0.0
  • _controller.repeat(reverse: true) — يكرر الرسم المتحرك مع تبديل الاتجاه في كل دورة
  • _controller.reset() — يُعيد القيمة فوراً إلى 0.0 دون تحريك
  • _controller.stop() — يجمد التشغيل عند القيمة الحالية
نصيحة: يمكنك الاستماع إلى تغييرات حالة الرسوم المتحركة باستخدام _controller.addStatusListener((status) { ... }). الحالات الأربع هي AnimationStatus.forward وdismissed وreverse وcompleted. هذه هي الطريقة الصحيحة لتسلسل الرسوم المتحركة أو تشغيل إجراءات عند انتهاء رسم متحرك.

إعادة بناء واجهة المستخدم مع AnimatedBuilder

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

ملخص

تتطلب الرسوم المتحركة الصريحة في Flutter ثلاث خطوات: (1) خلط SingleTickerProviderStateMixin (أو TickerProviderStateMixin) في State الخاص بك؛ (2) إنشاء AnimationController في initState والتخلص منه في dispose؛ (3) إرفاق Tween عبر .animate() لتعيين النطاق 0–1 على القيمة المكتوبة المرغوبة. قد الرسم المتحرك بـ forward() أو reverse() أو repeat() استجابةً لأحداث المستخدم، وأعد بناء واجهة المستخدم مع AnimatedBuilder لأداء مثالي.