onLayout() เป็นเมธอดของคลาส ViewGroup ที่กำหนดตำแหน่งและขนาดของ View ย่อยบนระนาบพิกัดของคอนเทนเนอร์หลัก ระบบ Android จะเรียก onLayout หลังจากขั้นตอนการวัด (onMeasure) เมื่อทราบความกว้างและความสูงที่วัดได้ของ View ย่อยแต่ละตัวแล้ว ตามเอกสาร Android Developers Documentation (2026) onLayout เป็นเมธอดที่จำเป็นต้องโอเวอร์ไรด์ใน ViewGroup ที่กำหนดเอง เนื่องจากการ implement มาตรฐานของ ViewGroup ไม่ได้ทำการจัดตำแหน่งอัตโนมัติขององค์ประกอบย่อย
ประเด็นสำคัญ
onLayout(boolean changed, int l, int t, int r, int b) เป็นเมธอด protected ของคลาส ViewGroup ที่ระบบเรียกเพื่อจัดตำแหน่ง View ย่อยภายในคอนเทนเนอร์หลัก นักพัฒนาโอเวอร์ไรด์เมธอดนี้เมื่อสร้าง ViewGroup ที่กำหนดเองที่มีการจัดเรียงองค์ประกอบที่ไม่เป็นมาตรฐาน: แบบเรียงซ้อน, ตาราง, แบบกระดานหมากรุก หรือตามพิกัดที่กำหนดเอง View ย่อยแต่ละตัวจะได้รับขอบเขตสุดท้ายผ่านการเรียก child.layout()
พารามิเตอร์ changed บ่งชี้ว่าตำแหน่งหรือขนาดของ ViewGroup เองเปลี่ยนแปลงไปตั้งแต่ layout ครั้งล่าสุดหรือไม่ ถ้า changed เป็น true องค์ประกอบย่อยทั้งหมดอาจจำเป็นต้องจัดตำแหน่งใหม่ พารามิเตอร์ l, t, r, b คือพิกัดของมุมบนซ้ายและมุมล่างขวาของ ViewGroup ในระบบพิกัดของหลัก ภายใน onLayout นักพัฒนาใช้ค่าเหล่านี้เป็นพิกัดเริ่มต้นสำหรับการจัดเรียงองค์ประกอบย่อย
ViewGroup เป็นคลาสเดียวที่โอเวอร์ไรด์ onLayout View ทั่วไป (ที่ไม่ใช่ ViewGroup) ไม่มีองค์ประกอบย่อยและไม่จำเป็นต้องใช้ onLayout — การจัดตำแหน่งของมันถูกจัดการโดยคอนเทนเนอร์หลัก แม้ว่า View ทั่วไปจะโอเวอร์ไรด์ onLayout ระบบจะไม่เรียกมัน นี่คือความแตกต่างพื้นฐานจาก onMeasure ซึ่งถูกเรียกสำหรับ View ใดๆ
ขั้นตอน layout เริ่มต้นด้วยการเรียกเมธอดสาธารณะ layout(int l, int t, int r, int b) บน View ระดับราก เมธอดนี้กำหนดพิกัดสุดท้ายของ View เองและเรียก onLayout ถ้า View เป็น ViewGroup จากนั้น onLayout จะเรียก child.layout() แบบเวียนเกิดสำหรับองค์ประกอบย่อยแต่ละตัว และกระบวนการจะทำซ้ำลงไปตามลำดับชั้น ดังนั้น layout จึงแพร่กระจายจากรากไปยังใบ
ก่อนเรียก onLayout ระบบจะตรวจสอบว่าขนาดของ View เปลี่ยนแปลงไปเมื่อเทียบกับรอบก่อนหน้าหรือไม่ ถ้าขนาดไม่เปลี่ยนแปลงและไม่ได้เรียก requestLayout onLayout อาจไม่ถูกเรียก — ระบบใช้ผลลัพธ์ของ layout ก่อนหน้า นี่คือการปรับให้เหมาะสมที่ป้องกันการคำนวณตำแหน่งซ้ำโดยไม่จำเป็นระหว่างแอนิเมชันหรือการเลื่อน เมื่อเฉพาะเนื้อหาเปลี่ยนแปลงแต่ขนาดไม่เปลี่ยน
requestLayout() เป็นเมธอดของ View ที่แจ้งระบบว่า layout ของ View ล้าสมัยและจำเป็นต้องคำนวณใหม่ การเรียก requestLayout จะเริ่มรอบที่สมบูรณ์: ก่อนอื่น onMeasure ถูกเรียก จากนั้น onLayout จากนั้น onDraw ต่างจาก invalidate ที่เริ่มเพียงการวาดใหม่ requestLayout เริ่มการคำนวณขนาดและตำแหน่งใหม่ทั้งหมด การเรียกที่มากเกินไป ไปยัง requestLayout เป็นสาเหตุทั่วไปของปัญหาประสิทธิภาพ
l (left) — พิกัด X ของขอบซ้ายของ ViewGroup ในระบบพิกัดหลัก t (top) — พิกัด Y ของขอบบน r (right) — พิกัด X ของขอบขวา b (bottom) — พิกัด Y ของขอบล่าง ความกว้างของ ViewGroup คำนวณเป็น r - l ความสูงเป็น b - t พิกัดเหล่านี้รวม padding ทั้งหมดของ ViewGroup เองแล้ว
ภายใน onLayout นักพัฒนาเรียก child.layout(int childLeft, int childTop, int childRight, int childBottom) สำหรับ View ย่อยแต่ละตัว พิกัดที่ส่งไปยัง child.layout ต้องอยู่ในระบบพิกัดของ ViewGroup หลัก โดยปกติ childLeft และ childTop จะคำนวณโดยพิจารณา padding ของหลัก: childLeft = l + paddingLeft + offsetX, childTop = t + paddingTop + offsetY
| พารามิเตอร์ | คำอธิบาย | การใช้งานทั่วไป |
|---|---|---|
| l (left) | พิกัดขอบซ้ายของ ViewGroup ในหลัก | จุดเริ่มต้นบนแกน X สำหรับองค์ประกอบย่อย |
| t (top) | พิกัดขอบบนของ ViewGroup ในหลัก | จุดเริ่มต้นบนแกน Y สำหรับองค์ประกอบย่อย |
| r (right) | พิกัดขอบขวาของ ViewGroup ในหลัก | ขอบเขตบนของความกว้าง, r - l = getWidth() |
| b (bottom) | พิกัดขอบล่างของ ViewGroup ในหลัก | ขอบเขตบนของความสูง, b - t = getHeight() |
พิกัดย่อย คำนวณโดยสูตร: childLeft = l + paddingLeft + (marginLeft ถ้ามี), childRight = childLeft + child.getMeasuredWidth() ในทำนองเดียวกันสำหรับแกนตั้ง: childTop = t + paddingTop + (marginTop), childBottom = childTop + child.getMeasuredHeight() หลังจากคำนวณค่าทั้งสี่นี้แล้ว child.layout(childLeft, childTop, childRight, childBottom) จะถูกเรียก
มาสร้าง FlowLayout — ViewGroup ที่กำหนดเองซึ่งจัดเรียง View ย่อยเป็นแถว ย้ายองค์ประกอบไปยังแถวใหม่เมื่อแถวปัจจุบันเต็ม นี่คือสิ่งที่เทียบเท่ากับ Flexbox แบบ wrap ในระนาบเดียว onLayout วนซ้ำผ่าน View ย่อยทั้งหมด คำนวณตำแหน่งสำหรับแต่ละตัว และเรียก child.layout() ด้วยขอบเขตที่ถูกต้อง
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)
}
onMeasure และ onLayout เป็นสองขั้นตอนต่อเนื่องของวงจรชีวิต View ที่ทำงานแตกต่างกันโดยพื้นฐาน onMeasure กำหนดขนาดที่ต้องการ (ที่วัดได้) ของ View ในขณะที่ onLayout กำหนดพิกัดและขนาดจริง (สุดท้าย) ความแตกต่างสำคัญ: ใน onMeasure ขนาดอาจเป็นชั่วคราวและสามารถปรับเปลี่ยนได้ภายหลังโดยหลัก ในขณะที่ onLayout กำหนดตำแหน่งสุดท้ายของ View ย่อยแต่ละตัว
onMeasure ถูกเรียกสำหรับทุก View รวมถึง View ปลายทาง (TextView, ImageView, Button) onLayout ถูกเรียกสำหรับ ViewGroup เท่านั้น ทั้งนี้เพราะการจัดตำแหน่งเป็นความรับผิดชอบของคอนเทนเนอร์หลัก ไม่ใช่ของ View เอง View ปลายทางได้รับตำแหน่งผ่าน layout() ที่เรียกจาก onLayout ของหลัก
getMeasuredWidth() และ getMeasuredHeight() พร้อมใช้งานหลังจาก onMeasure ในขณะที่ getWidth() และ getHeight() พร้อมใช้งานหลังจาก onLayout เท่านั้น ถ้าคุณเข้าถึง getWidth() ภายใน onMeasure มันจะคืนค่าจากรอบก่อนหน้าหรือศูนย์ ดังนั้น สำหรับการคำนวณขนาดใน onMeasure คุณควรใช้ MeasureSpec และองค์ประกอบย่อยตามลำดับ
การจัดตำแหน่งโดยไม่คำนึงถึง padding — ข้อผิดพลาดแรกเมื่อ implement onLayout นักพัฒนามักลืมเพิ่ม paddingLeft และ paddingTop ของหลักไปยังพิกัดเริ่มต้นของ View ย่อย ผลลัพธ์คือองค์ประกอบย่อยแสดงที่ขอบของ ViewGroup โดยไม่สนใจ padding ที่กำหนดผ่าน setPadding() หรือในมาร์กอัป XML การคำนวณที่ถูกต้อง: childLeft = paddingLeft + offsetX
การเรียก layout สำหรับองค์ประกอบย่อยที่มองไม่เห็น — ปัญหาทั่วไปที่สอง ถ้า ViewGroup มี View ย่อยที่มีการมองเห็นเป็น GONE ไม่จำเป็นต้องจัดตำแหน่ง — มันไม่กินพื้นที่ อย่างไรก็ตาม onLayout ควรจัดการกรณีนี้อย่างถูกต้อง โดยข้ามองค์ประกอบย่อย GONE สำหรับองค์ประกอบย่อย INVISIBLE ยังคงต้องเรียก layout — มันคงพื้นที่ไว้แม้จะไม่แสดง
การละเลยพารามิเตอร์ changed — ข้อผิดพลาดที่สาม พารามิเตอร์ changed บ่งชี้ว่าขนาดหรือตำแหน่งของ ViewGroup เปลี่ยนแปลงหรือไม่ ถ้า changed == false สามารถใช้พิกัดที่แคชไว้ได้โดยไม่ต้องคำนวณ layout ขององค์ประกอบย่อยทั้งหมดใหม่ อย่างไรก็ตาม การแคช layout ทั้งหมดเป็นงานที่ซับซ้อน และในการ implement ส่วนใหญ่ onLayout เพียงคำนวณ องค์ประกอบทั้งหมด ใหม่ทุกครั้ง ซึ่งยอมรับได้เมื่อมีจำนวนองค์ประกอบย่อยน้อย
คำถามที่พบบ่อย
ใช่ เป็นไปได้ ถ้า ViewGroup ใช้ LayoutParams มาตรฐานและไม่เพิ่มตรรกะการจัดตำแหน่งที่กำหนดเอง อย่างไรก็ตาม การ implement มาตรฐานของ onLayout ใน ViewGroup ไม่ดำเนินการใดๆ — องค์ประกอบย่อยจะไม่ถูกจัดตำแหน่ง ในทางปฏิบัติ ViewGroup ทั้งหมด (LinearLayout, RelativeLayout, FrameLayout) โอเวอร์ไรด์ onLayout
layout() เป็นเมธอด public final ของ View ที่ถูกเรียกโดยระบบหรือ ViewGroup หลัก มันกำหนดพิกัดของ View เองและเรียก onLayout ถ้า View เป็น ViewGroup onLayout() เป็นเมธอด protected ที่นักพัฒนาโอเวอร์ไรด์สำหรับการจัดเรียงองค์ประกอบย่อยที่กำหนดเอง
ในทางเทคนิค — ได้ แต่ไม่แนะนำอย่างยิ่ง เพราะนำไปสู่การเรียกซ้ำไม่สิ้นสุด: requestLayout → onMeasure → onLayout → requestLayout ถ้า requestLayout ถูกเรียกภายใน onLayout ระบบจะโยนข้อยกเว้น StackOverflowError การเปลี่ยนแปลงขนาดทั้งหมดควรดำเนินการก่อน onLayout
แอนิเมชัน layout (LayoutTransition) สกัดกั้นการเปลี่ยนแปลงตำแหน่งของ View ย่อยและใช้แอนิเมชันการเปลี่ยนภาพ เมื่อเปิดใช้ LayoutTransition onLayout จะกำหนดตำแหน่งสุดท้ายก่อน จากนั้น LayoutTransition จะทำแอนิเมชันการเคลื่อนที่จากตำแหน่งเก่าไปยังตำแหน่งใหม่ ซึ่งต้องการการ implement onLayout ที่ถูกต้องด้วยพิกัดสุดท้ายที่เหมาะสม
invalidate() เริ่มเฉพาะขั้นตอน draw (การวาดใหม่) โดยไม่ส่งผลต่อ measure และ layout ในการเริ่ม onLayout คุณต้องเรียก requestLayout() ซึ่งเริ่มรอบที่สมบูรณ์: measure → layout → draw invalidate มีประสิทธิภาพมากกว่าสำหรับการอัปเดตลักษณะที่ปรากฏเมื่อขนาดและตำแหน่งไม่เปลี่ยนแปลง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม