onDraw() — là phương thức quan trọng của lớp View trong Android SDK, chịu trách nhiệm vẽ nội dung của view. Hệ thống gọi onDraw mỗi khi giao diện của View được cập nhật — khi kết xuất ban đầu, sau invalidate() hoặc khi kích thước thay đổi. Theo Android Developers Documentation (2026), việc ghi đè onDraw bằng Canvas API là cách chính để tạo đồ họa tùy chỉnh trong các ứng dụng Android.
Điểm chính
onDraw(Canvas canvas) — là một phương thức protected của lớp android.view.View, được hệ thống Android gọi khi View cần được vẽ. Nhà phát triển ghi đè phương thức này để triển khai việc vẽ tùy chỉnh: hình dạng, văn bản, hình ảnh, gradient, hoạt ảnh. Tham số Canvas là bề mặt đồ họa mà trên đó tất cả các thao tác vẽ được thực hiện.
Chữ ký của phương thức rất đơn giản: override fun onDraw(canvas: Canvas). Bên trong phương thức này không thể tạo các đối tượng mới (để tránh tải lên GC), thực hiện các thao tác lâu dài hoặc làm việc với mạng. View cũng chứa phương thức onDraw trong vòng đời của nó giữa onLayout (tính toán vị trí) và dispatchDraw (vẽ các phần tử con).
Khi kế thừa từ View, việc ghi đè onDraw là bắt buộc để hiển thị bất kỳ nội dung nào — nếu không View sẽ trống rỗng. Khi kế thừa từ các lớp con hiện có (TextView, ImageView, Button), việc ghi đè onDraw là tùy chọn và được dùng để thêm đồ họa bổ sung lên trên nội dung chuẩn.
Android thực hiện việc vẽ View theo một trình tự xác định nghiêm ngặt, được gọi từ luồng chính (UI thread). Vòng đầy đủ gồm ba giai đoạn: measure (đo kích thước), layout (tính toán vị trí) và draw (vẽ). onDraw là một phần của giai đoạn draw và được gọi sau khi kích thước cũng như vị trí của View đã được xác định.
draw(Canvas canvas) — là một phương thức public của View, gọi onDraw và dispatchDraw. Phương thức draw không dành cho việc ghi đè — thay vào đó, onDraw được ghi đè. Sau khi thực hiện onDraw cho View hiện tại, dispatchDraw được gọi, gọi đệ quy draw cho tất cả các View con (nếu View hiện tại là một thùng chứa ViewGroup).
onDraw được gọi trong các điều kiện sau: 1) View lần đầu tiên trở nên hiển thị (sau setVisibility(VISIBLE)); 2) ứng dụng gọi invalidate() cho View này hoặc cha của nó; 3) kích thước View thay đổi (layout mới); 4) hoạt ảnh liên quan đến View thay đổi (qua ValueAnimator hoặc ObjectAnimator); 5) trạng thái của View thay đổi (pressed, focused, enabled).
Từ Android 3.0 (API 11), hầu hết các thao tác Canvas được thực hiện với tăng tốc phần cứng qua GPU. Nhà phát triển có thể kiểm tra xem việc tăng tốc có được bật hay không qua canvas.isHardwareAccelerated(). Khi tăng tốc phần cứng, một số thao tác Canvas không được hỗ trợ (ví dụ: clipPath với vùng không phải hình chữ nhật) và có thể bị bỏ qua hoặc gây ra lỗi.
Canvas cung cấp một bộ phương thức phong phú để vẽ. Tất cả các phương thức hoạt động trong hệ tọa độ hiện tại, có thể được thay đổi qua các phép biến đổi (translate, rotate, scale, skew). Mỗi phương thức nhận một đối tượng Paint, xác định màu sắc, kiểu, độ dày đường kẻ, hiệu ứng và chống răng cưa.
| Phương thức Canvas | Mục đích | Các tham số chính của Paint |
|---|---|---|
| drawCircle | Vẽ hình tròn | centerX, centerY, radius, paint |
| drawRect | Vẽ hình chữ nhật | left, top, right, bottom, paint |
| drawLine | Vẽ đường kẻ | startX, startY, endX, endY, paint |
| drawPath | Vẽ đường viền tùy ý | path, paint |
| drawBitmap | Vẽ hình ảnh | bitmap, srcRect, dstRect, paint |
| drawText | Vẽ văn bản | text, x, y, paint |
| drawArc | Vẽ cung hoặc hình quạt | ovalRect, startAngle, sweepAngle, useCenter, paint |
| drawOval | Vẽ hình bầu dục | ovalRect, paint |
Paint xác định kiểu vẽ: màu sắc (setColor), độ dày đường kẻ (setStrokeWidth), kiểu tô (setStyle — FILL, STROKE, FILL_AND_STROKE), chống răng cưa (setAntiAlias), hiệu ứng (setShadowLayer, setMaskFilter) và gradient (setShader). Nên tạo và cấu hình Paint một lần trong constructor của View, thay vì bên trong onDraw, để tránh tạo đối tượng trong vòng vẽ.
Canvas sử dụng hệ tọa độ trong đó trục X hướng sang phải, còn trục Y hướng xuống dưới. Gốc tọa độ (0, 0) — góc trên bên trái của View. Trước khi gọi các phương thức draw, nhà phát triển có thể thay đổi hệ tọa độ qua canvas.translate(dx, dy), canvas.rotate(degrees, px, py), canvas.scale(sx, sy, px, py). Tất cả các thao tác vẽ tiếp theo sẽ được thực hiện trong hệ tọa độ đã biến đổi.
onDraw được thực hiện trong luồng chính UI, và thời lượng của nó ảnh hưởng trực tiếp đến tốc độ khung hình (fps). Để duy trì 60 fps, phương thức onDraw phải hoàn thành trong không quá 16 mili giây. Bất kỳ sự chậm lại nào dẫn đến việc rớt khung hình (jank), được nhìn nhận trực quan như hoạt ảnh bị giật hoặc độ trễ khi cuộn.
Các quy tắc chính để tối ưu onDraw bao gồm: 1) không tạo đối tượng bên trong onDraw — tất cả Paint, Path, Rect phải là các trường của lớp; 2) không cấp phát bộ nhớ (new) — điều này gây ra thu gom rác (GC) và rớt khung hình; 3) lưu vào bộ nhớ đệm các thao tác phức tạp trong Bitmap qua Bitmap.createBitmap và vẽ Bitmap đã sẵn sàng; 4) không thực hiện các thao tác nhập/xuất hoặc tải tài nguyên.
clipRect giới hạn vùng vẽ bằng hình chữ nhật xác định. Được dùng để Canvas không mất thời gian vẽ các điểm ảnh nằm ngoài vùng nhìn thấy. Đặc biệt hiệu quả khi cuộn danh sách lớn hoặc phóng to/thu nhỏ: hãy đặt clipRect cho vùng nhìn thấy (dirty rect), và Canvas sẽ chỉ vẽ những gì nhìn thấy được.
canvas.save() lưu trạng thái hiện tại của Canvas (phép biến đổi và clip) vào ngăn xếp. canvas.restore() khôi phục trạng thái đã lưu gần nhất. Điều này cần thiết cho các phép biến đổi tạm thời: lưu trạng thái, áp dụng translate/rotate, vẽ, sau đó khôi phục. save và restore rất nhẹ và không ảnh hưởng đến hiệu suất khi sử dụng hợp lý (dưới 10–15 cấp lồng nhau).
invalidate() — là phương thức View, thông báo cho hệ thống rằng nội dung của View đã lỗi thời và cần được vẽ lại. Việc gọi invalidate khởi tạo một vòng draw mới, trong đó onDraw sẽ được gọi. invalidate chỉ nên được gọi từ luồng UI (main thread). Để gọi từ luồng nền, sử dụng postInvalidate.
postInvalidate() — là phiên bản an toàn luồng của invalidate, gửi yêu cầu vẽ lại vào hàng đợi của luồng chính qua Handler. Được dùng trong các tác vụ nền khi cần cập nhật View sau khi hoàn tất việc tải dữ liệu hoặc tính toán. Kết hợp với runOnUiThread để đồng bộ với UI.
Đối với việc vẽ lại từng phần, sử dụng invalidate(Rect dirty) hoặc invalidate(int l, int t, int r, int b). Điều này cho hệ thống biết rằng chỉ vùng xác định thay đổi, không phải toàn bộ View. Android tối ưu việc vẽ, chỉ vẽ lại dirty rect, giúp tăng đáng kể hiệu suất trong các hoạt ảnh có cập nhật từng phần.
Hãy xem các ví dụ thực tế về việc ghi đè onDraw để tạo View tùy chỉnh trong Android bằng Kotlin.
Ví dụ tạo một chỉ báo tiến trình hình tròn với phần điền được hoạt hình hóa. onDraw vẽ hai hình tròn: nền màu xám và hình tròn màu được điền, cung của nó được xác định bởi tiến trình hiện tại. Việc gọi invalidate trong setProgress đảm bảo vẽ lại khi giá trị thay đổi.
class CircularProgressView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private val bgPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 12f
color = Color.parseColor("#E0E0E0")
isAntiAlias = true
}
private val progressPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 12f
color = Color.BLUE
isAntiAlias = true
strokeCap = Paint.Cap.ROUND
}
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f)
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = minOf(width, height) - 24f
val oval = RectF(12f, 12f,
12f + size, 12f + size)
canvas.drawArc(oval, 270f, 360f, false, bgPaint)
canvas.drawArc(oval, 270f,
progress * 360f,
false, progressPaint)
}
}
Ví dụ vẽ một hình chữ nhật với gradient và văn bản phía trên. Gradient được tạo qua LinearGradient (Shader), còn văn bản — qua drawText. Cả hai Paint được tạo một lần trong constructor, còn trong onDraw chỉ thay đổi kích thước của hình chữ nhật tùy theo chiều rộng hiện tại của View.
class GradientTextView(context: Context)
: View(context) {
private val shaderPaint = Paint().apply {
isAntiAlias = true
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 48f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val gradient = LinearGradient(
0f, 0f, width.toFloat(), 0f,
Color.parseColor("#FF6B35"),
Color.parseColor("#FF0040"),
Shader.TileMode.CLAMP
)
shaderPaint.shader = gradient
canvas.drawRect(0f, 0f,
width.toFloat(),
height.toFloat(), shaderPaint)
val xCenter = width / 2f
val yCenter = height / 2f
val yOffset = (textPaint.descent() +
textPaint.ascent()) / 2f
canvas.drawText("Xin chào từ Canvas",
xCenter, yCenter - yOffset,
textPaint)
}
}
Ví dụ tạo một đồng hồ hoạt hình, cập nhật việc vẽ mỗi 1000 mili giây. postDelayed trong onDraw gọi invalidate sau một giây, tạo vòng lặp vẽ lại vô hạn. Các kim đồng hồ được vẽ qua drawLine với độ dài và độ dày khác nhau.
class AnalogClockView(context: Context)
: View(context) {
private val handPaint = Paint().apply {
style = Paint.Style.STROKE
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
val radius = minOf(cx, cy) - 20f
val now = Calendar.getInstance()
val hours = now.get(Calendar.HOUR) % 12
val minutes = now.get(Calendar.MINUTE)
val seconds = now.get(Calendar.SECOND)
val secondAngle = Math.toRadians(
seconds * 6f - 90f)
handPaint.strokeWidth = 2f
handPaint.color = Color.RED
canvas.drawLine(cx, cy,
cx + cos(secondAngle) * radius * 0.8f,
cy + sin(secondAngle) * radius * 0.8f,
handPaint)
postDelayed({ invalidate() }, 1000)
}
}
Các câu hỏi thường gặp
Không, không nên gọi trực tiếp onDraw. Để yêu cầu vẽ lại, hãy sử dụng invalidate() (từ luồng UI) hoặc postInvalidate() (từ luồng nền). Hệ thống tự quyết định khi nào gọi onDraw và tối ưu việc vẽ, kết hợp nhiều invalidate thành một lượt vẽ.
Nguyên nhân: View bị ẩn (setVisibility(GONE) hoặc INVISIBLE); View chưa được thêm vào phân cấp của Window; kích thước View bằng 0 (width hoặc height = 0); gọi setWillNotDraw(true) (mặc định là true đối với ViewGroup). Hãy kiểm tra từng điểm này khi gỡ lỗi.
Ở 60 fps, onDraw có thể được gọi tới 60 lần mỗi giây. Ở 120 fps (trên các thiết bị có màn hình 120 Hz) — tới 120 lần. Mỗi lần gọi phải nằm trong khoảng ~8–16 mili giây. Khi có hoạt ảnh chủ động qua ObjectAnimator hoặc ValueAnimator, tần suất được xác định bởi số khung hình của hoạt ảnh.
onDraw chịu trách nhiệm vẽ nội dung của chính View. dispatchDraw chịu trách nhiệm vẽ các View con (trong ViewGroup). dispatchDraw được gọi sau onDraw và đệ quy đi qua tất cả các phần tử con. Đối với View thông thường (không phải ViewGroup), dispatchDraw rỗng.
Có, Canvas có thể được tạo cho Bitmap qua Canvas(bitmap) và vẽ trên đó ở bất kỳ luồng nào. Điều này được dùng để tính toán trước đồ họa phức tạp trong nền. Tuy nhiên, Canvas được truyền vào onDraw thuộc về hệ thống và chỉ có thể được sử dụng trong luồng UI bên trong phương thức onDraw.
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