onLayout(): khái niệm, thuật toán bố cục và tham số phương thức

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

onLayout() là một phương thức của lớp ViewGroup, xác định vị trí và kích thước của View con trên mặt phẳng tọa độ của vùng chứa cha. Hệ thống Android gọi onLayout sau giai đoạn đo lường (onMeasure), khi chiều rộng và chiều cao đã được đo cho mỗi View con. Theo Tài liệu dành cho nhà phát triển Android (2026), onLayout là phương thức bắt buộc phải ghi đè trong bất kỳ ViewGroup tùy chỉnh nào, vì triển khai ViewGroup tiêu chuẩn không thực hiện định vị tự động các phần tử con.

Các điểm chính

  • onLayout(boolean changed, int l, int t, int r, int b) — phương thức ViewGroup xác định vị trí của các phần tử con trong tọa độ cha
  • child.layout(l, t, r, b) — lời gọi cho mỗi View con, thiết lập ranh giới cuối cùng của nó
  • Giai đoạn layout theo sau giai đoạn measure và trước giai đoạn draw trong vòng đời View
  • getWidth() và getHeight() chỉ khả dụng sau khi thực thi onLayout, khác với getMeasuredWidth khả dụng sau onMeasure
  • requestLayout() — phương thức khởi tạo lời gọi lặp lại onMeasure và onLayout khi dữ liệu ảnh hưởng đến định vị thay đổi

onLayout() là gì?

onLayout(boolean changed, int l, int t, int r, int b) là một phương thức protected của lớp ViewGroup được hệ thống gọi để định vị các View con bên trong vùng chứa cha. Nhà phát triển ghi đè phương thức này khi tạo ViewGroup tùy chỉnh với cách sắp xếp phần tử không chuẩn: dạng tầng, dạng lưới, dạng bàn cờ hoặc theo tọa độ tùy ý. Mỗi View con nhận ranh giới cuối cùng của nó thông qua lời gọi child.layout().

Tham số changed cho biết vị trí hoặc kích thước của chính ViewGroup có thay đổi kể từ lần layout cuối cùng hay không. Nếu changed là true, tất cả các phần tử con có khả năng cũng cần được định vị lại. Các tham số l, t, r, b là tọa độ của góc trên bên trái và góc dưới bên phải của ViewGroup trong hệ tọa độ của cha nó. Bên trong onLayout, nhà phát triển sử dụng các giá trị này làm tọa độ bắt đầu để sắp xếp các phần tử con.

ViewGroup là lớp duy nhất ghi đè onLayout. Một View thông thường (không phải ViewGroup) không có phần tử con và không cần onLayout — việc định vị của nó được xử lý bởi vùng chứa cha. Ngay cả khi một View thông thường ghi đè onLayout, hệ thống sẽ không gọi nó. Đây là sự khác biệt cơ bản với onMeasure, được gọi cho bất kỳ View nào.

Luồng thực thi giai đoạn Layout

Giai đoạn layout bắt đầu bằng lời gọi phương thức công khai layout(int l, int t, int r, int b) trên View gốc. Phương thức này thiết lập tọa độ cuối cùng của chính View và gọi onLayout nếu View là ViewGroup. Sau đó onLayout gọi đệ quy child.layout() cho mỗi phần tử con và quy trình lặp lại xuống dưới hệ thống phân cấp. Do đó, layout lan truyền từ gốc đến các lá.

Trước khi gọi onLayout, hệ thống kiểm tra xem kích thước của View có thay đổi so với chu kỳ trước hay không. Nếu kích thước không thay đổi và requestLayout chưa được gọi, onLayout có thể không được gọi — hệ thống sử dụng kết quả của layout trước đó. Đây là một tối ưu hóa ngăn chặn việc tính toán lại vị trí không cần thiết trong quá trình hoạt ảnh hoặc cuộn, khi chỉ có nội dung thay đổi chứ không phải kích thước.

requestLayout() là một phương thức của View thông báo cho hệ thống rằng layout của View đã lỗi thời và cần được tính toán lại. Gọi requestLayout sẽ kích hoạt một chu kỳ đầy đủ: đầu tiên onMeasure được gọi, sau đó onLayout, sau đó onDraw. Không giống như invalidate, chỉ kích hoạt vẽ lại, requestLayout kích hoạt tính toán lại hoàn toàn kích thước và vị trí. Các lời gọi quá mức requestLayout là nguyên nhân phổ biến gây ra vấn đề hiệu suất.

Tham số onLayout: l, t, r, b

l (left) — tọa độ X của cạnh trái ViewGroup trong hệ tọa độ cha. t (top) — tọa độ Y của cạnh trên. r (right) — tọa độ X của cạnh phải. b (bottom) — tọa độ Y của cạnh dưới. Chiều rộng của ViewGroup được tính là r - l, chiều cao là b - t. Các tọa độ này đã bao gồm tất cả padding của chính ViewGroup.

Bên trong onLayout, nhà phát triển gọi child.layout(int childLeft, int childTop, int childRight, int childBottom) cho mỗi View con. Các tọa độ được truyền cho child.layout phải nằm trong hệ tọa độ của ViewGroup cha. Thông thường, childLeft và childTop được tính toán có tính đến padding của cha: childLeft = l + paddingLeft + offsetX, childTop = t + paddingTop + offsetY.

Tham sốMô tảCách sử dụng điển hình
l (left)Tọa độ cạnh trái của ViewGroup trong chaĐiểm bắt đầu trên trục X cho các phần tử con
t (top)Tọa độ cạnh trên của ViewGroup trong chaĐiểm bắt đầu trên trục Y cho các phần tử con
r (right)Tọa độ cạnh phải của ViewGroup trong chaGiới hạn trên của chiều rộng, r - l = getWidth()
b (bottom)Tọa độ cạnh dưới của ViewGroup trong chaGiới hạn trên của chiều cao, b - t = getHeight()

Tính toán tọa độ cho các phần tử con

Tọa độ con được tính theo công thức: childLeft = l + paddingLeft + (marginLeft nếu có), childRight = childLeft + child.getMeasuredWidth(). Tương tự cho trục dọc: childTop = t + paddingTop + (marginTop), childBottom = childTop + child.getMeasuredHeight(). Sau khi tính toán bốn giá trị này, child.layout(childLeft, childTop, childRight, childBottom) được gọi.

Ví dụ ViewGroup tùy chỉnh với onLayout trong Kotlin

Hãy tạo một FlowLayout — một ViewGroup tùy chỉnh sắp xếp các View con thành hàng, chuyển phần tử sang hàng mới khi hàng hiện tại đầy. Đây là phiên bản tương đương của Flexbox với wrap trong một mặt phẳng. onLayout lặp qua tất cả các View con, tính toán vị trí cho từng View và gọi child.layout() với ranh giới chính xác.

kotlin
class FlowLayout(context: Context)
    : ViewGroup(context) {

    private val horizontalSpacing = 12
    private val verticalSpacing = 8

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        val parentWidth =
            MeasureSpec.getSize(widthMeasureSpec)
        var rowX = paddingLeft
        var rowY = paddingTop
        var maxRowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child,
                widthMeasureSpec, 0,
                heightMeasureSpec, 0)

            if (rowX + child.measuredWidth >
                parentWidth - paddingRight) {
                rowX = paddingLeft
                rowY += maxRowHeight + verticalSpacing
                maxRowHeight = 0
            }

            rowX += child.measuredWidth +
                horizontalSpacing
            maxRowHeight = maxOf(maxRowHeight,
                child.measuredHeight)
        }

        val totalHeight = rowY + maxRowHeight +
            paddingBottom
        setMeasuredDimension(
            resolveSize(parentWidth, widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec))
    }

    override fun onLayout(changed: Boolean,
                       l: Int, t: Int,
                       r: Int, b: Int) {
        val parentWidth = r - l
        var rowX = paddingLeft
        var rowY = paddingTop
        var maxRowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val cw = child.measuredWidth
            val ch = child.measuredHeight

            if (rowX + cw >
                parentWidth - paddingRight) {
                rowX = paddingLeft
                rowY += maxRowHeight + verticalSpacing
                maxRowHeight = 0
            }

            child.layout(rowX, rowY,
                rowX + cw, rowY + ch)
            rowX += cw + horizontalSpacing
            maxRowHeight =
                maxOf(maxRowHeight, ch)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?)
        : LayoutParams =
        MarginLayoutParams(context, attrs)
}

Sự khác biệt giữa onLayout và onMeasure

onMeasureonLayout là hai giai đoạn tuần tự của vòng đời View thực hiện các nhiệm vụ khác nhau về cơ bản. onMeasure xác định kích thước mong muốn (đã đo) của View, trong khi onLayout thiết lập tọa độ và kích thước thực tế (cuối cùng). Sự khác biệt chính: trong onMeasure, kích thước có thể là tạm thời và sau đó được cha điều chỉnh, trong khi onLayout cố định vị trí cuối cùng của mỗi View con.

onMeasure được gọi cho mọi View, bao gồm View lá (TextView, ImageView, Button). onLayout chỉ được gọi cho ViewGroup. Điều này bởi vì định vị là trách nhiệm của vùng chứa cha, không phải của chính View. View lá nhận vị trí của nó thông qua layout() được gọi từ onLayout của cha.

getMeasuredWidth()getMeasuredHeight() khả dụng sau onMeasure, trong khi getWidth()getHeight() chỉ khả dụng sau onLayout. Nếu bạn truy cập getWidth() bên trong onMeasure, nó sẽ trả về giá trị từ chu kỳ trước hoặc không. Do đó, để tính toán kích thước trong onMeasure, bạn nên sử dụng MeasureSpec và các phần tử con một cách tuần tự.

Lỗi thường gặp khi làm việc với onLayout

Định vị không tính đến padding — lỗi đầu tiên khi triển khai onLayout. Nhà phát triển thường quên thêm paddingLeftpaddingTop của cha vào tọa độ ban đầu của View con. Kết quả là các phần tử con hiển thị ở rìa ViewGroup, bỏ qua padding được thiết lập qua setPadding() hoặc trong đánh dấu XML. Tính toán đúng: childLeft = paddingLeft + offsetX.

Gọi layout cho phần tử con vô hình — vấn đề phổ biến thứ hai. Nếu ViewGroup chứa View con có tầm nhìn là GONE, chúng không cần được định vị — chúng không chiếm không gian. Tuy nhiên, onLayout phải xử lý trường hợp này đúng cách, bỏ qua các phần tử con GONE. Đối với phần tử con INVISIBLE, vẫn cần gọi layout — chúng giữ không gian mặc dù không được hiển thị.

Bỏ qua tham số changed — lỗi thứ ba. Tham số changed cho biết kích thước hoặc vị trí của ViewGroup đã thay đổi hay chưa. Nếu changed == false, có thể sử dụng tọa độ đã lưu trong bộ nhớ đệm mà không cần tính toán lại layout của tất cả các phần tử con. Tuy nhiên, lưu đệm toàn bộ layout là một tác vụ phức tạp và trong hầu hết các triển khai, onLayout chỉ đơn giản tính toán lại tất cả các phần tử mỗi lần. Điều này có thể chấp nhận được với số lượng phần tử con nhỏ.

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

Có thể không ghi đè onLayout trong ViewGroup không?

Có, có thể nếu ViewGroup sử dụng LayoutParams tiêu chuẩn và không thêm logic định vị tùy chỉnh. Tuy nhiên, triển khai onLayout tiêu chuẩn trong ViewGroup không thực hiện hành động nào — các phần tử con sẽ không được định vị. Trong thực tế, tất cả ViewGroup (LinearLayout, RelativeLayout, FrameLayout) đều ghi đè onLayout.

Sự khác biệt giữa layout và onLayout là gì?

layout() là phương thức public final của View, được gọi bởi hệ thống hoặc ViewGroup cha. Nó thiết lập tọa độ của chính View và gọi onLayout nếu View là ViewGroup. onLayout() là phương thức protected mà nhà phát triển ghi đè để sắp xếp tùy chỉnh các phần tử con.

onLayout có thể gọi requestLayout không?

Về mặt kỹ thuật — có, có thể. Nhưng điều này hoàn toàn không được khuyến nghị, vì nó dẫn đến đệ quy vô hạn: requestLayout → onMeasure → onLayout → requestLayout. Nếu requestLayout được gọi bên trong onLayout, hệ thống sẽ ném ra ngoại lệ StackOverflowError. Tất cả các thay đổi kích thước phải được thực hiện trước onLayout.

onLayout hoạt động với hoạt ảnh như thế nào?

Hoạt ảnh layout (LayoutTransition) chặn các thay đổi vị trí của View con và áp dụng hoạt ảnh chuyển tiếp. Khi LayoutTransition được bật, onLayout đầu tiên thiết lập vị trí cuối cùng, sau đó LayoutTransition tạo hoạt ảnh di chuyển từ vị trí cũ sang vị trí mới. Điều này yêu cầu triển khai onLayout chính xác với tọa độ cuối cùng phù hợp.

Tại sao onLayout không được gọi sau invalidate?

invalidate() chỉ kích hoạt giai đoạn draw (vẽ lại), không ảnh hưởng đến measure và layout. Để kích hoạt onLayout, bạn cần gọi requestLayout(), khởi tạo chu kỳ đầy đủ: measure → layout → draw. invalidate hiệu quả hơn để cập nhật giao diện khi kích thước và vị trí không thay đổi.

Tổng kết

  • onLayout() — phương thức ViewGroup xác định vị trí cuối cùng của View con sau khi hoàn thành giai đoạn đo lường
  • child.layout(l, t, r, b) — cơ chế chính để thiết lập tọa độ cho mỗi phần tử con
  • Tham số l, t, r, b — tọa độ các cạnh của ViewGroup trong hệ thống cha, chiều rộng = r - l, chiều cao = b - t
  • Giai đoạn layout lan truyền đệ quy từ View gốc đến các phần tử con, gọi onLayout trên mỗi ViewGroup
  • requestLayout() kích hoạt chu kỳ tính toán lại hoàn toàn kích thước và vị trí, khác với invalidate chỉ kích hoạt vẽ lại
  • Tính đến padding trong onLayout là bắt buộc — tọa độ ban đầu của phần tử con phải bao gồm paddingLeft và paddingTop của cha

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