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

منحنيات الرسوم المتحركة: التحكم في طبيعة الحركة

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

منحنيات الرسوم المتحركة: التحكم في طبيعة الحركة

في Flutter، يُحدِّد منحنى الرسوم المتحركة كيفية تقدم القيمة المتحركة من نقطة البداية إلى نقطة النهاية عبر الزمن. بدون منحنى، تتقدم كل رسوم متحركة بوتيرة ثابتة — وهو سلوك يُعرف بـالحركة الخطية. غير أن الأجسام في العالم الحقيقي نادراً ما تتحرك بهذه الطريقة. باب يفتح ببطء ثم يتسارع ثم يتباطأ بلطف حتى يتوقف. تتيح المنحنيات تكرار هذا الطابع الفيزيائي في واجهة المستخدم، مما يمنح الانتقالات طابعاً طبيعياً ومتقناً.

تُمثَّل المنحنيات بالفئة المجردة Curve الموجودة في dart:ui والمكشوفة من خلال فئة الثوابت Curves في Flutter. كل منحنى يربط قيمة إدخال في النطاق [0.0, 1.0] بقيمة إخراج، تقع أيضاً عادةً ضمن [0.0, 1.0] (وإن كانت منحنيات التجاوز قد تتعدى هذه الحدود مؤقتاً).

ملاحظة: لا يُغيِّر المنحنى مدة الرسوم المتحركة — بل يُعيد تشكيل كيفية تغيُّر القيمة المستكملة فحسب خلال تلك المدة. المدة والمنحنى عنصران مستقلان عن بعضهما.

تطبيق منحنى باستخدام CurvedAnimation

الطريقة الأكثر شيوعاً لتطبيق منحنى هي تغليف AnimationController داخل CurvedAnimation. يعمل CurvedAnimation كمزيِّن: يمرر نفس تدفق الإطارات لكنه يحوِّل كل قيمة من خلال المنحنى المختار قبل أن يقرأها أي Tween أو AnimatedWidget.

الاستخدام الأساسي لـ CurvedAnimation

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

  @override
  State<FadeInBox> createState() => _FadeInBoxState();
}

class _FadeInBoxState extends State<FadeInBox>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacity;

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

    // تغليف المتحكم داخل CurvedAnimation
    final curved = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,       // بداية بطيئة، وسط سريع، نهاية بطيئة
    );

    _opacity = Tween<double>(begin: 0.0, end: 1.0).animate(curved);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _opacity,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

مكتبة المنحنيات المدمجة

يأتي Flutter مزوداً بعشرات المنحنيات الجاهزة في فئة Curves، وتنتمي إلى عدة عائلات:

منحنيات Ease

  • Curves.linear — سرعة ثابتة؛ لا تسارع. يُستخدم بحذر إذ يبدو آلياً في الغالب.
  • Curves.ease — تسارع لطيف ثم تباطؤ (الافتراضي في CSS). خيار آمن للأغراض العامة.
  • Curves.easeIn — بداية بطيئة، نهاية سريعة. مناسب للعناصر المغادِرة للشاشة.
  • Curves.easeOut — بداية سريعة، نهاية بطيئة. مناسب للعناصر القادمة إلى الشاشة.
  • Curves.easeInOut — بداية بطيئة، وسط سريع، نهاية بطيئة. سلس ومتوازن.
  • Curves.easeInOutCubic — نسخة أقوى وأكثر سينمائية من easeInOut.

منحنيات مستوحاة من الفيزياء

  • Curves.bounceIn / Curves.bounceOut / Curves.bounceInOut — يحاكي كرة مرتدة. bounceOut هو الأكثر طبيعية (الكائن يستقر عند الوجهة).
  • Curves.elasticIn / Curves.elasticOut / Curves.elasticInOut — تجاوز وتذبذب شبيه بالنابض. يُستخدم باعتدال؛ الأنسب لواجهات المستخدم المرحة وأسلوب الألعاب.

التباطؤ والتجاوز

  • Curves.decelerate — دخول سريع، تباطؤ تدريجي؛ يُستخدم كثيراً لانزلاق الألواح السفلية.
  • Curves.fastOutSlowIn — المنحنى القياسي في Material Design. تسارع قوي ثم تباطؤ طويل وسلس.
  • Curves.slowMiddle — سريع في الطرفين، هادئ في المنتصف.
نصيحة: توصي إرشادات الحركة في Material Design باستخدام Curves.fastOutSlowIn لمعظم رسوم الدخول وCurves.fastLinearToSlowEaseIn لرسوم الخروج. اتباع هذه الافتراضيات يضمن تجانس تطبيقك مع النظام الأساسي.

مقارنة المنحنيات جنباً إلى جنب

أفضل طريقة لاستيعاب شكل كل منحنى هي تحريك نفس الخاصية بمنحنيات مختلفة في آنٍ واحد. المثال التالي يُزحزح أربعة حاويات نحو اليمين على مدار ثانية واحدة، كلٌّ منها يستخدم منحنىً مختلفاً:

مقارنة المنحنيات جنباً إلى جنب

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

  @override
  State<CurveComparisonPage> createState() => _CurveComparisonPageState();
}

class _CurveComparisonPageState extends State<CurveComparisonPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _ctrl;

  // متحكم واحد يقود المنحنيات الأربعة
  late Animation<double> _linear;
  late Animation<double> _easeOut;
  late Animation<double> _bounceOut;
  late Animation<double> _elasticOut;

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

    _linear     = Tween<double>(begin: 0, end: 200)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.linear));
    _easeOut    = Tween<double>(begin: 0, end: 200)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
    _bounceOut  = Tween<double>(begin: 0, end: 200)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.bounceOut));
    _elasticOut = Tween<double>(begin: 0, end: 200)
        .animate(CurvedAnimation(parent: _ctrl, curve: Curves.elasticOut));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('مقارنة المنحنيات')),
      body: AnimatedBuilder(
        animation: _ctrl,
        builder: (context, _) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              _track('linear',      _linear.value,     Colors.grey),
              _track('easeOut',     _easeOut.value,    Colors.blue),
              _track('bounceOut',   _bounceOut.value,  Colors.green),
              _track('elasticOut',  _elasticOut.value, Colors.red),
            ],
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _ctrl.forward(from: 0),
        child: const Icon(Icons.play_arrow),
      ),
    );
  }

  Widget _track(String label, double offset, Color color) {
    return Row(
      children: [
        SizedBox(width: 90, child: Text(label)),
        Transform.translate(
          offset: Offset(offset, 0),
          child: Container(
            width: 30, height: 30,
            decoration: BoxDecoration(color: color, shape: BoxShape.circle),
          ),
        ),
      ],
    );
  }
}

منحنيات مخصصة باستخدام Cubic Bezier

عندما لا يتطابق أيٌّ من المنحنيات المدمجة مع نيتك التصميمية، يمكنك إنشاء منحنى مخصص باستخدام Cubic. هذا يعكس دالة cubic-bezier() في CSS ويقبل أربعة معاملات لنقاط التحكم: a وb وc وd.

منحنى Cubic مخصص

// منحنى easeOut حاد جداً: بداية سريعة، تباطؤ مفاجئ
const snappyEaseOut = Cubic(0.22, 1.0, 0.36, 1.0);

final animation = Tween<double>(begin: 0, end: 300).animate(
  CurvedAnimation(parent: controller, curve: snappyEaseOut),
);
تحذير: منحنيات التجاوز مثل elasticOut تُنتج قيماً خارج [0.0, 1.0] لفترة وجيزة. إذا طبَّقت مثل هذا المنحنى على خاصية ذات حدود صارمة (مثل Opacity التي تُقيِّد القيم إلى [0.0, 1.0])، فسيُقصَّر التجاوز بصمت ويضيع تأثير النابض. استخدم منحنيات التجاوز على خصائص غير مقيَّدة كإزاحات الإزاحة أو قيم المقياس عوضاً عن ذلك.

منحنيات العكس

يقبل CurvedAnimation معاملاً اختيارياً reverseCurve، يُطبَّق عند تشغيل المتحكم بشكل عكسي. هذا قيِّم لإنشاء انتقالات دخول/خروج غير متماثلة تبدو صحيحة فيزيائياً — مثلاً، قد ينزلق درج إلى الداخل بـeaseOut لكنه ينزلق للخارج بـeaseIn.

منحنى دخول/خروج غير متماثل

final curved = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeOut,        // يُستخدم عند التشغيل للأمام
  reverseCurve: Curves.easeIn,  // يُستخدم عند التشغيل بالعكس
);

ملخص

منحنيات الرسوم المتحركة أداة بسيطة لكنها قوية لتشكيل شعور الحركة. النقاط الرئيسية:

  • غلِّف AnimationController داخل CurvedAnimation لتطبيق المنحنى.
  • استخدم منحنيات عائلة ease للانتقالات القياسية دخولاً وخروجاً.
  • استخدم منحنيات الارتداد والمرونة للتأثيرات المرحة اللافتة للانتباه — لكن باعتدال.
  • فضِّل Curves.fastOutSlowIn افتراضياً للامتثال لمعايير Material Design.
  • استخدم Cubic لتعريف منحنيات مخصصة دقيقة تتوافق مع مواصفات التصميم.
  • قدِّم reverseCurve للسلوك غير المتماثل بين التشغيل الأمامي والعكسي.