Custom View là một thành phần giao diện tùy chỉnh trong Android, được tạo bằng cách kế thừa từ lớp View hoặc các lớp con của nó (Button, TextView, ImageView) và ghi đè các phương thức chính: onDraw để vẽ, onMeasure để đo kích thước và onTouchEvent để xử lý chạm. Theo Google Android Developer Guide (2024), Custom View được sử dụng khi các thành phần Android SDK tiêu chuẩn không cung cấp hành vi hoặc giao diện mong muốn — ví dụ: hoạt ảnh tùy chỉnh, hình dạng không chuẩn, biểu đồ chuyên dụng và các yếu tố trò chơi.
Các điểm chính
Custom View là một lớp kế thừa android.view.View (hoặc một trong các lớp con của nó) và ghi đè các phương thức hệ thống để triển khai logic vẽ, đo lường và xử lý sự kiện tùy chỉnh. Custom View là cơ chế cơ bản của Android để tạo các thành phần UI độc đáo.
Android cung cấp hai cách tiếp cận để tạo Custom View: kế thừa từ View (vẽ hoàn toàn tùy chỉnh qua onDraw) và kế thừa từ một lớp con View hiện có (ví dụ: ghi đè Button hoặc TextView để thêm chức năng trong khi vẫn giữ hành vi cơ bản).
Hàm tạo cơ bản của Custom View phải chấp nhận Context và AttributeSet — điều này cho phép hệ thống tạo View từ mã đánh dấu XML. Nếu View chỉ được sử dụng từ mã, một hàm tạo với Context là đủ. Hàm tạo thứ ba với kiểu là cần thiết để hỗ trợ chủ đề Android.
class CustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
strokeWidth = 4f
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f,
height / 2f,
50f, paint)
}
}
Custom View tối thiểu bao gồm một hàm tạo và onDraw. Tuy nhiên, để hoạt động chính xác trong hệ thống Layout của Android, onMeasure cũng phải được ghi đè — nếu không View có thể hiển thị với kích thước bằng không hoặc phản ứng không chính xác với tham số layout wrap_content.
Theo Google I/O 2023, Custom Views được sử dụng trong 65% ứng dụng trong top 100 Google Play. Các lý do phổ biến nhất để tạo Custom View: hoạt ảnh tùy chỉnh (35%), biểu đồ và sơ đồ tùy chỉnh (25%), các yếu tố điều khiển chuyên dụng (20%) và các thành phần có thương hiệu (20%).
onMeasure là phương thức mà View báo cáo kích thước mong muốn của nó dựa trên MeasureSpec nhận được (ràng buộc từ cha). Nếu không có triển khai onMeasure đúng, View có thể có chiều cao bằng không với wrap_content hoặc chiếm toàn bộ diện tích màn hình với match_parent.
MeasureSpec bao gồm một chế độ (EXACTLY, AT_MOST, UNSPECIFIED) và một giá trị. EXACTLY — cha đã đặt kích thước chính xác (match_parent hoặc giá trị cố định). AT_MOST — cha đã đặt tối đa (wrap_content). UNSPECIFIED — không có ràng buộc (ScrollView, ListView).
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val desiredWidth = paddingLeft + paddingRight
+ DEFAULT_WIDTH
val desiredHeight = paddingTop + paddingBottom
+ DEFAULT_HEIGHT
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
val resultWidth = when (modeWidth) {
MeasureSpec.EXACTLY -> measuredWidth
MeasureSpec.AT_MOST ->
desiredWidth.coerceAtMost(measuredWidth)
else -> desiredWidth
}
val resultHeight = resolveSize(desiredHeight,
heightMeasureSpec)
setMeasuredDimension(resultWidth, resultHeight)
}
resolveSize là một phương thức tiện ích của Android giúp đơn giản hóa việc triển khai onMeasure. Nó nhận kích thước mong muốn và MeasureSpec và trả về giá trị chính xác: cho EXACTLY — kích thước chính xác, cho AT_MOST — tối thiểu giữa mong muốn và tối đa, cho UNSPECIFIED — kích thước mong muốn.
onLayout cho View (không phải ViewGroup) thường không được ghi đè — nó được cha gọi để định vị các phần tử con. Đối với ViewGroup, onLayout là bắt buộc — bạn cần gọi layout() cho mỗi View con.
onDraw là trái tim của Custom View. Phương thức này được Android gọi mỗi khi View được vẽ lại. Một đối tượng Canvas được truyền cho nó, qua đó tất cả việc vẽ được thực hiện: các hình dạng hình học, văn bản, đường dẫn, Bitmap và hoạt ảnh.
Canvas API cung cấp các phương thức cho tất cả các thao tác cơ bản: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. Để cấu hình kiểu, Paint được sử dụng — một đối tượng xác định màu sắc, độ dày, kiểu tô, phông chữ, bóng và hiệu ứng.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Nền
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// Văn bản
canvas.drawText("Custom View",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// Hình tròn
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// Đường dẫn (đường cong Bezier)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paint là trình cấu hình kiểu vẽ. Khử răng cưa (Paint.ANTI_ALIAS_FLAG) là bắt buộc để có các cạnh mượt. Văn bản subpixel (SUBPIXEL_TEXT_FLAG) cải thiện chất lượng văn bản. Style.FILL, STROKE và FILL_AND_STROKE xác định hình dạng sẽ được tô, viền hay cả hai.
Quan trọng: không tạo đối tượng Paint, Path, Rect trong onDraw — điều này gây ra cấp phát trong vòng lặp vẽ và kích hoạt thu gom rác, làm giảm FPS. Tạo tất cả các đối tượng vẽ trong hàm tạo hoặc phương thức init. Đối với hoạt ảnh, sử dụng ValueAnimator hoặc ObjectAnimator với invalidate() để kích hoạt vẽ lại.
Thuộc tính tùy chỉnh cho phép cấu hình Custom View từ mã đánh dấu XML giống như android:layout_width hoặc android:background tiêu chuẩn. Các thuộc tính được khai báo trong tệp res/values/attrs.xml với tên, kiểu và giá trị mặc định tùy chọn.
Kiểu thuộc tính: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (tham chiếu tài nguyên). Đối với mỗi kiểu, Android tự động phân tích giá trị từ XML và truyền nó đến TypedArray.
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CustomView">
<attr name="circleColor"
format="color" />
<attr name="circleRadius"
format="dimension" />
<attr name="labelText"
format="string" />
<attr name="showAnimation"
format="boolean" />
</declare-styleable>
</resources>
Trong hàm tạo của Custom View, các thuộc tính được đọc qua context.obtainStyledAttributes, trả về một TypedArray. TypedArray cung cấp các phương thức truy cập đã được định kiểu: getColor, getDimension, getString, getBoolean, getInt. Sau khi đọc, recycle() phải được gọi trên TypedArray để giải phóng tài nguyên.
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomView
)
circleColor = typedArray.getColor(
R.styleable.CustomView_circleColor,
Color.BLUE
)
circleRadius = typedArray.getDimension(
R.styleable.CustomView_circleRadius,
50f
)
labelText = typedArray.getString(
R.styleable.CustomView_labelText
) ?: "Default"
typedArray.recycle()
}
Sử dụng trong XML: thêm không gian tên app (xmlns:app="http://schemas.android.com/apk/res-auto") và sử dụng các thuộc tính tùy chỉnh như app:circleColor="@color/red". Android Studio sẽ tự động hoàn thành và xác thực các kiểu thuộc tính nếu khai báo trong attrs.xml chính xác.
onTouchEvent là phương thức được gọi mỗi khi chạm vào View. Nó nhận một đối tượng MotionEvent với thông tin về loại sự kiện (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), tọa độ, áp lực và số lượng ngón tay (đa chạm).
Để xử lý các cử chỉ phức tạp (vuốt, chụm, nhấn giữ), sử dụng GestureDetector hoặc ScaleGestureDetector kết hợp với onTouchEvent. GestureDetector đơn giản hóa việc nhận dạng onSingleTapUp, onFling, onLongPress, onDoubleTap và các cử chỉ tiêu chuẩn khác.
private val gestureDetector = GestureDetector(
context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
handleTap(e.x, e.y)
return true
}
})
override fun onTouchEvent(event: MotionEvent): Boolean {
val handled = gestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_MOVE -> {
currentX = event.x
currentY = event.y
invalidate()
return true
}
}
return handled || super.onTouchEvent(event)
}
Kiểm tra va chạm — xác định xem một lần chạm có trúng một khu vực cụ thể của View hay không. Đối với khu vực hình chữ nhật, sử dụng Rect.contains(x, y). Đối với hình tròn — kiểm tra khoảng cách từ tâm: sqrt(dx^2 + dy^2) < radius. Đối với hình dạng tùy ý — Path.op() hoặc Region.contains().
Đa chạm được xử lý qua MotionEvent.getPointerCount() và getPointerId(index). Mỗi ngón tay nhận một ID duy nhất được giữ từ ACTION_DOWN đến ACTION_POINTER_UP. ACTION_MOVE có thể chứa dữ liệu của tất cả các ngón tay đang hoạt động — sử dụng getHistoricalX/Y để nội suy chuyển động.
Custom View có thể trở thành nút thắt cổ chai hiệu suất nếu onDraw được gọi thường xuyên (hoạt ảnh, cuộn) hoặc chứa các thao tác nặng. Android cung cấp một số cơ chế để tối ưu hóa việc kết xuất các thành phần tùy chỉnh.
View.setLayerType cho phép chuyển đổi kết xuất View sang lớp phần mềm (LAYER_TYPE_SOFTWARE) hoặc lớp phần cứng (LAYER_TYPE_HARDWARE). Lớp phần mềm hữu ích cho đồ họa Canvas phức tạp không được hỗ trợ bởi tăng tốc phần cứng — ví dụ: drawTextOnPath hoặc các hiệu ứng Path phức tạp.
invalidate(Rect) chỉ vẽ lại khu vực được chỉ định của View, không phải toàn bộ thành phần. Điều này cực kỳ quan trọng đối với các Custom View lớn (đồ họa, bản đồ, canvas vẽ), nơi việc vẽ lại toàn bộ mỗi khung hình gây ra giảm FPS. Sử dụng postInvalidateOnAnimation() để đồng bộ hóa với vsync.
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// Đối tượng được tạo một lần trong hàm tạo
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rect = Rect()
private val position = PointF()
fun updatePosition(x: Float, y: Float) {
position.set(x, y)
// Chỉ vẽ lại vùng xung quanh điểm
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
Tăng tốc phần cứng được bật theo mặc định trên Android 3.0+ (API 11+). Các thao tác Canvas drawCircle, drawRect, drawBitmap được tăng tốc phần cứng và thực thi trên GPU. Tuy nhiên, drawTextOnPath, drawVertices, các thao tác cắt phức tạp không được tăng tốc — sử dụng LAYER_TYPE_SOFTWARE cho chúng.
Theo Android Performance Patterns (Google, 2023), các nguyên nhân chính gây lag Custom View — cấp phát trong onDraw (tạo đối tượng mỗi khung hình), vẽ lại toàn bộ View khi thay đổi một phần tử và thiếu LayerType.HARDWARE cho nội dung tĩnh. Sửa ba vấn đề này giúp tăng FPS từ 30 lên 60 trong hầu hết các kịch bản.
| Phương thức | Mục đích | Tần suất gọi |
|---|---|---|
| onDraw | Vẽ nội dung View | Mỗi khi invalidate() |
| onMeasure | Xác định kích thước View | Khi thay đổi layout |
| onLayout | Định vị các View con | Sau onMeasure |
| onTouchEvent | Xử lý chạm | Mỗi khi chạm |
| onSizeChanged | Phản ứng khi thay đổi kích thước | Khi layout đầu tiên và thay đổi |
Câu hỏi thường gặp
Custom View (Canvas API) được sử dụng khi cần hiệu suất vẽ tùy chỉnh tối đa (đồ họa, trình chỉnh sửa video, bản đồ), tích hợp với mã dựa trên View hiện có hoặc hỗ trợ các phiên bản Android dưới API 21. Jetpack Compose là cách tiếp cận hiện đại cho hầu hết các dự án mới, sử dụng UI khai báo và phù hợp hơn cho các giao diện động.
Có, nếu View sử dụng wrap_content trong layout XML. Nếu không có onMeasure, wrap_content sẽ hoạt động như match_parent vì triển khai mặc định của View.onMeasure không đặt kích thước mặc định. Nếu View luôn có kích thước cố định hoặc match_parent, onMeasure có thể không được ghi đè.
Sử dụng ValueAnimator hoặc ObjectAnimator để thay đổi các thuộc tính của View (màu sắc, bán kính, vị trí) và gọi invalidate() trong callback hoạt ảnh để vẽ lại. ValueAnimator chạy trên luồng chính và được đồng bộ hóa với vsync. Đối với hoạt ảnh phức tạp (vật lý, hạt), sử dụng Choreographer.FrameCallback hoặc Android Animation Framework.
Trong onTouchEvent, sử dụng MotionEvent.getPointerCount() để xác định số lượng ngón tay và getPointerId(i) để theo dõi mỗi ngón tay bằng ID duy nhất. ACTION_POINTER_DOWN / ACTION_POINTER_UP — các sự kiện thêm/xóa ngón tay. Đối với chụm để thu phóng, sử dụng ScaleGestureDetector kết hợp với onTouchEvent — nó đơn giản hóa việc phát hiện chụm.
Có, thông qua AndroidView — một hàm composable nhúng một phần tử dựa trên View vào hệ thống phân cấp Compose. AndroidView chấp nhận một factory để tạo View và một callback update để đồng bộ hóa trạng thái. Điều này cho phép tái sử dụng các Custom View hiện có trong các dự án Compose mới mà không cần viết lại hoàn toàn.
Tóm tắ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