CustomPainter هي فئة مجردة من Flutter SDK تمنح المطورين تحكمًا كاملاً في عرض الرسومات ثنائية الأبعاد المخصصة على Canvas. ينشئ المطور فئة فرعية من CustomPainter وينفذ طريقتين إلزاميتين: paint(Canvas, Size) و shouldRepaint(covariant CustomPainter oldDelegate). وفقًا لوثائق Flutter API (2026)، يُستخدم CustomPainter مع عنصر واجهة CustomPaint، الذي يفوض الرسم إلى كائن CustomPainter المُمرر في كل إطار رسوم متحركة أو تغيير حالة.
الملخص
CustomPainter هي فئة أساسية في Flutter لتنفيذ الرسومات ثنائية الأبعاد المخصصة. تحل محل نهج تجاوز onDraw في Android، مما يوفر نموذج عرض أكثر مرونة وتصريحية. ينشئ المطور فئة فرعية من CustomPainter، ويتجاوز طريقة paint حيث يتلقى Canvas و Size، وينفذ جميع عمليات الرسم على Canvas. يدير عنصر واجهة CustomPaint استدعاء paint ويدمج النتيجة في شجرة عناصر الواجهة.
الفرق الرئيسي بين CustomPainter والرسم المباشر عبر Canvas هو فصل منطق العرض (paint) عن محفز إعادة الرسم (shouldRepaint). يستدعي نظام Flutter shouldRepaint في كل مرة يتغير فيها عنصر الواجهة الأب. إذا أعاد shouldRepaint القيمة false، فلن يتم استدعاء paint، مما يوفر الموارد. هذه الآلية مشابهة لـ shouldRebuild في RenderObject وهي جزء من بنية Flutter بأشجارها الثلاثة (Widget و Element و RenderObject).
CustomPaint هو عنصر واجهة يقبل معاملي painter (الطبقة الخلفية) و foregroundPainter (الطبقة الأمامية). يقبل كلا المعاملين كائنات CustomPainter. يُرسَم painter الخلفي قبل عنصر الواجهة التابع لـ CustomPaint، بينما يُرسَم foregroundPainter بعده. يتيح ذلك استخدام CustomPaint كحاوية ذات خلفية زخرفية أو تراكب دون فقدان القدرة على تضمين عناصر الواجهة العادية بداخله.
Canvas هو الكائن المركزي للرسم في Flutter، الذي يغلف سطح الرسم. يوفر حوالي 40 طريقة رسم: drawLine و drawCircle و drawRect و drawPath و drawArc و drawImage و drawPicture و drawPoints وغيرها. يتوافق Canvas في Flutter مع فئة مماثلة في Android Canvas ولكنه مُكيَّف مع بنية Dart و Flutter Engine. يعمل Canvas في نظام إحداثيات حيث الأصل (0, 0) هو الزاوية العلوية اليسرى.
Paint — الذي لا ينبغي الخلط بينه وبين طريقة paint() — هو فئة تُحدد نمط الرسم: اللون، سمك الخط، النمط (تعبئة أو حدود)، الشفافية، وضع المزج، وقناع التنعيم. Paint يتم تكوينه قبل الرسم ويُمرر إلى طرق Canvas. مهم: Paint هو كائن قابل للتغيير، لكن يُنصح بإنشائه مرة واحدة وإعادة استخدامه، مع تغيير المعاملات الضرورية فقط.
| طريقة Canvas | الغرض | المعاملات |
|---|---|---|
| drawCircle | رسم دائرة | Offset center, double radius, Paint paint |
| drawLine | رسم قطعة خطية | Offset p1, Offset p2, Paint paint |
| drawRect | رسم مستطيل | Rect rect, Paint paint |
| drawPath | رسم مسار عشوائي | Path path, Paint paint |
| drawArc | رسم قوس أو قطاع | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | مستطيل بزوايا مدورة | RRect rrect, Paint paint |
| drawOval | رسم بيضاوي | Rect rect, Paint paint |
| drawImage | رسم صورة من Image | Image image, Offset p, Paint paint |
Canvas يدعم تحويلات نظام الإحداثيات: translate(double dx, double dy) يزوح الأصل؛ rotate(double radians) يدير النظام؛ scale(double sx, double sy) يغير المقياس؛ skew(double sx, double sy) يميل. قبل إجراء تحويل، يُنصح بحفظ حالة Canvas عبر save() واستعادتها بعد الانتهاء عبر restore(). يمنع ذلك تراكم التحويلات في استدعاءات paint اللاحقة.
paint(Canvas canvas, Size size) هي الطريقة الإلزامية التي تنفذ كل منطق الرسم. معامل canvas هو سطح الرسم. معامل size هو حجم المنطقة المخصصة للرسم (العرض والارتفاع بالبكسل المنطقي). Canvas مُكوَّن مسبقًا بحيث يتوافق الأصل (0,0) مع الزاوية العلوية اليسرى لمنطقة الرسم. جميع الإحداثيات تُحدد بالنسبة لهذا الأصل.
shouldRepaint(covariant CustomPainter oldDelegate) هي طريقة إلزامية تُحسّن تواتر إعادة الرسم. يستدعي Flutter shouldRepaint في كل تحديث لشجرة عناصر الواجهة يؤثر على CustomPainter. إذا أعادت الطريقة false، تُعتبر الصورة المرسومة بالفعل صالحة ولا يُستدعى paint. التنفيذ النموذجي: return oldDelegate.someField != someField — مقارنة حقول painter القديم والجديد لتحديد الحاجة إلى إعادة الرسم.
للصور المتحركة أو المتغيرة باستمرار (الساعات، مؤشرات التحميل، الرسوم البيانية) يجب أن يعيد shouldRepaint القيمة true عند كل تغيير حالة. إذا كان الكائن ثابتًا (شعار، أيقونة، خلفية)، يُعيد shouldRepaint القيمة false بعد الرسم الأول. يخزن Flutter تلقائيًا نتيجة paint في الذاكرة المؤقتة ويعيد الرسم فقط عند تغيير الحجم أو الشفافية أو استدعاء shouldRepaint.
لننشئ GaugePainter — مقياس دائري مخصص بتعبئة متحركة. يقبل CustomPainter القيمة الحالية (التقدم من 0.0 إلى 1.0) ويرسم قوسًا بتدرج لوني. تقارن طريقة shouldRepaint قيمة التقدم لتحسين الأداء. CustomPaint يُوضع في شجرة عناصر الواجهة مع معامل size لتحديد منطقة الرسم.
class GaugePainter extends CustomPainter {
final double progress;
final Color startColor;
final Color endColor;
GaugePainter({
required this.progress,
this.startColor = Colors.blue,
this.endColor = Colors.cyan,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2,
size.height / 2);
final radius = center.dx - 12;
final bgPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
final progressPaint = Paint()
..shader = LinearGradient(
colors: [startColor, endColor],
).createShader(
Rect.fromCircle(
center, radius))
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint);
canvas.drawArc(
Rect.fromCircle(center, radius),
-math.pi / 2,
2 * math.pi * progress,
false, progressPaint);
}
@override
bool shouldRepaint(
GaugePainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
يرسم المثال رسمًا بيانيًا موجيًا باستخدام Path. يتيح Path بناء خطوط عشوائية من خطوط ومنحنيات بيزيه وأقواس. بعد بناء الخط، يتم تعبئته باللون أو تحديده بخط. في المثال، يُبنى الرسم البياني من مصفوفة نقاط تحاكي شكلًا موجيًا.
class WavePainter extends CustomPainter {
final List<double> values;
WavePainter({required this.values});
@override
void paint(Canvas canvas, Size size) {
final path = Path();
final dx = size.width /
(values.length - 1);
path.moveTo(0, size.height);
for (var i = 0; i < values.length; i++) {
final x = i * dx;
final y = size.height *
(1 - values[i]);
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.close();
final fillPaint = Paint()
..color = Colors.blue.withOpacity(0.3)
..style = PaintingStyle.fill;
canvas.drawPath(path, fillPaint);
}
@override
bool shouldRepaint(
WavePainter oldDelegate) {
return oldDelegate.values != values;
}
}
painter (الخلفية) يُرسم قبل عنصر الواجهة التابع لـ CustomPaint. إذا كان لدى CustomPaint عنصر واجهة تابع (معامل child)، يُعرض painter الخلفي أسفله. هذا مناسب للزخارف: أشرطة التقدم، أنماط الخلفية، العلامات المائية. foregroundPainter يُرسم بعد عنصر الواجهة التابع، أي فوقه. يُستخدم للتراكبات: الإضاءات، الأقنعة، مساطر القياس، التعليقات.
الفرق بين painter و foregroundPainter حاسم للترتيب Z. إذا كنت بحاجة لرسم شيء خلف المحتوى — استخدم painter. إذا كان فوق المحتوى — foregroundPainter. في بعض الحالات، يمكن استخدام كلا painter في وقت واحد: الخلفية عبر painter، تراكب التسمية عبر foregroundPainter. يتيح ذلك فصل منطق طبقات الرسم المختلفة إلى فئتي CustomPainter منفصلتين.
معامل size في طريقة paint هو نفسه لكلا painter ويُحدد بحجم المنطقة المخصصة لـ CustomPaint. يمكن تعيين الحجم صراحة عبر معامل size في CustomPaint أو ضمنيًا عبر الحاوية الأم. إذا كان الحجم هو Size.zero، فلن يُستدعى paint — يتجاوز Flutter الرسم لمنطقة حجمها صفر. يجب مراعاة هذا السلوك عند تحريك ظهور العنصر.
القاعدة الرئيسية لأداء CustomPainter هي عدم إنشاء كائنات داخل طريقة paint. يجب إنشاء Canvas و Paint و Path و Rect و Offset وكائنات الرسم الأخرى في مُنشئ CustomPainter أو تخزينها مؤقتًا. يؤدي إنشاء كائنات داخل paint إلى تخصيص ذاكرة في كل إطار، مما يسبب عند 60 إطارًا في الثانية جمعًا متكررًا للقمامة وتقطعات في الحركة (jank).
التخزين المؤقت لـ Path يُسرّع بشكل كبير عرض المسارات المعقدة المتكررة. إذا لم يتغير المسار بين الإطارات (مثل شبكة خلفية)، أنشئ Path مرة واحدة وأعد استخدامه عدة مرات. RepaintBoundary هو عنصر واجهة مغلف يمنع إعادة رسم CustomPainter عند تغير شجرة عناصر الواجهة الأعلى. إذا كان CustomPainter ثابتًا، لفه في RepaintBoundary للعزل.
لـ CustomPainter المتحركة ذات إعادة الرسم المتكررة، استخدم Ticker أو AnimationController مع shouldRepaint الذي يعيد true عند كل تغيير في القيمة المتحركة. من الأمثل مقارنة الحقل المتحرك في shouldRepaint بدلاً من إعادة الرسم في كل إطار دون داعٍ. التخزين المؤقت للمستطيل للتدرجات المعقدة: إذا لم تتغير ألوان التدرج، يمكنك تخزين Shader مؤقتًا عبر Paint.shader.
الأسئلة الشائعة
CustomPainter هي فئة عقد تُحدد ماذا وكيف ترسم على Canvas. Canvas هو كائن الرسم نفسه، الذي يوفر الطرق (drawCircle و drawLine وما إلى ذلك). يتلقى CustomPainter Canvas عبر طريقة paint ويتحكم في استخدامه. يمكن استخدام Canvas واحد من قبل CustomPainter مختلفة في أوقات مختلفة.
لا، ليس لـ CustomPainter معنى بدون CustomPaint. CustomPaint هو عنصر الواجهة الذي يستدعي طريقة paint لـ CustomPainter ويعرض النتيجة على الشاشة. بدون CustomPaint، لن يتم إنشاء Canvas ولن يتم تنفيذ paint. يمكنك، مع ذلك، تنفيذ RenderObject خاص بك مع Canvas، لكن هذا نهج منخفض المستوى.
استخدم AnimationController مع Ticker، مررًا القيمة المتحركة إلى CustomPainter عبر معاملات المُنشئ. عندما تتغير القيمة، استدع setState() في عنصر الواجهة الذي يحتوي على CustomPaint، مما يُحفز استدعاء shouldRepaint. إذا أعاد shouldRepaint true، يستدعي النظام paint للإطار الجديد للرسوم المتحركة.
السبب دائمًا تقريبًا هو shouldRepaint الذي يعيد false. تحقق مما إذا كانت حقول oldDelegate تُقارن بشكل صحيح مع القيم الجديدة. إذا تم استخدام كائن قابل للتغيير، فقد لا يلاحظ shouldRepaint التغييرات لأن مرجع الكائن يظل كما هو. استخدم == أو كائنات غير قابلة للتغيير للمقارنة الصحيحة.
نعم، يُستخدم CustomPainter لعرض الزخارف المخصصة في القوائم المعقدة. ومع ذلك، ضع في اعتبارك أنه عند إعادة ترتيب العناصر، يخزن RepaintBoundary داخل ListView عرض كل عنصر مؤقتًا. يجب أن يكون CustomPainter خفيفًا وألا يحتوي على عمليات ثقيلة في paint لتجنب إبطاء حركة السحب.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.