Canvas trong Android — là một lớp từ gói android.graphics cung cấp API để kết xuất 2D trên View. Canvas hoạt động như một tấm vải mà trên đó nhà phát triển vẽ các hình dạng hình học, văn bản, hình ảnh và đường dẫn bằng các phương thức draw* khác nhau. Theo Tài liệu dành cho nhà phát triển Android (2025), Canvas là công cụ chính để kết xuất tùy chỉnh trong Android và được sử dụng cùng với lớp Paint để định nghĩa kiểu dáng và màu sắc. Mỗi lần gọi onDraw() nhận được một phiên bản Canvas sẵn sàng, được liên kết với bộ đệm bitmap của view.
Các điểm chính
Canvas — là lớp đồ họa 2D trung tâm trong Android cung cấp các phương thức để vẽ trên một mặt phẳng. Mỗi Canvas được gắn với một Bitmap — vùng bộ nhớ nơi kết quả của các thao tác vẽ được ghi lại. Khi một View được vẽ lại, hệ thống truyền vào onDraw() một Canvas đã được liên kết với bộ đệm của View đó.
Canvas hoạt động trong hệ tọa độ nơi (0,0) là góc trên cùng bên trái, trục X hướng sang phải, trục Y hướng xuống dưới. Tất cả các thao tác vẽ sử dụng trạng thái hiện tại của Canvas, bao gồm ma trận biến đổi, vùng cắt và kiểu Paint. Theo Tài liệu Android (2025), Canvas API bao gồm hơn 30 phương thức draw* cho các loại nguyên thủy đồ họa khác nhau.
Từ Android 3.0 (API 11), Canvas hỗ trợ tăng tốc phần cứng thông qua DisplayList. Ở chế độ tăng tốc, các thao tác Canvas được ghi vào DisplayList và sau đó thực thi trên GPU. Điều này mang lại hiệu suất tăng đáng kể nhưng áp đặt các hạn chế: một số thao tác Canvas (clipPath với clip không phải Path) không được hỗ trợ ở chế độ tăng tốc.
Canvas cung cấp một tập hợp các phương thức để vẽ các nguyên thủy cơ bản. Mỗi phương thức chấp nhận các tham số hình học và một đối tượng Paint định nghĩa kiểu trực quan.
| Phương thức | Mục đích | Ví dụ |
|---|---|---|
| drawRect() | Vẽ hình chữ nhật | canvas.drawRect(10f, 10f, 100f, 100f, paint) |
| drawCircle() | Vẽ hình tròn | canvas.drawCircle(50f, 50f, 30f, paint) |
| drawLine() | Vẽ đường thẳng | canvas.drawLine(0f, 0f, 100f, 100f, paint) |
| drawText() | Vẽ văn bản | canvas.drawText(“Hello”, 10f, 20f, paint) |
| drawBitmap() | Vẽ hình ảnh | canvas.drawBitmap(bitmap, 0f, 0f, paint) |
| drawPath() | Vẽ đường dẫn tùy ý | canvas.drawPath(path, paint) |
| drawOval() | Vẽ hình bầu dục | canvas.drawOval(rectF, paint) |
| drawArc() | Vẽ cung hoặc hình quạt | canvas.drawArc(rectF, 0f, 90f, true, paint) |
Mỗi phương thức có thể được gọi theo bất kỳ thứ tự nào — Canvas vẽ theo thứ tự các lời gọi, xếp chồng các phần tử mới lên trên các phần tử trước đó. Đối với các bố cục phức tạp, kỹ thuật kết xuất nhiều lớp với lưu và khôi phục trạng thái Canvas được sử dụng.
Paint — là lớp định nghĩa cách Canvas sẽ vẽ. Không có Paint, các phương thức Canvas vô dụng — nó đặt màu sắc, độ dày đường kẻ, kiểu tô, kích thước văn bản, hiệu ứng (bóng, chuyển màu, làm mờ) và chế độ hòa trộn.
Paint có các thuộc tính chính: color (màu sắc), style (FILL, STROKE hoặc FILL_AND_STROKE), strokeWidth (độ dày nét vẽ), textSize (kích thước văn bản), isAntiAlias (làm mịn), shader (chuyển màu hoặc hình ảnh). Tạo một đối tượng Paint là thao tác tốn kém, vì vậy nên được tạo một lần và tái sử dụng.
private val fillPaint = Paint().apply {
color = Color.parseColor("#FF6200EE")
style = Paint.Style.FILL
isAntiAlias = true
}
private val strokePaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 4f
isAntiAlias = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(100f, 100f, 80f, fillPaint)
canvas.drawCircle(100f, 100f, 80f, strokePaint)
}
Trong ví dụ này, hai đối tượng Paint được sử dụng: một để tô hình tròn với màu tím, cái thứ hai cho đường viền trắng. Tách biệt tô và đường viền là thực hành tiêu chuẩn cho phép kiểm soát linh hoạt giao diện mà không cần tạo lại đối tượng.
Đối với tô màu chuyển tiếp, Paint chấp nhận Shader — các lớp con LinearGradient, RadialGradient, SweepGradient. Shader được gắn vào Paint thông qua phương thức setShader() và định nghĩa cách màu sắc thay đổi trên vùng vẽ.
Canvas hỗ trợ các biến đổi ma trận của hệ tọa độ: translate() (dịch chuyển), rotate() (xoay), scale() (thu phóng) và skew() (nghiêng). Các biến đổi tích lũy: mỗi lần gọi tiếp theo được áp dụng lên trên các lần gọi trước.
Việc quản lý ngăn xếp biến đổi được thực hiện thông qua save() và restore(). save() lưu trạng thái hiện tại của Canvas (ma trận, vùng cắt) vào ngăn xếp, restore() khôi phục trạng thái đã lưu gần nhất. Giữa save() và restore(), các biến đổi lồng nhau có thể được áp dụng.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save() // Save initial state
canvas.translate(100f, 100f) // Translate origin
// First square
canvas.drawRect(0f, 0f, 50f, 50f, fillPaint)
canvas.save() // Save after translate
canvas.rotate(45f, 25f, 25f) // Rotate around square center
// Rotated square
canvas.drawRect(0f, 0f, 50f, 50f, strokePaint)
canvas.restore() // Restore after rotate
canvas.restore() // Restore initial state
}
Luôn sử dụng save() trước khi bắt đầu các biến đổi và restore() sau khi hoàn thành chúng. Các cặp save/restore mất cân bằng dẫn đến tích lũy ma trận và kết xuất không chính xác các phần tử tiếp theo.
Đối với đồ họa phức tạp, Canvas được kết hợp với Bitmap và Path. Bitmap được sử dụng để tải hình ảnh và lưu đệm các khung hình đã kết xuất. Path được sử dụng để xây dựng các đường cong Bezier tùy ý, đa giác và các đường viền phức tạp.
Kỹ thuật kết xuất ngoài màn hình: một Bitmap được tạo, một cảnh phức tạp được vẽ lên đó thông qua một Canvas riêng, và sau đó Bitmap hoàn chỉnh được xuất ra trong onDraw() thông qua drawBitmap(). Điều này cho phép cập nhật đồ họa phức tạp mà không cần vẽ lại toàn bộ mỗi khung hình.
private var offscreenBitmap: Bitmap? = null
private var offscreenCanvas: Canvas? = null
fun buildOffscreenScene(width: Int, height: Int) {
offscreenBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
offscreenCanvas = Canvas(offscreenBitmap)
// Render scene once
offscreenCanvas!!.drawColor(Color.WHITE)
drawGrid(offscreenCanvas!!)
drawComplexShape(offscreenCanvas!!)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
offscreenBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
}
Path được sử dụng để xây dựng các đường viền phức tạp: các đường cong Bezier (quadTo, cubicTo), cung (arcTo), đường thẳng (lineTo). Path có thể được đóng (close()) cho các hình dạng tô màu hoặc mở cho các đường thẳng. Phương thức chính drawPath() kết hợp Path và Paint để kết xuất cuối cùng.
Hiệu suất kết xuất Canvas ảnh hưởng trực tiếp đến FPS của ứng dụng. Các vấn đề chính: cấp phát quá mức trong onDraw(), gọi invalidate() thường xuyên và sử dụng Tăng tốc phần cứng không tối ưu.
Paint, Path, Rect, RectF và các đối tượng khác nên được tạo một lần trong bộ khởi tạo hoặc phương thức setup(). Mỗi lần cấp phát trong onDraw() tạo áp lực lên bộ thu gom rác, dẫn đến các micro-lag. Ngoại lệ là static hoặc companion object cho các đối tượng bất biến.
Nếu phần lớn View bị ẩn bên ngoài vùng hiển thị (ví dụ, trong ScrollView), hãy sử dụng canvas.clipRect() để giới hạn kết xuất. Tăng tốc phần cứng trong trường hợp này tối ưu hóa thêm việc cắt các pixel không nhìn thấy.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Clip to visible area
canvas.save()
canvas.clipRect(scrollX, scrollY, scrollX + width, scrollY + height)
// Render large scene
renderScene(canvas)
canvas.restore()
}
Sử dụng View.setLayerType() với LAYER_TYPE_HARDWARE cho các View có đồ họa Canvas phức tạp ít khi được cập nhật. Đối với hoạt ảnh cập nhật thường xuyên, ngược lại, sử dụng LAYER_TYPE_NONE để tránh cập nhật lớp phần cứng tốn kém. Hãy cân đo bằng GPU Profile Rendering và Android Studio Profiler.
Câu hỏi thường gặp
Có, bạn có thể tạo Canvas(Bitmap) — một hàm tạo chấp nhận Bitmap. Điều này được sử dụng cho kết xuất ngoài màn hình: bạn vẽ lên Bitmap, sau đó xuất nó qua drawBitmap(). Canvas cũng có thể được tạo từ Surface (cho SurfaceView) hoặc thông qua Picture.
Canvas — là API để vẽ trên View trong luồng UI. SurfaceView — là một Surface riêng biệt có thể được cập nhật từ luồng nền thông qua Canvas riêng của nó (lockCanvas() / unlockCanvasAndPost()). SurfaceView được ưu tiên cho trò chơi và video.
Nguyên nhân phổ biến nhất là các cặp save() và restore() mất cân bằng. Nếu sau khi biến đổi (translate, rotate) không gọi restore(), Canvas vẫn ở trong hệ tọa độ đã bị thay đổi và các phần tử tiếp theo được vẽ với độ lệch. Hãy kiểm tra các cặp save()/restore() của bạn.
Sử dụng Paint.setTextAlign(Paint.Align.CENTER) để canh giữa theo chiều ngang và Paint.getTextBounds() hoặc Paint.descent() / ascent() cho chiều dọc. Canh giữa: canvas.drawText(văn bản, centerX, centerY - (ascent + descent) / 2, paint).
Bản thân Canvas không tạo hoạt ảnh — nó chỉ vẽ trạng thái hiện tại. Hoạt ảnh đạt được thông qua các lời gọi liên tiếp tới invalidate() (hoặc postInvalidateOnAnimation()), mỗi lần vẽ một khung hình mới trong onDraw(). Để có hoạt ảnh mượt mà, hãy sử dụng ValueAnimator hoặc Choreographer để đồng bộ với Vsync.
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