onMeasure() là một phương thức protected của lớp android.view.View được hệ thống Android gọi để xác định kích thước của View. Hệ thống truyền hai đối tượng MeasureSpec vào phương thức, mỗi đối tượng chứa chế độ đo (EXACTLY, AT_MOST hoặc UNSPECIFIED) và kích thước do container cha đề xuất. Theo Tài liệu dành cho Nhà phát triển Android (2026), ghi đèn onMeasure với xử lý MeasureSpec chính xác là bước bắt buộc đối với tất cả các View và ViewGroup tùy chỉnh yêu cầu kiểm soát kích thước chính xác.
Những điểm chính
onMeasure(int widthMeasureSpec, int heightMeasureSpec) là một phương thức của lớp View mà hệ thống Android gọi để xác định chiều rộng và chiều cao của một view. Nhà phát triển ghi đèn phương thức này để chỉ định kích thước mà View nên có dựa trên các ràng buộc được truyền trong MeasureSpec. Nếu không ghi đèn onMeasure đúng cách, View tùy chỉnh có thể hiển thị không chính xác hoặc không xuất hiện.
Hệ thống gọi onMeasure trong giai đoạn measure của vòng đời View, diễn ra trước các giai đoạn layout (onLayout) và draw (onDraw). Nếu View không ghi đèn onMeasure, việc triển khai từ siêu lớp được sử dụng, đặt kích thước mặc định dựa trên background drawable hoặc layout_params. Gọi super.onMeasure(widthMeasureSpec, heightMeasureSpec) chỉ hoạt động cho các lớp con tiêu chuẩn của View như TextView hoặc ImageView.
Một yêu cầu quan trọng đối với onMeasure là lệnh gọi setMeasuredDimension(int, int) phải có mặt ở cuối phương thức. Nếu thiếu lệnh gọi này, hệ thống sẽ ném IllegalStateException thông báo rằng View chưa thiết lập kích thước được đo. Các kích thước cuối cùng có sẵn thông qua các getter getMeasuredWidth() và getMeasuredHeight() sau khi giai đoạn measure hoàn tất.
MeasureSpec là một số nguyên 32 bit, trong đó 2 bit trên cùng mã hóa chế độ đo và 30 bit dưới mã hóa kích thước. Chế độ xác định mức độ tự do của View trong việc chọn kích thước riêng. Android cung cấp ba chế độ: EXACTLY, AT_MOST và UNSPECIFIED. Mỗi chế độ chỉ định logic xử lý khác nhau trong onMeasure.
| Chế độ MeasureSpec | Giá trị | Hành vi |
|---|---|---|
| EXACTLY | Cha đã chỉ định kích thước chính xác | View phải khớp chính xác với kích thước đã cho nếu không muốn vượt quá ranh giới |
| AT_MOST | Cha đã đặt kích thước tối đa | View có thể chọn bất kỳ kích thước nào từ 0 đến tối đa đã cho |
| UNSPECIFIED | Cha không áp đặt ràng buộc | View có thể chọn bất kỳ kích thước mong muốn nào mà không có giới hạn trên |
Để trích xuất chế độ và kích thước từ MeasureSpec, các phương thức tĩnh của lớp MeasureSpec được sử dụng: MeasureSpec.getMode(int) trả về một trong ba chế độ (EXACTLY, AT_MOST, UNSPECIFIED) và MeasureSpec.getSize(int) trả về kích thước số bằng pixel. Để tạo MeasureSpec tùy chỉnh, sử dụng MeasureSpec.makeMeasureSpec(int size, int mode). Ba phương thức này bao phủ tất cả các kịch bản làm việc với kích thước trong onMeasure.
Mẫu chuẩn để xử lý MeasureSpec: nếu chế độ là EXACTLY — sử dụng kích thước đã cho làm kích thước cuối cùng; nếu AT_MOST — chọn giá trị tối thiểu giữa kích thước mong muốn (nội dung của View) và tối đa đã cho; nếu UNSPECIFIED — sử dụng kích thước mong muốn của View mà không có ràng buộc. Mẫu này đảm bảo hành vi chính xác dưới mọi ràng buộc từ cha.
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val desiredWidth = 200
val desiredHeight = 100
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
Để đơn giản hóa logic chuẩn, Android cung cấp phương thức resolveSizeAndState, phương thức này nhận kích thước mong muốn, MeasureSpec và trả về kích thước cuối cùng với chế độ chính xác. Phương thức này triển khai mẫu được mô tả ở trên trong một dòng mã. Hàm resolveSize(int size, int measureSpec) cũng có sẵn, trả về kích thước sạch mà không có bit trạng thái.
Android sử dụng thuật toán đo hai lần để đảm bảo mỗi View trong hệ phân cấp nhận được kích thước chính xác, xét đến các ràng buộc của cha và sở thích của con. Ở lần đầu tiên, cha truyền MeasureSpec kèm ràng buộc cho các View con và các View con tính toán kích thước mong muốn của chúng. Ở lần thứ hai, cha đưa ra quyết định cuối cùng về kích thước.
Đối với ViewGroup, quy trình đo phức tạp hơn: cha phải đo tất cả các con trước, sau đó xác định kích thước của chính mình dựa trên kích thước của chúng. Gọi measureChildren(int widthMeasureSpec, int heightMeasureSpec) sẽ duyệt qua tất cả View con và gọi measure(child, childWidthSpec, childHeightSpec) cho mỗi View. Sau khi đo tất cả các con, ViewGroup gọi setMeasuredDimension với kích thước riêng của nó.
Một sắc thái quan trọng: phương thức measure (public, final) không thể bị ghi đèn — thay vào đó, onMeasure được ghi đèn. Điều này đảm bảo hệ thống có thể thực hiện các tác vụ bảo trì trước và sau onMeasure, chẳng hạn như kiểm tra thay đổi kích thước và tính toán vùng bẩn để vẽ tiếp theo. Nếu View có kích thước cố định, việc ghi đèn onMeasure có thể không cần thiết.
MeasureSpec không chỉ bao gồm kích thước và chế độ mà còn có các bit trạng thái, có thể truy cập qua MeasureSpec.getMode(). Sau khi gọi setMeasuredDimension, trạng thái trở thành một phần của kích thước được đo của View và có thể được kiểm tra qua getMeasuredState(). Điều này được sử dụng trong ScrollView và các container cuộn khác để truyền ràng buộc chính xác cho các con.
Hãy xem xét một ví dụ thực tế về việc tạo View tùy chỉnh với onMeasure được ghi đèn để hiển thị hình vuông. Lớp SquareView mở rộng View và đảm bảo rằng chiều rộng và chiều cao luôn bằng nhau, bất kể MeasureSpec được truyền vào là gì. Trong onMeasure, cạnh tối thiểu được xác định và kích thước hình vuông được thiết lập.
class SquareView(context: Context)
: View(context) {
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val widthSize =
MeasureSpec.getSize(widthMeasureSpec)
val heightSize =
MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(widthSize, heightSize)
setMeasuredDimension(size, size)
}
}
ViewGroup yêu cầu logic onMeasure phức tạp hơn vì trước tiên phải đo các con, sau đó mới xác định kích thước của chính ViewGroup. Ví dụ CascadeLayout phân phối các con theo dạng thác nước với một độ lệch. Sau khi đo tất cả các con qua measureChildWithMargins, chiều rộng và chiều cao tổng thể được tính toán.
class CascadeLayout(context: Context)
: ViewGroup(context) {
private val cascadeOffset = 40
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
var maxWidth = 0
var totalHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child,
widthMeasureSpec,
cascadeOffset * i,
heightMeasureSpec, 0)
maxWidth = maxOf(maxWidth,
child.measuredWidth +
cascadeOffset * i)
totalHeight += child.measuredHeight
}
setMeasuredDimension(
resolveSize(maxWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec))
}
override fun generateLayoutParams(attrs: AttributeSet?)
: LayoutParams = MarginLayoutParams(context, attrs)
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
var top = t
for (i in 0 until childCount) {
val child = getChildAt(i)
val left = l + cascadeOffset * i
child.layout(left, top,
left + child.measuredWidth,
top + child.measuredHeight)
top += child.measuredHeight
}
}
}
Thiếu lệnh gọi setMeasuredDimension là lỗi phổ biến nhất. Nếu nhà phát triển ghi đèn onMeasure nhưng không gọi setMeasuredDimension, ứng dụng sẽ bị treo với IllegalStateException. Điều này đặc biệt thường xảy ra khi phương thức có các nhánh có điều kiện và một trong các nhánh thiếu lệnh gọi. Mỗi nhánh mã trong onMeasure phải kết thúc bằng lệnh gọi setMeasuredDimension.
Bỏ qua chế độ AT_MOST là lỗi phổ biến thứ hai. Nếu View ở chế độ AT_MOST luôn sử dụng kích thước được truyền vào thay vì tính toán dựa trên nội dung, container cha sẽ không thể phân phối không gian chính xác. Ví dụ, TextView ở AT_MOST phải tính chiều rộng văn bản và sử dụng giá trị tối thiểu giữa chiều rộng mong muốn và chiều rộng được truyền. Bỏ qua AT_MOST khiến View chiếm tất cả không gian có sẵn ngay cả khi nội dung nhỏ.
Tạo đối tượng bên trong onMeasure là lỗi hiệu suất kinh điển. Vì onMeasure có thể được gọi nhiều lần (ở mỗi yêu cầu layout), việc tạo các đối tượng (Paint, Rect, String) bên trong phương thức này làm tắc nghẽn bộ nhớ và kích hoạt thu gom rác. Tất cả các đối tượng nên được tạo một lần trong trình xây dựng View và chỉ logic tính toán kích thước mới nên chạy trong onMeasure. Quy tắc tương tự áp dụng cho onDraw và onLayout.
measureChildWithMargins là phương thức protected của ViewGroup đo một View con duy nhất có xét đến MarginLayoutParams của nó. Phương thức này chấp nhận MeasureSpec của cha và các độ lệch chiều rộng và chiều cao tích lũy. Nó tự động điều chỉnh MeasureSpec cho View con bằng cách trừ padding của cha và margin của con, sau đó truyền MeasureSpec đã điều chỉnh vào child.measure().
Đối với logic đo nâng cao, ViewGroup có thể ghi đèn measureChild(View child, int parentWidthSpec, int parentHeightSpec) hoặc làm việc trực tiếp với MeasureSpec cho từng con. Ví dụ, LinearLayout trong onMeasure duyệt qua tất cả View con, đo từng View có xét layout_weight của nó và phân phối không gian còn lại theo tỷ lệ. Cách tiếp cận này cho phép triển khai các thuật toán bố cục tùy ý.
Lưu trữ đệm kết quả đo thông qua cơ chế bộ nhớ đệm đo có sẵn thông qua cờ setMeasureWithLargestChildEnabled trong một số ViewGroup nhất định. Tuy nhiên, trong hầu hết các trường hợp, onMeasure được gọi lại khi có bất kỳ thay đổi layout nào và bộ nhớ đệm không được áp dụng. Trong các ViewGroup tùy chỉnh, nên giảm thiểu tính toán trong onMeasure thay vì dựa vào bộ nhớ đệm.
Các câu hỏi thường gặp
Có, nếu View tùy chỉnh kế thừa trực tiếp từ lớp View. Nếu nó kế thừa từ TextView, ImageView hoặc Button với kích thước tiêu chuẩn, onMeasure có thể được giữ nguyên. Đối với ViewGroup, việc ghi đèn onMeasure luôn cần thiết — nếu không, các con sẽ không được đo chính xác.
Hệ thống Android ném IllegalStateException với thông báo “The View did not call setMeasuredDimension”. Ngoại lệ này xảy ra trong phương thức measure() sau khi onMeasure kết thúc, nếu kích thước cuối cùng vẫn là 0. Ngoại lệ sẽ làm sập ứng dụng nếu không được xử lý qua try-catch.
getMeasuredWidth() trả về kích thước được đặt trong onMeasure (giai đoạn đo). getWidth() trả về kích thước thực tế mà View nhận được trong onLayout sau tất cả các điều chỉnh vị trí. Đối với hầu hết các View, các giá trị này trùng nhau, nhưng trong ViewGroup tùy chỉnh, chúng có thể khác nhau.
Không, onMeasure được dành riêng cho việc tính toán kích thước. Thay đổi trạng thái, bắt đầu hoạt ảnh, kết nối mạng hoặc cập nhật dữ liệu trong phương thức này vi phạm kiến trúc Android và có thể dẫn đến các lệnh gọi measure đệ quy, vì thay đổi trạng thái có thể kích hoạt requestLayout.
ConstraintLayout quản lý việc đo các con một cách độc lập dựa trên các ràng buộc đã xác định. Nếu View tùy chỉnh bên trong ConstraintLayout ghi đèn onMeasure, nó phải xử lý chính xác MeasureSpec được truyền từ ConstraintLayout, nếu không các ràng buộc có thể không hoạt động. ConstraintLayout sử dụng thuật toán hai lần với WidgetContainer riêng để tính toán.
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