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

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

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

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

تستخدم معظم رسوم Flutter المتحركة مدةً ثابتة (Duration) ومنحنى (Curve) لتحويل الوقت إلى قيمة. أما الرسوم المتحركة الفيزيائية فتعمل بأسلوب مختلف: إذ تُقاد بـقوى محاكاة بدلاً من جدول زمني محدد مسبقاً. تنتهي الحركة حين يبلغ النظام المحاكى حالة الاتزان، لا حين ينتهي المؤقت. وهذا ما يمنح التطبيقات عالية الجودة إحساسها الطبيعي والعضوي.

يوفّر Flutter واجهة المحاكاة الفيزيائية عبر الدالة AnimationController.animateWith(Simulation). بدلاً من استدعاء forward() أو reverse()، تُسلّم المتحكمَ كائنَ Simulation يحسب الموضع والسرعة في كل نبضة. أكثر المحاكاة المضمّنة فائدةً هما SpringSimulation وFrictionSimulation.

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

SpringSimulation (محاكاة الزنبرك)

تُمثّل SpringSimulation مذبذباً توافقياً مُخمَّداً — أي كتلة مُعلَّقة بزنبرك. يُحدَّد سلوكه عبر SpringDescription الذي يأخذ ثلاثة معاملات مُسمّاة:

  • mass (الكتلة) — الكتلة الافتراضية المُعلَّقة بالزنبرك (كتلة أكبر = عطالة أكبر = استجابة أبطأ).
  • stiffness (الصلابة) — مدى قوة شد الزنبرك نحو الهدف (قيمة أعلى = استجابة أسرع وأكثر حيوية).
  • damping (التخميد) — المقاومة التي تبدد الطاقة (قيمة أعلى = اهتزاز أقل؛ قيمة >= 2×√(stiffness×mass) تعني تخميداً زائداً دون ارتداد).

مثال SpringSimulation — بطاقة مرتدة

import 'package:flutter/physics.dart';

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

  @override
  State<SpringCard> createState() => _SpringCardState();
}

class _SpringCardState extends State<SpringCard>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this)
      ..addListener(() => setState(() {}));

    // زنبرك خفيف التخميد: سيهتز مرات عدة قبل الاستقرار.
    final spring = SpringDescription(
      mass: 1.0,
      stiffness: 200.0,
      damping: 10.0,
    );
    final simulation = SpringSimulation(
      spring,
      0.0,   // الموضع الابتدائي
      1.0,   // الموضع النهائي
      0.0,   // السرعة الابتدائية
    );
    _controller.animateWith(simulation);
  }

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

  @override
  Widget build(BuildContext context) {
    return Transform.scale(
      scale: 0.8 + 0.2 * _controller.value,
      child: Card(
        color: Colors.deepPurple,
        child: const SizedBox(width: 200, height: 120,
          child: Center(
            child: Text('Spring!',
              style: TextStyle(color: Colors.white, fontSize: 24)),
          ),
        ),
      ),
    );
  }
}

FrictionSimulation (محاكاة الاحتكاك)

تُمثّل FrictionSimulation جسماً يتباطأ تحت تأثير الاحتكاك — كبطاقة تُرمى على سطح وتتوقف تدريجياً. تُحدد معامل السحب (مقدار قوة الاحتكاك) والموضع الابتدائي والسرعة الابتدائية. لا يوجد هدف محدد؛ تُحدَّد نقطة التوقف النهائية بالفيزياء وحدها.

مثال FrictionSimulation — لوحة قابلة للرمي

import 'package:flutter/physics.dart';

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

  @override
  State<FrictionPanel> createState() => _FrictionPanelState();
}

class _FrictionPanelState extends State<FrictionPanel>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // AnimationController بلا مدة؛ الفيزياء تقوده.
    _controller = AnimationController(vsync: this)
      ..addListener(() => setState(() {}));
  }

  void _onFlingEnd(DragEndDetails details) {
    final velocity = details.primaryVelocity ?? 0.0;
    if (velocity.abs() < 50) return;

    // تطبيع السرعة إلى النطاق [0, 1] — قيمة المتحكم 0..1.
    final normalised = velocity / 1000.0;

    final simulation = FrictionSimulation(
      0.135,          // معامل السحب (أعلى = توقف أسرع)
      _controller.value,
      normalised,     // السرعة الابتدائية بالوحدات في الثانية
    );
    _controller.animateWith(simulation);
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onHorizontalDragEnd: _onFlingEnd,
      child: Transform.translate(
        offset: Offset(_controller.value * 300, 0),
        child: Container(
          width: 280, height: 80,
          color: Colors.teal,
          alignment: Alignment.center,
          child: const Text('ارمني!',
            style: TextStyle(color: Colors.white)),
        ),
      ),
    );
  }
}

اختيار المحاكاة المناسبة

  • استخدم SpringSimulation حين تريد انجذاب عنصر نحو موضع مستهدف مع ارتداد اختياري — مثالي للنوافذ المنبثقة والبطاقات وألواح الأسفل عند ظهورها.
  • استخدم FrictionSimulation حين تريد حركة مدفوعة بإيماءة رمي أو سحب تتوقف بصورة طبيعية — مثالي للأسطح القابلة للتمرير أو تفاعلات السحب للرفض.
  • ادمجهما معاً بتسلسل الحركات: التقط الرمية بـFrictionSimulation، ثم ثبّت العنصر في موضع بـSpringSimulation.
نصيحة: قيّد دائماً قيمة المتحكم باستخدام AnimationController(lowerBound: 0.0, upperBound: 1.0, vsync: this) حين تحتاج إبقاء المحاكاة ضمن نطاق آمن. بدون الحدود، يمكن لزنبرك سريع أو رمية قوية أن تدفع القيمة بعيداً عن 1.0 أو دون 0.0 مما يُفسد تحويلات التخطيط.

التسامحات ومتى تنتهي المحاكاة

تُشير المحاكاة إلى انتهائها عبر isDone(double time). يستخدم Flutter كائن Tolerance افتراضي (من physics.dart) بقيم distance = 0.001 وvelocity = 0.001. يمكنك تمرير Tolerance مخصص إلى SpringSimulation كمعامل خامس اختياري إذا أردت انتهاء الرسوم المتحركة مبكراً (تسامح أوسع) أو استقراراً أدق (تسامح أضيق).

تحذير: لا تستدعِ animateWith() أبداً أثناء تشغيل المتحكم محاكاةً أخرى دون استدعاء stop() أولاً. التداخل بين الاستدعاءات يُتلف مرجع الساعة الداخلي وقد يُسبّب اهتزاز الواجهة أو خطأ في وضع التصحيح.

ملخص

تحل الرسوم المتحركة الفيزيائية في Flutter محل منحنيات المدة الثابتة بـكائنات Simulation مدفوعة بقوى كصلابة الزنبرك والتخميد والاحتكاك. AnimationController.animateWith() هي نقطة الدخول. تستهدف SpringSimulation موضعاً محدداً ويمكنها الاهتزاز، بينما تنطلق FrictionSimulation من سرعة ابتدائية دون نهاية محددة. معاً، تُمكّنان من تفاعلات تبدو طبيعية واستجابية كالأجسام الفيزيائية في العالم الحقيقي.