Kodokon kodokon.com

الرسوم المتحركة: الضمنية والصريحة من الداخل

افهم الآلية الداخلية للرسوم المتحركة الضمنية، وتحكّم بدقة في AnimationController الخاص بك.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

ينتمي AnimatedContainer إلى عائلة الرسوم المتحركة الضمنية: فأنت تغيّر قيمة هدف ويتولّى إطار العمل تحريك الانتقال. من الداخل، يحتفظ كل ImplicitlyAnimatedWidget بـ AnimationController مخفيّ في حالته. وعندما يصل تكوين جديد، يمرّ didUpdateWidget عبر الخصائص من خلال forEachTween ويعيد توجيه كل تدرّج (tween): فيصبح الحدّ begin هو القيمة المُستوفَاة (interpolated) الحالية، والحدّ end هو الهدف الجديد، ثم يعيد المتحكّم الانطلاق من الصفر. ونتيجة قيّمة لذلك: تغيير الهدف في منتصف الحركة لا يُسبّب أيّ قفزة بصرية - فتستأنف الحركة من حيث كانت تمامًا.

DART
import 'package:flutter/material.dart';

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

  @override
  State<SelectableCard> createState() =>
      _SelectableCardState();
}

class _SelectableCardState extends State<SelectableCard> {
  bool _selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () =>
          setState(() => _selected = !_selected),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOutCubic,
        width: _selected ? 220 : 160,
        height: 96,
        decoration: BoxDecoration(
          color: _selected
              ? Colors.indigo
              : Colors.indigo.shade200,
          borderRadius: BorderRadius.circular(
            _selected ? 28 : 12,
          ),
        ),
      ),
    );
  }
}
الحجم واللون ونصف القطر، جميعها مُحرَّكة من setState واحد.

تغطّي الرسوم الضمنية الخصائص التي يتوقّعها إطار العمل، ويُعمِّم TweenAnimationBuilder العملية لتشمل أيّ قيمة قابلة للاستيفاء (interpolation). وحالما تحتاج إلى قيادة الحركة - إيقاف مؤقت، أو عكس، أو تكرار، أو تفاعل مع الحالة - انتقل إلى AnimationController الصريح. يتوقّع الوسيط vsync قيمة من نوع TickerProvider: فيُنشئ الـ mixin كائن Ticker، وهو ردّ نداء يستدعيه SchedulerBinding في كل إطار. وثمة دقّة قليلة الشيوع: TickerMode يُخرِس مؤقّتات (tickers) شجرة فرعية مخفية (تحت مسار مغطّى مثلًا). فيستمر الزمن في التدفق، لكن لا يعمل أيّ ردّ نداء بعد ذلك - أيْ صفر إهدار لوحدة المعالجة المركزية على رسوم متحركة غير مرئية.

DART
import 'package:flutter/material.dart';

class RevealPanel extends StatefulWidget {
  const RevealPanel({super.key, required this.child});

  final Widget child;

  @override
  State<RevealPanel> createState() => _RevealPanelState();
}

class _RevealPanelState extends State<RevealPanel>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 400),
    reverseDuration: const Duration(milliseconds: 200),
  );

  late final Animation<double> _opacity = _controller
      .drive(CurveTween(curve: Curves.easeOutCubic));

  void toggle() {
    switch (_controller.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        _controller.reverse();
      case AnimationStatus.reverse:
      case AnimationStatus.dismissed:
        _controller.forward();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: toggle,
      child: FadeTransition(
        opacity: _opacity,
        child: widget.child,
      ),
    );
  }
}
reverseDuration: للذهاب وللإياب كلٌّ إيقاعه الخاص.

المتحكّم ما هو إلا Animation<double> يسير من 0 إلى 1: ركّبه بدلًا من استهلاكه خامًا. فـ controller.drive(CurveTween(...)) يُنتِج حركة مشتقّة دون أيّ حالة إضافية. ولتطبيق القيمة على الشجرة، فضِّل ودجات Transition (FadeTransition، RotationTransition): فهي تُنصِت إلى الحركة مباشرةً على مستوى كائن العرض (render object)، دون إعادة بناء أيّ ودجت في كل إطار. وعندما يكون التحويل اعتباطيًا، يؤدّي AnimatedBuilder المهمّة - شريطة أن تمرّر الشجرة الفرعية الثابتة عبر child: إذ تُبنى مرة واحدة فقط ثم يُعاد حقنها في builder في كل إطار. وهذا هو التحسين الأعلى مردودًا للرسوم المتحركة الصريحة.

DART
import 'dart:math' as math;

import 'package:flutter/material.dart';

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

  @override
  State<Orbit> createState() => _OrbitState();
}

class _OrbitState extends State<Orbit>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(seconds: 4),
  )..repeat();

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      child: const Icon(Icons.satellite_alt, size: 32),
      builder: (context, child) {
        final angle = _controller.value * 2 * math.pi;
        return Transform.translate(
          offset: Offset(
            math.cos(angle) * 80,
            math.sin(angle) * 80,
          ),
          child: child,
        );
      },
    );
  }
}
الأيقونة المُمرَّرة عبر child تُبنى مرة واحدة فقط.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. يتلقّى AnimatedContainer لونًا هدفًا جديدًا بينما لا تزال الحركة السابقة في منتصفها. ماذا يحدث؟
    • يُعاد توجيه التدرّج (tween): فيعيد الانطلاق من القيمة الحالية نحو الهدف الجديد، دون أيّ قفزة بصرية
    • تنتهي الحركة الجارية أولًا، ثم تبدأ الحركة الجديدة
    • يقفز الودجت فورًا إلى اللون الجديد
  2. ما الغرض الدقيق من الوسيط vsync في AnimationController؟
    • يوفّر Ticker متزامنًا مع الإطارات، يمكن لـ TickerMode أن يُخرِسه عندما تكون الشجرة الفرعية مخفية
    • يزامن المتحكّم مع الرسوم المتحركة الأخرى في التطبيق
    • يقصر الحركة على 30 إطارًا في الثانية
    • يُفعِّل التسريع العتادي لوحدة معالجة الرسوميات (GPU)
  3. لماذا تمرّر الشجرة الفرعية الثابتة عبر الوسيط child في AnimatedBuilder؟
    • تُبنى مرة واحدة ثم يُعاد حقنها في builder، بدلًا من إعادة بنائها في كل إطار
    • إنه إلزامي: فلا يستطيع builder إرجاع ودجات جديدة
    • يحرّر المتحكّم تلقائيًا في النهاية