CustomPainter là một lớp trừu tượng từ Flutter SDK, cung cấp cho nhà phát triển toàn quyền kiểm soát việc kết xuất đồ họa 2D tùy chỉnh trên Canvas. Nhà phát triển tạo một lớp con của CustomPainter và triển khai hai phương thức bắt buộc: paint(Canvas, Size) và shouldRepaint(covariant CustomPainter oldDelegate). Theo Tài liệu API Flutter (2026), CustomPainter được sử dụng cùng với widget CustomPaint, widget này ủy quyền kết xuất cho đối tượng CustomPainter được cung cấp trên mỗi khung hình hoạt ảnh hoặc thay đổi trạng thái.
Các điểm chính
CustomPainter là lớp cơ sở của Flutter để triển khai đồ họa 2D tùy chỉnh. Nó thay thế cách tiếp cận ghi đè onDraw trong Android, cung cấp mô hình kết xuất linh hoạt và khai báo hơn. Nhà phát triển tạo một lớp con của CustomPainter, ghi đè phương thức paint nơi họ nhận được Canvas và Size, và thực hiện tất cả các thao tác vẽ trên Canvas. Widget CustomPaint quản lý việc gọi paint và tích hợp kết quả vào cây widget.
Sự khác biệt chính giữa CustomPainter và vẽ trực tiếp qua Canvas là sự tách biệt giữa logic kết xuất (paint) và trình kích hoạt vẽ lại (shouldRepaint). Hệ thống Flutter gọi shouldRepaint mỗi khi widget cha thay đổi. Nếu shouldRepaint trả về false, paint không được gọi, tiết kiệm tài nguyên. Cơ chế này tương tự shouldRebuild trong RenderObject và là một phần của kiến trúc Flutter với ba cây (Widget, Element, RenderObject).
CustomPaint là một widget chấp nhận các tham số painter (lớp nền) và foregroundPainter (lớp nổi). Cả hai tham số đều chấp nhận đối tượng CustomPainter. Painter nền được vẽ trước widget con của CustomPaint, và foregroundPainter được vẽ sau. Điều này cho phép sử dụng CustomPaint như một container có nền trang trí hoặc lớp phủ mà không làm mất khả năng nhúng các widget thông thường bên trong.
Canvas là đối tượng vẽ trung tâm trong Flutter, bao bọc bề mặt đồ họa. Nó cung cấp khoảng 40 phương thức vẽ: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints và các phương thức khác. Canvas trong Flutter tương ứng với một lớp tương tự trong Android Canvas nhưng được điều chỉnh cho kiến trúc Dart và Flutter Engine. Canvas hoạt động trong hệ tọa độ nơi gốc (0, 0) là góc trên cùng bên trái.
Paint — không nhầm lẫn với phương thức paint() — là một lớp xác định kiểu vẽ: màu sắc (color), độ dày nét vẽ (strokeWidth), kiểu (style: fill hoặc stroke), độ trong suốt, chế độ hòa trộn (blendMode) và mặt nạ khử răng cưa. Paint được cấu hình trước khi vẽ và được truyền vào các phương thức Canvas. Quan trọng: Paint là đối tượng có thể thay đổi, nhưng nên tạo nó một lần và tái sử dụng, chỉ thay đổi các tham số cần thiết.
| Phương thức Canvas | Mục đích | Tham số |
|---|---|---|
| drawCircle | Vẽ hình tròn | Offset center, double radius, Paint paint |
| drawLine | Vẽ đoạn thẳng | Offset p1, Offset p2, Paint paint |
| drawRect | Vẽ hình chữ nhật | Rect rect, Paint paint |
| drawPath | Vẽ đường dẫn tùy ý | Path path, Paint paint |
| drawArc | Vẽ cung hoặc hình quạt | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Hình chữ nhật góc bo tròn | RRect rrect, Paint paint |
| drawOval | Vẽ hình bầu dục | Rect rect, Paint paint |
| drawImage | Vẽ hình ảnh từ Image | Image image, Offset p, Paint paint |
Canvas hỗ trợ biến đổi hệ tọa độ: translate(double dx, double dy) dịch chuyển gốc tọa độ; rotate(double radians) xoay hệ thống; scale(double sx, double sy) thay đổi tỷ lệ; skew(double sx, double sy) làm nghiêng. Trước khi biến đổi, nên lưu trạng thái Canvas qua save() và khôi phục sau khi hoàn thành qua restore(). Điều này ngăn chặn sự tích lũy biến đổi trong các lần gọi paint sau đó.
paint(Canvas canvas, Size size) là phương thức bắt buộc triển khai tất cả logic vẽ. Tham số canvas là bề mặt đồ họa để vẽ. Tham số size là kích thước vùng được phân bổ để vẽ (chiều rộng và chiều cao tính bằng pixel logic). Canvas được cấu hình sẵn sao cho gốc (0,0) tương ứng với góc trên cùng bên trái của vùng vẽ. Tất cả tọa độ được chỉ định tương đối so với gốc này.
shouldRepaint(covariant CustomPainter oldDelegate) là phương thức bắt buộc tối ưu hóa tần suất vẽ lại. Flutter gọi shouldRepaint mỗi khi cập nhật cây widget ảnh hưởng đến CustomPainter. Nếu phương thức trả về false, hình ảnh đã kết xuất được coi là hợp lệ và paint không được gọi. Một triển khai điển hình là: return oldDelegate.someField != someField — so sánh các trường của painter cũ và mới để xác định nhu cầu vẽ lại.
Đối với hình ảnh hoạt ảnh hoặc thay đổi liên tục (đồng hồ, chỉ báo tải, biểu đồ), shouldRepaint nên trả về true ở mỗi thay đổi trạng thái. Nếu đối tượng tĩnh (logo, biểu tượng, nền), shouldRepaint trả về false sau lần kết xuất đầu tiên. Flutter tự động lưu vào bộ nhớ đệm kết quả paint và chỉ vẽ lại khi kích thước, độ trong suốt hoặc lời gọi shouldRepaint thay đổi.
Hãy tạo GaugePainter — một đồng hồ đo hình tròn tùy chỉnh với phần tô màu động. CustomPainter chấp nhận giá trị hiện tại (tiến trình từ 0.0 đến 1.0) và vẽ một cung với gradient. Phương thức shouldRepaint so sánh giá trị tiến trình để tối ưu hóa. CustomPaint được đặt trong cây widget với tham số size để giới hạn vùng vẽ.
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;
}
}
Ví dụ vẽ biểu đồ sóng bằng Path. Path cho phép xây dựng các đường viền tùy ý từ đường thẳng, đường cong Bezier và cung. Sau khi xây dựng đường viền, nó được tô màu hoặc viền bằng đường kẻ. Trong ví dụ, biểu đồ được xây dựng từ một mảng điểm mô phỏng dạng sóng.
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 (nền) được vẽ trước widget con của CustomPaint. Nếu CustomPaint có widget con (tham số child), painter nền được hiển thị bên dưới nó. Điều này phù hợp cho trang trí: thanh tiến trình, họa tiết nền, hình mờ. foregroundPainter được vẽ sau widget con, tức là bên trên nó. Nó được sử dụng cho lớp phủ: điểm nhấn, mặt nạ, thước đo, chú thích.
Sự khác biệt giữa painter và foregroundPainter rất quan trọng đối với thứ tự Z. Nếu bạn cần vẽ thứ gì đó phía sau nội dung — hãy sử dụng painter. Nếu phía trên nội dung — foregroundPainter. Trong một số trường hợp, bạn có thể sử dụng cả hai painter cùng lúc: nền qua painter, nhãn lớp phủ qua foregroundPainter. Điều này cho phép tách logic của các lớp đồ họa khác nhau thành hai lớp CustomPainter riêng biệt.
Tham số size trong phương thức paint giống nhau cho cả hai painter và được xác định bởi kích thước vùng được phân bổ cho CustomPaint. Kích thước có thể được đặt rõ ràng qua tham số size trong CustomPaint hoặc ngầm định qua container cha. Nếu kích thước là Size.zero, paint không được gọi — Flutter bỏ qua kết xuất cho vùng có kích thước bằng không. Hành vi này cần được xem xét khi tạo hoạt ảnh xuất hiện phần tử.
Nguyên tắc chính về hiệu suất của CustomPainter là không tạo đối tượng bên trong phương thức paint. Canvas, Paint, Path, Rect, Offset và các đối tượng vẽ khác nên được tạo trong hàm tạo của CustomPainter hoặc được lưu vào bộ nhớ đệm. Tạo đối tượng bên trong paint dẫn đến cấp phát bộ nhớ trên mỗi khung hình, điều này ở 60 khung hình mỗi giây gây ra thu gom rác thường xuyên và giật hoạt ảnh (jank).
Lưu vào bộ nhớ đệm Path tăng tốc đáng kể việc kết xuất các đường dẫn phức tạp lặp lại. Nếu đường dẫn không thay đổi giữa các khung hình (ví dụ: lưới nền), hãy tạo Path một lần và sử dụng lại nhiều lần. RepaintBoundary là một widget bao bọc ngăn chặn việc vẽ lại CustomPainter khi cây widget phía trên thay đổi. Nếu CustomPainter tĩnh, hãy bọc nó trong RepaintBoundary để cách ly.
Đối với CustomPainter có hoạt ảnh với việc vẽ lại thường xuyên, hãy sử dụng Ticker hoặc AnimationController với shouldRepaint trả về true ở mỗi thay đổi của giá trị hoạt ảnh. Tối ưu nhất là so sánh trường hoạt ảnh trong shouldRepaint thay vì vẽ lại trên mỗi khung hình không cần thiết. Lưu vào bộ nhớ đệm Rect cho gradient phức tạp: nếu màu gradient không thay đổi, bạn có thể lưu vào bộ nhớ đệm Shader qua Paint.shader.
Các câu hỏi thường gặp
CustomPainter là một lớp hợp đồng xác định vẽ gì và vẽ như thế nào trên Canvas. Canvas là đối tượng vẽ, cung cấp các phương thức (drawCircle, drawLine, v.v.). CustomPainter nhận Canvas qua phương thức paint và kiểm soát việc sử dụng nó. Một Canvas có thể được sử dụng bởi các CustomPainter khác nhau vào các thời điểm khác nhau.
Không, CustomPainter không có ý nghĩa nếu không có CustomPaint. CustomPaint là widget gọi phương thức paint của CustomPainter và hiển thị kết quả trên màn hình. Nếu không có CustomPaint, Canvas sẽ không được tạo và paint sẽ không được thực thi. Tuy nhiên, bạn có thể triển khai RenderObject của riêng mình với Canvas, nhưng đây là cách tiếp cận cấp thấp hơn.
Sử dụng AnimationController với Ticker, truyền giá trị hoạt ảnh vào CustomPainter qua tham số hàm tạo. Khi giá trị thay đổi, gọi setState() trong widget chứa CustomPaint, điều này kích hoạt lời gọi shouldRepaint. Nếu shouldRepaint trả về true, hệ thống gọi paint cho khung hình hoạt ảnh mới.
Nguyên nhân hầu như luôn là shouldRepaint trả về false. Hãy kiểm tra xem các trường của oldDelegate có được so sánh chính xác với các giá trị mới hay không. Nếu sử dụng đối tượng có thể thay đổi, shouldRepaint có thể không phát hiện thay đổi vì tham chiếu đối tượng vẫn giữ nguyên. Sử dụng == hoặc đối tượng bất biến để so sánh chính xác.
Có, CustomPainter được sử dụng để kết xuất trang trí tùy chỉnh trong danh sách phức tạp. Tuy nhiên, lưu ý rằng khi sắp xếp lại các mục, RepaintBoundary bên trong ListView lưu vào bộ nhớ đệm kết xuất của từng mục. CustomPainter phải nhẹ và không chứa các thao tác nặng trong paint để không làm chậm hoạt ảnh kéo thả.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm