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

مقدمة إلى الرسوم المتحركة في Flutter

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

مقدمة إلى الرسوم المتحركة في Flutter

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

تصنيف الرسوم المتحركة على ثلاثة مستويات

يُفهم نظام الرسوم المتحركة في Flutter على أفضل وجه باعتباره ثلاثة مستويات، كل منها يمنح تحكماً أكبر على حساب مزيد من الكود:

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

الرسوم المتحركة الضمنية — التحريك بدون متحكم

عائلة ودجات AnimatedFoo (مثل AnimatedOpacity وAnimatedContainer وAnimatedPadding وAnimatedAlign) تلف ودجتاً عادياً وتُحوّل أي خاصية تلقائياً عند تغيير قيمتها. تحدد duration واختيارياً curve؛ وFlutter يتولى الاستيفاء.

متى تختار الضمني: يُشغَّل الحركة بتغيير الحالة (قلب قيمة منطقية، أو قيمة بيانات جديدة)، ويعمل مرة واحدة لكل تشغيل، وليس له تبعية على رسوم أخرى، ولا يحتاج إلى إيقاف مؤقت أو عكس عند الطلب أو ترتيب تسلسلي.

مثال 1 — AnimatedOpacity

import 'package:flutter/material.dart';

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

  @override
  State<FadeToggle> createState() => _FadeToggleState();
}

class _FadeToggleState extends State<FadeToggle> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedOpacity(
          opacity: _visible ? 1.0 : 0.0,
          duration: const Duration(milliseconds: 400),
          curve: Curves.easeInOut,
          child: const FlutterLogo(size: 100),
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: () => setState(() => _visible = !_visible),
          child: Text(_visible ? 'إخفاء' : 'إظهار'),
        ),
      ],
    );
  }
}

بالضغط على الزر يتم قلب _visible، مما يغير opacity، ويقوم AnimatedOpacity بالتدرج السلس في قيمة الشفافية على مدى 400 ملي ثانية — دون الحاجة لأي AnimationController.

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

عندما تحتاج تحكماً دقيقاً — تكرار، أو عكس بإيماءة، أو تسلسل قيم متعددة — تنتقل إلى AnimationController مع Tween وAnimatedBuilder (أو اختصارات الـ mixin: SingleTickerProviderStateMixin / TickerProviderStateMixin).

متى تختار الصريح: يجب أن يبدأ الحركة أو يتوقف استجابةً لإدخال المستخدم، أو يتكرر إلى أجل غير مسمى (مثل مؤشر التحميل)، أو يتزامن مع رسوم متحركة أخرى، أو يحرك قيمة مرسومة مخصصة.

مثال 2 — AnimationController مع Tween

import 'package:flutter/material.dart';

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

  @override
  State<PulsingDot> createState() => _PulsingDotState();
}

class _PulsingDotState extends State<PulsingDot>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    )..repeat(reverse: true); // يتكرر ذهاباً وإياباً

    _scale = Tween<double>(begin: 0.8, end: 1.4).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _scale,
      builder: (context, child) => Transform.scale(
        scale: _scale.value,
        child: child,
      ),
      child: Container(
        width: 60,
        height: 60,
        decoration: const BoxDecoration(
          color: Colors.deepPurple,
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}

الرسوم المتحركة الفيزيائية — حركة طبيعية

تستبدل محاكاة الفيزياء المنحنيات ذات المدة الثابتة بنماذج قائمة على القوة. الأكثر شيوعاً هو SpringSimulation، الذي يُتاح عبر SpringDescription وAnimationController.animateWith(). تُمثّل FrictionSimulation التباطؤ (مثالية للتمرير السريع)، بينما تُنتج GravitySimulation الأجسام الساقطة.

متى تختار الفيزيائي: تبدأ الحركة بإيماءة لها سرعة (سحب وإطلاق، أو قذف)، أو تريد أن "يستقر" الودجت بشكل طبيعي بدلاً من التوقف المفاجئ عند وقت ثابت. قد لا تكون نقطة نهاية الحركة معروفة تماماً عند البداية.

نصيحة: معاملات spring_description — الكتلة والصلابة والتخميد — تتوافق مباشرة مع الحدس الفيزيائي. صلابة أعلى = نابض أكثر سرعة؛ تخميد أقل = مزيد من الاهتزاز (ارتداد). ابدأ بـ SpringDescription.withDampingRatio(mass: 1, stiffness: 200, ratio: 0.8) للحصول على ارتداد خفيف وذو مظهر احترافي.

دليل القرار: أي مستوى تستخدم؟

  • تغيير الحالة يُشغّل انتقالاً واحداً؟ → ضمني (AnimatedOpacity، AnimatedContainer، TweenAnimationBuilder).
  • تحتاج تكراراً أو تسلسلاً أو تحكماً بالإيماءة؟ → صريح (AnimationController + Tween + AnimatedBuilder).
  • مُبادَر بقذف أو سحب بسرعة حقيقية؟ → فيزيائي (SpringSimulation، FrictionSimulation).
  • انتقال مسار أو صفحة محدد مسبقاً؟PageRouteBuilder مع transitionsBuilder مخصص (صريح تحت الغطاء).
تحذير: احرص دائماً على التخلص من AnimationController في dispose(). المتحكم المتسرب يُبقي Ticker حياً، مما يستهلك المعالج والبطارية في كل إطار حتى بعد اختفاء الودجت. هذه واحدة من أكثر أخطاء الأداء شيوعاً في تطبيقات Flutter.

خط أنابيب الرسوم المتحركة من الداخل

كل رسوم متحركة في Flutter تحرك في نهاية المطاف قيمة double (قيمة الحركة) عبر الزمن باستخدام Ticker — وهو رد نداء مسجل مع إشارة VSYNC للمحرك. خط الأنابيب هو: Ticker → AnimationController (من 0.0 إلى 1.0) → Tween (يعيّن نطاقاً مكتوباً) → Curve (يُشكّل التخفيف) → إعادة بناء الودجت عبر AnimatedBuilder أو مستمع. الودجات الضمنية تلف كل ذلك داخلياً؛ أما الرسوم الصريحة فتكشف كل خطوة.

الخلاصة

تقع رسوم Flutter المتحركة في ثلاثة مستويات: ضمني (تلقائي، مدفوع بالحالة)، صريح (يدوي، مدفوع بالمتحكم)، وفيزيائي (مدفوع بالمحاكاة، مدرك للسرعة). ابدأ بالودجات الضمنية مثل AnimatedOpacity للانتقالات البسيطة، والجأ إلى AnimationController حين تحتاج تحكماً، واستخدم محاكاة الفيزياء للحركة المبادَرة بالإيماءة التي يجب أن تبدو طبيعية. في الدروس القادمة ستُتقن كل مستوى بأمثلة أكثر تعقيداً وواقعية.