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

رسوم Rive المتحركة: الرسوم التفاعلية بآلة الحالة

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

رسوم Rive المتحركة: الرسوم التفاعلية بآلة الحالة

Rive هي أداة تصميم ورسوم متحركة تفاعلية تعمل في الوقت الحقيقي، وتنتج ملفات .riv مدمجة تُستهلك مباشرةً بواسطة Flutter. على عكس الصور المتحركة التقليدية أو ملفات Lottie بصيغة JSON، تحتوي رسوم Rive المتحركة على آلة حالة — وهي أوتوماتون حالة محدودة تحدد الحالات والانتقالات والمدخلات. يتيح لك ذلك قيادة رسوم متحركة معقدة ومتشعبة بالكامل من كود Dart دون شحن مئات صور الإطارات.

لماذا نستخدم Rive بدلاً من مناهج الرسوم المتحركة الأخرى؟

  • آلات الحالة تستبدل منطق الرسوم المتحركة الشرطي في Dart بعقد رسم بياني يتحكم فيها المصمم.
  • حجم الملف — عادةً ما تزن رسوم الشخصية الغنية 20–80 كيلوبايت بتنسيق .riv مقارنةً بعدة ميجابايت من إطارات الرسوم المتحركة.
  • الاستيفاء في الوقت الحقيقي — يقوم مصير Rive باستيفاء القيم بين الحالات في وقت التشغيل، مما ينتج انتقالات سلسة للغاية دون تحضير إطارات مسبقاً.
  • الربط ثنائي الاتجاه — تتدفق المدخلات المنطقية ومدخلات التشغيل والأرقام من Dart إلى آلة الحالة؛ ويمكن قراءة الحالة النشطة للآلة في المقابل.
ملاحظة: تُؤلَّف آلات حالة Rive في محرر Rive. تصمم الفن وتحدد الرسم البياني للحالة هناك؛ كود Dart فقط يرسل المدخلات ويتفاعل مع الأحداث.

الخطوة 1 — إضافة التبعية والأصل

أضف rive إلى pubspec.yaml وأعلن ملف .riv الخاص بك كأصل Flutter:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  rive: ^0.13.0          # استخدم أحدث إصدار مستقر

flutter:
  assets:
    - assets/animations/button.riv

شغّل flutter pub get لجلب الحزمة. ضع button.riv داخل assets/animations/.

الخطوة 2 — تحميل ملف .riv وربط متحكم آلة الحالة

النهج الموصى به هو تحميل بايتات الأصل في initState، وتحليل RiveFile، وتحديد موقع اللوحة الفنية التي تريدها، وربط StateMachineController. يعرض المتحكم مدخلات مسماة تخزنها كحقول حتى تتمكن من تشغيلها لاحقاً.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

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

  @override
  State<RiveButtonDemo> createState() => _RiveButtonDemoState();
}

class _RiveButtonDemoState extends State<RiveButtonDemo> {
  // مرجع اللوحة الفنية المستخدمة بواسطة RiveAnimation.direct
  Artboard? _artboard;

  // المدخلات المسترجعة من آلة الحالة
  SMITrigger? _pressTrigger;
  SMIBool? _isHovered;

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

  Future<void> _loadRive() async {
    // 1. قراءة بايتات الأصل
    final data = await rootBundle.load('assets/animations/button.riv');

    // 2. تحليل ملف Rive
    final file = RiveFile.import(data);

    // 3. الحصول على اللوحة الفنية الافتراضية (أو المسماة)
    final artboard = file.mainArtboard;

    // 4. إيجاد آلة الحالة بالاسم وربطها
    final controller = StateMachineController.fromArtboard(
      artboard,
      'ButtonMachine',       // يجب أن يطابق الاسم في محرر Rive
      onStateChange: _onStateChange,
    );

    if (controller != null) {
      artboard.addController(controller);

      // 5. تخزين المدخلات مؤقتاً لتشغيلها لاحقاً
      _pressTrigger = controller.findInput<SMITrigger>('Press');
      _isHovered    = controller.findInput<SMIBool>('Hovered');
    }

    // 6. تشغيل إعادة البناء مع اللوحة الفنية المحملة
    setState(() => _artboard = artboard);
  }

  void _onStateChange(String machineName, String stateName) {
    debugPrint('آلة الحالة "$machineName" دخلت الحالة "$stateName"');
  }

  @override
  Widget build(BuildContext context) {
    if (_artboard == null) return const CircularProgressIndicator();

    return GestureDetector(
      onTapDown: (_) => _pressTrigger?.fire(),
      onTapUp:   (_) => _isHovered?.value = false,
      child: SizedBox(
        width: 200,
        height: 80,
        child: RiveAnimation.direct(_artboard!),
      ),
    );
  }
}
نصيحة: تحقق دائماً من أن controller != null قبل استخدامه. تُرجع StateMachineController.fromArtboard قيمة null عندما لا يتطابق اسم الآلة — وهو فشل صامت من السهل تفويته أثناء التطوير.

أنواع مدخلات آلة الحالة

يعرض Rive ثلاثة بدائل مدخلات تُعيَّن على أنواع Dart:

  • SMITrigger — نبضة أحادية الاستخدام؛ استدعِ .fire() لإرسال حدث عابر (مثل ضغطة زر أو قفز).
  • SMIBool — منطقي مستمر؛ اضبط .value = true/false (مثل حالة التمرير، علامة النشاط).
  • SMINumber — فاصلة عائمة مستمرة؛ اضبط .value = 0.75 (مثل نسبة الصحة، مزج السرعة).

الخطوة 3 — تشغيل الانتقالات من مدخلات المستخدم

ربط إيماءات Flutter بمدخلات Rive أمر مباشر. يوضح المثال أدناه مفتاح تبديل تتم معالجة حالات الخمول والتمرير والتبديل فيه جميعاً بواسطة آلة الحالة — لا يدير كود Dart الخاص بك سوى قيمة SMIBool:

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

  @override
  State<RiveToggle> createState() => _RiveToggleState();
}

class _RiveToggleState extends State<RiveToggle> {
  Artboard? _artboard;
  SMIBool? _isOn;

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

  Future<void> _init() async {
    final bytes = await rootBundle.load('assets/animations/toggle.riv');
    final file  = RiveFile.import(bytes);
    final board = file.mainArtboard;

    final ctrl = StateMachineController.fromArtboard(board, 'ToggleMachine');
    if (ctrl != null) {
      board.addController(ctrl);
      _isOn = ctrl.findInput<SMIBool>('IsOn');
    }
    setState(() => _artboard = board);
  }

  void _toggle() {
    if (_isOn != null) {
      _isOn!.value = !_isOn!.value;   // قلب قيمة المدخل المنطقي
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_artboard == null) {
      return const SizedBox(width: 80, height: 40);
    }
    return GestureDetector(
      onTap: _toggle,
      child: SizedBox(
        width: 80,
        height: 40,
        child: RiveAnimation.direct(_artboard!),
      ),
    );
  }
}
تحذير: لا تستدعِ setState() داخل رد الاتصال onStateChange لتشغيل إعادة بناء واجهة المستخدم الثقيلة — يُطلق رد الاتصال على خيط العرض. استخدمه للتأثيرات الجانبية الخفيفة (التحليلات، نغمات الصوت). إذا كان يجب عليك تحديث حالة Flutter، أرسل الاستدعاء عبر WidgetsBinding.instance.addPostFrameCallback.

الفرق بين RiveAnimation.network و RiveAnimation.asset و RiveAnimation.direct

  • RiveAnimation.asset('assets/anim.riv', stateMachines: ['Machine']) — الأبسط؛ لا يلزم متحكم للتشغيل الأساسي، لكن لا يمكنك قيادة المدخلات.
  • RiveAnimation.network(url) — يبث ملف .riv بعيداً؛ نفس القيد.
  • RiveAnimation.direct(_artboard!) — مطلوب عند الحاجة إلى StateMachineController للرسوم المتحركة المدفوعة بالمدخلات.

ملخص

يتطلب دمج رسوم Rive المتحركة بآلة الحالة في Flutter أربع خطوات منسقة: إضافة حزمة rive والأصل، وتحميل ملف .riv وتحليله في وقت التشغيل، وربط StateMachineController باسم الآلة، وتخزين المدخلات المكتوبة (SMITrigger وSMIBool وSMINumber) مؤقتاً لتشغيلها أو ضبطها استجابةً لإيماءات المستخدم. تفصل هذه البنية بشكل نظيف بين تصميم الرسوم المتحركة (محرر Rive) ومنطق التفاعل (Dart)، مما ينتج حركة معبرة يقودها المصمم دون تضخيم قاعدة الكود.