CustomPainter เป็นคลาสนามธรรมจาก Flutter SDK ที่ให้นักพัฒนาควบคุมการเรนเดอร์กราฟิก 2D แบบกำหนดเองบน Canvas ได้อย่างสมบูรณ์ นักพัฒนาสร้างคลาสย่อยของ CustomPainter และใช้งานสองเมธอดที่จำเป็น: paint(Canvas, Size) และ shouldRepaint(covariant CustomPainter oldDelegate) ตามเอกสาร Flutter API (2026) CustomPainter ถูกใช้ร่วมกับวิดเจ็ต CustomPaint ซึ่งมอบหมายการเรนเดอร์ให้กับออบเจ็กต์ CustomPainter ที่ส่งให้ในแต่ละเฟรมของแอนิเมชันหรือการเปลี่ยนแปลงสถานะ
ประเด็นสำคัญ
CustomPainter เป็นคลาสพื้นฐานของ Flutter สำหรับการใช้งานกราฟิก 2D แบบกำหนดเอง มันแทนที่วิธีการโอเวอร์ไรด์ 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() — เป็นคลาสที่กำหนดสไตล์การวาด: สี (color), ความหนาของเส้น (strokeWidth), สไตล์ (style: fill หรือ stroke), ความโปร่งใส, โหมดการผสม (blendMode) และมาสก์การลดรอยหยัก 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 แทนที่จะวาดใหม่ในทุกเฟรมโดยไม่จำเป็น การแคช Rect สำหรับเกรเดียนต์ที่ซับซ้อน: หากสีของเกรเดียนต์ไม่เปลี่ยนแปลง คุณสามารถแคช 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 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม