الودجات المخصصة والرسم المخصص

تحريك الرسامين المخصصين

16 دقيقة الدرس 9 من 12

تحريك الرسامين المخصصين

يرسم CustomPainter مرةً واحدةً افتراضيًا. لجعله متحركًا، تحتاج إلى تشغيل إعادات رسمه من كائن Animation. المفتاح هو تمرير الأنيميشن إلى الرسّام والإعلان عنه بوصفه مُخطِر إعادة الرسم — يستدعي Flutter عندها paint() تلقائيًا في كل نبضة أنيميشن دون إعادة بناء شجرة الودجات.

ملاحظة: الأنيميشن في Flutter هو تدفق قيم: يُنتج AnimationController قيمة double من 0.0 إلى 1.0 عبر الزمن. يحوّل CurvedAnimation أو Tween تلك القيمة إلى أي نوع أو نطاق تحتاجه. يقرأ رسّامك القيمة الحالية ويرسم بناءً عليها.

وسيطة repaint

يمتلك CustomPainter وسيطتين اختياريتين في مُنشئه للتحكم في إعادات الرسم:

  • repaintListenable (مثل Animation) يُطلق إعادة رسم عند كل إطلاق. هذه هي نقطة الربط القياسية للأنيميشن.
  • shouldRepaint — يُستدعى عند إعادة بناء الودجت الأب وتمرير نسخة جديدة من الرسّام؛ أعد true إذا كانت النسخة الجديدة ستُنتج رسمًا مختلفًا.

بتمرير repaint: animation، تفصل دورة إعادة الرسم عن دورة إعادة البناء. يشترك ودجت CustomPaint في Listenable ويجدول إعادة رسم في كل نبضة — دون الحاجة إلى setState.

ربط أنيميشن بـ CustomPainter

class _ProgressRingPainter extends CustomPainter {
  final Animation<double> animation;

  // تمرير الأنيميشن بوصفه مُخطِر إعادة الرسم
  const _ProgressRingPainter({required this.animation})
      : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    final progress = animation.value; // 0.0 → 1.0
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2 - 8;

    // مسار الخلفية
    final trackPaint = Paint()
      ..color = const Color(0xFFE0E0E0)
      ..strokeWidth = 10
      ..style = PaintingStyle.stroke;
    canvas.drawCircle(center, radius, trackPaint);

    // القوس المتحرك
    final arcPaint = Paint()
      ..color = const Color(0xFF2196F3)
      ..strokeWidth = 10
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -3.14159 / 2,               // البداية من الأعلى (−90°)
      2 * 3.14159 * progress,     // زاوية الكنس تُقاد بالأنيميشن
      false,
      arcPaint,
    );
  }

  @override
  bool shouldRepaint(_ProgressRingPainter old) =>
      old.animation != animation;
}

استضافة الرسّام في StatefulWidget

يمتلك StatefulWidget AnimationController. يجب أن تمزج SingleTickerProviderStateMixin (أو TickerProviderStateMixin لأكثر من متحكم) حتى يستطيع Flutter تشغيل العقارب الزمنية. تخلص من المتحكم في dispose() لتجنب التسريب.

ودجت حلقة التقدم الكاملة

class ProgressRing extends StatefulWidget {
  final double targetProgress; // 0.0 إلى 1.0
  const ProgressRing({super.key, required this.targetProgress});

  @override
  State<ProgressRing> createState() => _ProgressRingState();
}

class _ProgressRingState extends State<ProgressRing>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1200),
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
    _controller.animateTo(widget.targetProgress);
  }

  @override
  void didUpdateWidget(ProgressRing old) {
    super.didUpdateWidget(old);
    if (old.targetProgress != widget.targetProgress) {
      _controller.animateTo(widget.targetProgress);
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: const Size(120, 120),
      painter: _ProgressRingPainter(animation: _animation),
    );
  }
}

أنيميشن الموجة: دمج قيم متعددة

يُشغّل AnimationController واحد مضبوط على repeat() موجةً متكررة. بقراءة controller.value بوصفه إزاحة طور داخل paint()، تُزيح مسار موجة جيبية في كل نبضة لإنتاج وهم الماء المتدفق.

رسّام الموجة باستخدام أنيميشن متكرر

class _WavePainter extends CustomPainter {
  final Animation<double> animation;
  final Color waveColor;

  const _WavePainter({required this.animation, required this.waveColor})
      : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    final phase = animation.value * 2 * 3.14159; // 0 → 2π لكل دورة
    final amplitude = size.height * 0.08;
    final midY = size.height * 0.55;

    final path = Path()..moveTo(0, midY);
    for (double x = 0; x <= size.width; x++) {
      final y = midY +
          amplitude * _sin((2 * 3.14159 * x / size.width) - phase);
      path.lineTo(x, y);
    }
    path
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height)
      ..close();

    canvas.drawPath(path, Paint()..color = waveColor);
  }

  double _sin(double radians) {
    // في الإنتاج: import 'dart:math'; return sin(radians);
    return 0; // بديل — استبدله بـ sin() من dart:math
  }

  @override
  bool shouldRepaint(_WavePainter old) =>
      old.waveColor != waveColor || old.animation != animation;
}

// الاستخدام: controller.repeat() لتكرار الموجة إلى ما لا نهاية
// _controller = AnimationController(vsync: this,
//     duration: const Duration(seconds: 2))
//   ..repeat();
نصيحة: استورد دائمًا dart:math في كود الإنتاج لاستخدام sin() وcos() وpi. استبدل الأرقام السحرية مثل 3.14159 بـ pi من تلك المكتبة للدقة والقابلية للقراءة.

shouldRepaint مقابل repaint

تخدم هاتان الطريقتان أغراضًا مختلفة وكثيرًا ما يُخلط بينهما:

  • repaint (Listenable) — يُستدعى في كل نبضة أنيميشن لجدولة إعادة رسم حتى لو لم يُعد بناء الودجت. هذا ما يُشغّل الأنيميشن السلس.
  • shouldRepaint(old) — يُستدعى فقط عند إعادة بناء الأب وإنشاء رسّام جديد. أعد true إذا كان الرسّام الجديد سيُنتج صورة مختلفة. أعد false لتخطي إعادة رسم غير ضرورية.
تحذير: لا تُنفّذ تخصيصات مكلفة (مثل إنشاء كائنات Paint أو Path) داخل CustomPainter على مستوى الحقل بتهيئة late لكل إطار. بدلًا من ذلك، خصّص ما تحتاجه فقط داخل paint() أو خزّن الكائنات التي لا تتغير بين الإطارات. سيتعامل مجمّع القمامة في Dart مع الكائنات القصيرة العمر بكفاءة في معظم الحالات، لكن التخزين المؤقت يساعد في سيناريوهات التردد العالي.

اعتبارات الأداء

  • استخدم repaint: animation بدلًا من setState — يتجنب إعادة بناء شجرة الودجات في كل إطار.
  • اضبط isComplex: true على CustomPaint إذا كان استدعاء الرسم مكلفًا — سيُنقّط Flutter الطبقة ويُخزّنها.
  • اضبط willChange: true عندما يتغير الرسّام كثيرًا — يتجنب Flutter تخزين طبقة يعلم أنها ستُبطل فورًا.
  • استخدم canvas.clipRect لتقييد الرسم بالمنطقة المرئية وتقليل التلوين الزائد.

ملخص

يتطلب تحريك CustomPainter ثلاثة عناصر مترابطة: AnimationController يمتلكه StatefulWidget، وAnimation (مُنحنى أو مُحوَّل اختياريًا) يُمرَّر إلى الرسّام، ووسيطة repaint: animation التي تُشترك اللوحة في تدفق قيم الأنيميشن. تقراءة animation.value داخل paint() تمنحك قيمةً مُحدَّثة باستمرار لتشغيل أقواس وأمواج وتعبئات وأي رسم مخصص آخر. يُوضّح مثالان أساسيان — حلقة تقدم وموجة متكررة — كيف ينطبق نفس النمط على الأنيميشن ذي المرة الواحدة والأنيميشن المتكرر.