Custom View trong Android — bản chất, vẽ và xử lý sự kiện

Tác giả: IT Sectr Đã đăng: 2026-07-20 Thời gian đọc: 9 phút

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 — được tạo bằng cách kế thừa từ View hoặc các lớp con của nó với việc ghi đè onDraw, onMeasure, onTouchEvent.
  • Việc vẽ được thực hiện qua Canvas API — các hình dạng hình học, văn bản, đường dẫn, Bitmap và hoạt ảnh.
  • onMeasure xử lý kích thước chính xác của View có tính đến các tham số layout và padding.
  • Các thuộc tính XML tùy chỉnh được khai báo trong res/values/attrs.xml và được đọc trong hàm tạo của View.
  • Xử lý chạm qua onTouchEvent cho phép thêm hỗ trợ cử chỉ, kéo-thả và đa chạm.

Custom View trong Android là gì?

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.

kotlin
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 và onLayout: Quản lý kích thước

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).

kotlin
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 và Canvas API: Vẽ

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.

kotlin
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 XML tùy chỉnh trong attrs.xml

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.

xml
<!-- 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.

kotlin
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.

Xử lý chạm với onTouchEvent

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.

kotlin
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.

Tối ưu hiệu suất Custom View

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.

Loại lớp cho vẽ phức tạp

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.

Chỉ vô hiệu hóa khu vực cần thiết

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.

kotlin
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ứcMục đíchTần suất gọi
onDrawVẽ nội dung ViewMỗi khi invalidate()
onMeasureXác định kích thước ViewKhi thay đổi layout
onLayoutĐịnh vị các View conSau onMeasure
onTouchEventXử lý chạmMỗi khi chạm
onSizeChangedPhản ứng khi thay đổi kích thướcKhi layout đầu tiên và thay đổi

Câu hỏi thường gặp

Khi nào sử dụng Custom View thay vì Compose?

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ó bắt buộc phải ghi đè onMeasure khô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 đè.

Làm thế nào để tạo hoạt ảnh cho Custom View?

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.

Làm thế nào để xử lý đa chạm trong Custom View?

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ể sử dụng Custom View trong Jetpack Compose không?

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

  • Custom View — một thành phần Android tùy chỉnh được tạo bằng cách kế thừa từ View với việc ghi đè onDraw, onMeasure và onTouchEvent để vẽ và xử lý tùy chỉnh.
  • onDraw và Canvas API — nền tảng của vẽ: các hình dạng hình học, văn bản, Bitmap, đường dẫn — được cấu hình qua Paint cho màu sắc, kiểu và phông chữ.
  • onMeasure quan trọng cho hành vi wrap_content chính xác; sử dụng MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) và resolveSize để tính toán kích thước.
  • Thuộc tính tùy chỉnh qua attrs.xml và TypedArray cho phép cấu hình Custom View từ mã đánh dấu XML với hỗ trợ tự động hoàn thành trong Android Studio.
  • onTouchEvent với GestureDetector cung cấp xử lý chạm, cử chỉ và đa chạm, trong khi kiểm tra va chạm xác định khu vực tương tác.
  • Hiệu suất: tạo đối tượng một lần trong hàm tạo, sử dụng invalidate(Rect) để vẽ lại một phần và LayerType cho các hiệu ứng phức tạp.
  • Custom View tích hợp với Jetpack Compose qua AndroidView, cho phép tái sử dụng các thành phần có sẵn trong các dự án Compose mới.

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.

Thảo luận dự án

Đọc thêm