onMeasure() เป็นเมธอด protected ของคลาส android.view.View ที่ระบบ Android เรียกใช้เพื่อกำหนดขนาดของ View ระบบจะส่งวัตถุ MeasureSpec สองอันไปยังเมธอด แต่ละอันประกอบด้วยโหมดการวัด (EXACTLY, AT_MOST หรือ UNSPECIFIED) และขนาดที่แนะนำโดยคอนเทนเนอร์แม่ ตาม Android Developers Documentation (2026) การเกินโอเวอร์ไรด์ onMeasure ด้วยการจัดการ MeasureSpec ที่ถูกต้องเป็นขั้นตอนที่จำเป็นสำหรับ View และ ViewGroup ที่กำหนดเองทั้งหมดที่ต้องการควบคุมขนาดที่แม่นยำ.
ข้อมูลสำคัญ
onMeasure(int widthMeasureSpec, int heightMeasureSpec) เป็นเมธอดของคลาส View ที่ระบบ Android เรียกใช้เพื่อกำหนดความกว้างและความสูงของมุมมอง นักพัฒนาเกินโอเวอร์ไรด์เมธอดนี้เพื่อระบุว่า View ควรมีขนาดเเท่ใดโดยอาศัยข้อจำกัดที่ส่งมาใน MeasureSpec หากไม่มีการเกินโอเวอร์ไรด์ onMeasure ที่ถูกต้อง View ที่กำหนดเองอาจแสดงผิดหรือไม่ปรากฏเลย.
ระบบเรียก onMeasure ในระฮะวง measure ของวงจรของ View ซึ่งมาก่อนระฮะวง layout (onLayout) และ draw (onDraw) หาก View ไม่เกินโอเวอร์ไรด์ onMeasure จะใช้การดำเนินงานจากซูเปอร์คลาส ซึ่งกำหนดขนาดเรียกเรียงโดยอาศัยกับ background drawable หรือ layout_params การเรียก super.onMeasure(widthMeasureSpec, heightMeasureSpec) ทำงานเฉพาะสำหรับคลาสย่อยมาตรฐานของ View เช่น TextView หรือ ImageView.
ข้อกำหนดสำคัญของ onMeasure คือต้องมีการเรียก setMeasuredDimension(int, int) อยู่ในตอนท้ายของเมธอด หากขาดการเรียกนี้ ระบบจะสร้าง IllegalStateException โดยรายงานว่า View ไม่ได้กำหนดขนาดที่วัดได้ ขนาดสุดท้ายจะพร้อมให้ผ่านเทิร์เกอร์ getMeasuredWidth() และ getMeasuredHeight() หลังจากระฮะวง measure สิ้นสุด.
MeasureSpec เป็นจำนวนเต็ม 32 บิตที่ไบต์สูง 2 บิตเข้ารหัสโหมดการวัด และไบต์ล่าง 30 บิตเข้ารหัสขนาด โหมดจะเป็นตัวกำหนดว่า View มีอิสระไม้เพียงใดในการเลือกขนาดของตนเอง Android ให้สามโหมด: EXACTLY, AT_MOST และ UNSPECIFIED แต่ละโหมดจะกำหนดตรรกะการประมวลผลที่แตกต่างกันใน onMeasure.
| โหมด MeasureSpec | ค่า | พฤติกรรม |
|---|---|---|
| EXACTLY | แม่ระบุขนาดที่แน่นอน | View ต้องเหมาะกับขนาดที่กำหนดไว้อย่างแม่นยำหากไม่ต้องการเกินขอแนวขอบ |
| AT_MOST | แม่กำหนดขนาดสูงสุด | View สามารถเลือกขนาดใดๆ ตั้งแต่ 0 ถึงค่าสูงสุดที่กำหนด |
| UNSPECIFIED | แม่ไม่มีข้อจำกัด | View สามารถเลือกขนาดให้เหมาะสมได้โดยไม่มีขอจำกัดสูงสุด |
เพื่อดึงโหมดและขนาดจาก MeasureSpec จะใช้เมธอดสถิตของคลาส MeasureSpec: MeasureSpec.getMode(int) จะคืนหนึ่งในสามโหมด (EXACTLY, AT_MOST, UNSPECIFIED) และ MeasureSpec.getSize(int) จะคืนขนาดเป็นตัวเลขในหน่วยพีเซล ไฟล์ MeasureSpec.makeMeasureSpec(int size, int mode) ใช้สำหรับสร้าง MeasureSpec ที่กำหนดเอง ทั้งสามเมธอดนี้ครอบคลุมทุกสถานการณ์ของการทำงานกับขนาดใน onMeasure.
รูปแบบมาตรฐาน สำหรับการจัดการ MeasureSpec: หากโหมดเป็น EXACTLY — ใช้ขนาดที่ส่งมาเป็นขนาดสุดท้าย; หาก AT_MOST — เลือกค่าน้อยที่สุดระหว่างขนาดที่ต้องการ (เนื้อหาของ View) และค่าสูงสุดที่ส่งมา; หาก UNSPECIFIED — ใช้ขนาดที่ต้องการของ View โดยไม่มีข้อจำกัด รูปแบบนี้รับประกันพฤติกรรมที่ถูกต้องภายใต้ข้อจำกัดใดๆ ของแม่.
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)
}
เพื่อลดความซับซ้อนของตรรกะการมาตรฐาน Android ให้เมธอด resolveSizeAndState ซึ่งรับขนาดที่ต้องการ MeasureSpec และคืนขนาดสุดท้ายโดยมีโหมดที่ถูกต้อง เมธอดนี้ใช้รูปแบบที่อธิบายข้างต้นในโค้ดเดียว ฟังก์ชัน resolveSize(int size, int measureSpec) ก็มีให้เช่นกัน โดยคืนขนาดที่สะอาดโดยไม่มีบิตสถานะ.
Android ใช้อะไลโกรทัมการวัดสองผ่านที่ทำให้แน่ใจว่าทุก View ในลำดับชั้นได้รับขนาดที่ถูกต้องโดยคำนึงถึงข้อจำกัดของแม่และความต้องการของลูก ในผ่านแรก แม่จะส่ง MeasureSpec พร้อมข้อจำกัดไปยังลูก View และลูก View จะคำนวณขนาดที่ต้องการ ในผ่านที่สอง แม่จะตัดสินใจสุดท้ายเกี่ยวกับขนาด.
สำหรับ ViewGroup กระบวนการวัดซับซ้อนกว่า: แม่ต้องวัดลูกทั้งหมดก่อน จากนั้นกำหนดขนาดของตนเองโดยอาศัยขนาดของพวกเขา การเรียก measureChildren(int widthMeasureSpec, int heightMeasureSpec) จะวนซ้ำผ่านลูก View ทั้งหมดและเรียก measure(child, childWidthSpec, childHeightSpec) สำหรับแต่ละตัว หลังจากวัดลูกทั้งหมดแล้ว ViewGroup จะเรียก setMeasuredDimension ด้วยขนาดของตนเอง.
ข้อสำคัญอีกอย่างหนึ่ง: เมธอด measure (สาธารณะ final) ไม่สามารถเกินโอเวอร์ไรด์ได้ — แทนที่จะเกินโอเวอร์ไรด์ onMeasure แทน วิธีนี้ทำให้ระบบสามารถทำงานบำรุงรักษาก่อนและหลัง onMeasure เช่น ตรวจสอบการเปลี่ยนแปลงขนาดและคำนวณพื้นที่สกปรกสำหรับการวาดภาพต่อไป หาก View มีขนาดตามที่กำหนดไว้ การเกินโอเวอร์ไรด์ onMeasure อาจไม่จำเป็น.
MeasureSpec ไม่เพียงประกอบด้วยขนาดและโหมดเท่านั้น แต่ยังมีบิตสถานะ ซึ่งสามารถเข้าถึงได้ผ่าน MeasureSpec.getMode() หลังจากเรียก setMeasuredDimension สถานะจะกลายเป็นส่วนหนึ่งของขนาดที่วัดได้ของ View และสามารถตรวจสอบได้ผ่าน getMeasuredState() สิ่งนี้ใช้ใน ScrollView และคอนเทนเนอร์สกรอลได้อื่น ๆ เพื่อส่งต่อข้อจำกัดไปยังลูกได้อย่างถูกต้อง.
มาดู ตัวอย่างที่ใช้ได้จริง ของการสร้าง View ที่กำหนดเองโดยมี onMeasure ที่เกินโอเวอร์ไรด์สำหรับการแสดงผลแบบสี่เหลี่ยม คลาส SquareView สืบทอด View และรับประกันว่าความกว้างและความสูงจะเท่ากันเสมอ โดยไม่ขึ้นกับ MeasureSpec ที่ส่งมา ใน onMeasure จะกำหนดด้านต่ำสุดและกำหนดขนาดสี่เหลี่ยม.
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 ต้องการตรรกะการ onMeasure ที่ซับซ้อนกว่าเพราะต้องวัดลูกก่อน จากนั้นจึงกำหนดขนาดของ ViewGroup เอง ตัวอย่าง CascadeLayout จัดเรียงลูกแบบน้ำตกโดยมีการเลื่อนตำแหน่ง หลังจากวัดลูกทั้งหมดผ่าน measureChildWithMargins จะคำนวณความกว้างและความสูงรวม.
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
}
}
}
การลืมเรียก setMeasuredDimension เป็นข้อผิดพลาดที่พบบ่อยที่สุด หากนักพัฒนาเกินโอเวอร์ไรด์ onMeasure แต่ไม่เรียก setMeasuredDimension แอปพลิเคชันจะคราสหรือเสียหายด้วย IllegalStateException โดยเฉพาะอย่างยิ่งเมื่อเมธอดมีการแยกสาขาแบบมีเงื่อนไข และหนึ่งในสาขานั้นขาดการเรียก แต่ละสาขาโค้ดใน onMeasure ต้องลงท้ายด้วยการเรียก setMeasuredDimension.
การละเลยโหมด AT_MOST เป็นข้อผิดพลาดที่พบบ่อยเป็นอันดับสอง หาก View ในโหมด AT_MOST ใช้ขนาดที่ส่งมาเสมอ แทนที่จะคำนวณตามเนื้อหา คอนเทนเนอร์แม่จะไม่สามารถแจกจัยพื้นที่ได้อย่างถูกต้อง ตัวอย่างเช่น TextView ใน AT_MOST ควรคำนวณความกว้างข้อความและใช้ค่าน้อยที่สุดระหว่างความกว้างที่ต้องการและที่ส่งมา การละเลย AT_MOST ทำให้ View ใช้พื้นที่ที่มีอยู่ทั้งหมดแม้ว่าจะมีเนื้อหาน้อยก็ตาม.
การสร้างออบเจกต์ภายใน onMeasure เป็นข้อผิดพลาดด้านประสิทธิภาพที่คลาสิก เนื่องจาก onMeasure สามารถเรียกได้หลายครั้ง (ทุกครั้งที่มีการร้องขอ layout) การสร้างออบเจกต์ (Paint, Rect, String) ภายในเมธอดนี้จะทำให้หน่วยความจำเติมและกระตุ้นการเก็บขมะ ออบเจกต์ทั้งหมดควรสร้างครั้งเดียวในคอนสตรักเตอร์ของ View และใน onMeasure ควรมีเฉพาะตรรกะการคำนวณขนาดเท่านั้น กฎเดียวกันกับ onDraw และ onLayout.
measureChildWithMargins เป็นเมธอดที่ได้รับการป้องกันของ ViewGroup ที่วัดลูก View ดียวโดยคำนึงถึง MarginLayoutParams ของมัน เมธอดรับ MeasureSpec ของแม่และค่าเลื่อนที่สะสมของความกว้างและความสูง โดยปรับ MeasureSpec สำหรับลูก View โดยอัตโนมัติด้วยการลบ padding ของแม่และ margin ของลูก แล้วส่ง MeasureSpec ที่ปรับแล้วไปยัง child.measure().
สำหรับ ตรรกะการวัด ขั้นสูง ViewGroup สามารถเกินโอเวอร์ไรด์ measureChild(View child, int parentWidthSpec, int parentHeightSpec) หรือทำงานโดยตรงกับ MeasureSpec สำหรับแต่ละลูก ตัวอย่างเช่น LinearLayout ใน onMeasure จะวนซ้ำผ่านลูก View ทั้งหมด วัดแต่ละตัวโดยคำนึงถึง layout_weight ของมันและแจกจัยพื้นที่ที่เหลืออย่างไดสดส่วน วิธีการนี้ช่วยให้สามารถนำไปใช้กับอะไลโกรทัมการจัดเรียงแบบกำหนดเอง.
การเก็บแคชผลลัพธ์ของ การวัด ผ่านกลไกแคชการวัดนั้นสามารถใช้งานได้ผ่านแฟลก setMeasureWithLargestChildEnabled ใน ViewGroup บางชนิด อย่างไรก็ตาม ในกรณีส่วนใหญ่ onMeasure จะถูกเรียกอีกเมื่อมีการเปลี่ยนแปลง layout ใดๆ และการเก็บแคชจะไม่ได้ใช้ ใน ViewGroup ที่กำหนดเอง แนะนำให้ลดการคำนวณใน onMeasure แทนที่จะพึ่งพาการเก็บแคช.
คำถามที่พบบ่อย
ใช่ หากคุสต้ม View สืบทอดโดยตรงจากคลาส View หากมันสืบทอดจาก TextView, ImageView หรือ Button โดยมีขนาดมาตรฐาน onMeasure สามารถเปลื่อนได้ สำหรับ ViewGroup การเกินโอเวอร์ไรด์ onMeasure เป็นสิ่งจำเป็นเสมอ — มิฉะนั้นลูกจะไม่ได้รับการวัดที่ถูกต้อง.
ระบบ Android จะสร้าง IllegalStateException โดยมีข้อความ “The View did not call setMeasuredDimension” ข้อยกเว้นนี้เกิดขึ้นในเมธอด measure() หลังจาก onMeasure สิ้นสุด หากขนาดสุดท้ายยังเป็นศูนย์ ข้อยกเว้นจะทำให้แอพพลิเคชันคราสหรือสายไปหากไม่ได้จัดการโดย try-catch.
getMeasuredWidth() คืนขนาดที่กำหนดใน onMeasure (ระยะการวัด) getWidth() คืนขนาดจริงที่ View ได้รับใน onLayout หลังจากการปรับตำแหน่งทั้งหมด สำหรับ View ส่วนใหญ่ค่าเหล่านี้ตรงกัน แต่ใน ViewGroup ที่กำหนดเองอาจแตกต่างกัน.
ไม่ได้ onMeasure ถูกออกแบบมาเพื่อการคำนวณขนาดเท่านั้น การเปลี่ยนสถานะ เริ่มแอนิเมชัน การเชื่อมต่อเครือข่ายหรือการอัพเดทข้อมูลในเมธอดนี้จะละเมิดสถาปัตยกรรมของ Android และอาจนำไปสู่การเรียก measure แบบที่เกิดขึ้นเรื่อย เนื่องจากการเปลี่ยนสถานะสามารถทำให้ requestLayout เกิดขึ้น.
ConstraintLayout จัดการวัดลูกโดยอิสระโดยอาศัยข้อจำกัดที่กำหนด หากคุสต้ม View ภายใน ConstraintLayout เกินโอเวอร์ไรด์ onMeasure โดยตรง จะต้องจัดการ MeasureSpec ที่ส่งมาจาก ConstraintLayout อย่างถูกต้อง มิฉะนั้นข้อจำกัดอาจไม่ทำงาน ConstraintLayout ใช้อะไลโกรทัมสองผ่านโดยมี WidgetContainer ของตนเองสำหรับการคำนวณ.
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม