تحريك الرسامين المخصصين
تحريك الرسامين المخصصين
يرسم CustomPainter مرةً واحدةً افتراضيًا. لجعله متحركًا، تحتاج إلى تشغيل إعادات رسمه من كائن Animation. المفتاح هو تمرير الأنيميشن إلى الرسّام والإعلان عنه بوصفه مُخطِر إعادة الرسم — يستدعي Flutter عندها paint() تلقائيًا في كل نبضة أنيميشن دون إعادة بناء شجرة الودجات.
AnimationController قيمة double من 0.0 إلى 1.0 عبر الزمن. يحوّل CurvedAnimation أو Tween تلك القيمة إلى أي نوع أو نطاق تحتاجه. يقرأ رسّامك القيمة الحالية ويرسم بناءً عليها.وسيطة repaint
يمتلك CustomPainter وسيطتين اختياريتين في مُنشئه للتحكم في إعادات الرسم:
- repaint —
Listenable(مثل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() تمنحك قيمةً مُحدَّثة باستمرار لتشغيل أقواس وأمواج وتعبئات وأي رسم مخصص آخر. يُوضّح مثالان أساسيان — حلقة تقدم وموجة متكررة — كيف ينطبق نفس النمط على الأنيميشن ذي المرة الواحدة والأنيميشن المتكرر.