View Lifecycle — ลำดับของเมธอดที่ Android เรียกเพื่อวาดและวาดใหม่ส่วนติดต่อผู้ใช้ (View) บนหน้าจอ ต่างจาก Activity หรือ Fragment, View เป็นองค์ประกอบที่มีน้ำหนักเบาไม่มีวงจรชีวิตขยาย แต่ผ่านกระบวนการสามขั้นตอนที่เข้มงวด: onMeasure (การวัด), onLayout (การวางตำแหน่ง), onDraw (การวาด) การเข้าใจ View Lifecycle เป็นสิ่งจำเป็นสำหรับการสร้าง View แบบกำหนดเอง การเพิ่มประสิทธิภาพ และการแก้ปัญหาการวาด ตามข้อมูลจาก Google, View แบบกำหนดเองเมื่อนำไปใช้อย่างถูกต้อง ทำให้ UI เร็วขึ้น 15–40% เมื่อเทียบกับการใช้ ViewGroups มาตรฐานที่ซ้อนกัน เอกสาร Android เกี่ยวกับ View แบบกำหนดเอง อธิบาย onMeasure, onLayout และ onDraw เป็นสามเสาหลักของ View Lifecycle
ข้อควรจำสำคัญ
View Lifecycle คือกระบวนการที่ View (และ ViewGroup) ของ Android ผ่านเพื่อแสดงตัวเองบนหน้าจอ ต่างจาก Activity หรือ Fragment, View ไม่มี onStart/onStop/onDestroy — วงจรชีวิตของมันประกอบด้วยกระบวนการเป็นวงจรของการวัด การวางตำแหน่ง และการวาด วงจรนี้ถูกกระตุ้นทุกครั้งที่ View ต้องการแสดงหรือวาดใหม่
สามระยะของ View Lifecycle:
วงจร View Lifecycle ที่สมบูรณ์ยังรวมถึงเมธอดที่เกี่ยวข้องกับการแนบ View กับหน้าต่าง: onAttachedToWindow (View แนบกับหน้าต่าง, มีการเร่ง HW) และ onDetachedFromWindow (View ถูกถอด, ทรัพยากรถูกปล่อย) เมธอดเหล่านี้ถูกเรียกครั้งเดียวต่ออายุของ View และสำคัญสำหรับการลงทะเบียน/ยกเลิกแอนิเมชันและเซ็นเซอร์
ตาม Android Performance Blog, 65% ของปัญหาประสิทธิภาพ UI (jank, เฟรมหลุด) เกี่ยวข้องกับการใช้งาน onMeasure และ onDraw ที่ไม่ถูกต้อง: การแทนที่มากเกินไป, การเรียก requestLayout() โดยไม่จำเป็น, การสร้างออบเจ็กต์ใน onDraw
onMeasure — ระยะที่สำคัญที่สุดและซับซ้อนที่สุดของ View Lifecycle ในขั้นตอนนี้ Android กำหนดว่า View จะใช้พื้นที่เท่าใดบนหน้าจอ ระบบส่ง MeasureSpec — คำแนะนำที่บรรจุใน int ประกอบด้วยโหมดและขนาด
สามโหมด MeasureSpec:
| โหมด | ค่าคงที่ | ความหมาย | ตัวอย่าง |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | ขนาดที่แน่นอนกำหนดโดยผู้เป็นเจ้าของ (match_parent หรือความกว้างคงที่) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View สามารถมีขนาดสูงสุดที่ระบุ (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | ไม่มีข้อจำกัด — View สามารถมีขนาดใดก็ได้ (ScrollView, RecyclerView) | ความกว้างไม่จำกัด → MeasureSpec(0, UNSPECIFIED) |
การใช้งาน onMeasure ควร:
setMeasuredDimension(int width, int height) เพื่อบันทึกขนาดที่วัดได้getPaddingLeft() + getPaddingRight() จากความกว้างที่มีmeasureChild() หรือ measureChildWithMargins()ข้อผิดพลาดทั่วไป: ไม่คำนึงถึง MeasureSpec เมื่อใช้ wrap_content ถ้า View ถูกตั้งเป็น wrap_content แต่ onMeasure ไม่จัดการ AT_MOST และส่งคืนขนาดคงที่ View จะถูกตัดหรือใช้พื้นที่มากเกินกว่าที่จำเป็น
onLayout — ระยะที่ View หรือ ViewGroup จัดเรียงองค์ประกอบย่อย ภายในขอบเขตของมัน สำหรับ View ทั่วไป (ไม่ใช่ ViewGroup), onLayout ไม่จำเป็น — ระบบเรียก layout() ด้วยพารามิเตอร์ที่ส่งจากผู้เป็นเจ้าของ สำหรับ ViewGroup, onLayout เป็นสิ่งจำเป็น — หากไม่มี View ย่อยจะไม่ถูกวาง
ลายเซ็นของ onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// การจัดเรียง View ย่อย
}
พารามิเตอร์ changed บ่งชี้ว่าตำแหน่งหรือขนาดของ View เปลี่ยนไปเมื่อเทียบกับ layout ก่อนหน้าหรือไม่ ถ้าเป็น false, View สามารถข้ามการคำนวณตำแหน่งขององค์ประกอบย่อยใหม่เพื่อการเพิ่มประสิทธิภาพ
สำหรับ ViewGroup, onLayout ควร:
getChildCount() และ getChildAt(i)child.layout(l, t, r, b) สำหรับแต่ละองค์ประกอบย่อยonLayout ถูกเรียกหลังจาก onMeasure — ขนาดที่วัดได้พร้อมใช้งานผ่าน getMeasuredWidth()/getMeasuredHeight() ถ้า View ย่อยมีขนาดจริงที่แตกต่างหลังจาก layout(), requestLayout() จะถูกเรียกเพื่อวัดใหม่ สิ่งนี้เรียกว่า "layout pass" และสามารถกระตุ้นปฏิกิริยาลูกโซ่ของการคำนวณใหม่
onDraw — ระยะที่ View วาดตัวเองบน Canvas นี่เป็นระยะเดียวที่สามารถถูกเรียกหลายครั้งโดยไม่ต้อง onMeasure และ onLayout — ถ้า View ถูกทำเครื่องหมายเป็น invalidate() Canvas ให้ API การวาด: drawLine, drawRect, drawCircle, drawText, drawBitmap และ drawPath
กฎของ onDraw:
canvas.clipRect() เพื่อตัดส่วนที่มองไม่เห็นลำดับการวาดใน ViewGroup: พื้นหลัง (setBackgroundDrawable) → onDraw (เนื้อหา) → dispatchDraw (View ย่อย) → onDrawForeground (เบื้องหน้า) dispatchDraw เรียก onDraw ของแต่ละองค์ประกอบย่อย การแทนที่ dispatchDraw ใช้สำหรับการใช้เอฟเฟกต์เหนือองค์ประกอบย่อย
ตามสถิติ Android Vitals, สาเหตุที่พบบ่อยที่สุดของเฟรมหลุด ใน onDraw คือการสร้างออบเจ็กต์ภายในเมธอด (48%), การเรียก decodeResource (22%) และการดำเนินการ Path ที่ซับซ้อนโดยไม่มีแคช (15%)
การทำให้ใช้งานไม่ได้ — กลไกที่กระตุ้นการวาดใหม่ของ View การเรียก invalidate() ทำเครื่องหมาย View ว่า "สกปรก" และกำหนดเวลาการเรียก onDraw ในวงจรการวาดถัดไป การเรียก requestLayout() เป็นการดำเนินการที่ "หนักกว่า" ซึ่งเริ่มวงจรที่สมบูรณ์: onMeasure → onLayout → onDraw
| เมธอด | มันทำอะไร | เมื่อไรใช้ |
|---|---|---|
| invalidate() | เริ่ม onDraw โดยไม่ต้อง onMeasure/onLayout | เฉพาะรูปลักษณ์เปลี่ยน (สี, ข้อความ, ความคืบหน้า) |
| invalidate(Rect) | วาดใหม่เฉพาะพื้นที่ที่ระบุ | ส่วนหนึ่งของ View เปลี่ยน — แอนิเมชัน, การเลือก |
| postInvalidate() | เรียก invalidate จาก non-UI thread | background thread อัปเดตข้อมูลสำหรับการวาด |
| requestLayout() | เริ่ม onMeasure → onLayout → onDraw | ขนาดเนื้อหาเปลี่ยน (ข้อความ, รูปภาพ) |
| forceLayout() | ทำเครื่องหมาย View สำหรับการวัดใหม่แบบบังคับ | สถานะภายในเปลี่ยน ขนาดอาจเปลี่ยน |
แอนิเมชันและ View Lifecycle: ViewPropertyAnimator และ ValueAnimator เรียก invalidate() ในทุกเฟรมแอนิเมชัน ObjectAnimator เรียก setter บน View ซึ่งถ้า setter เปลี่ยนขนาด (กว้าง/สูง) จะเรียก requestLayout() โดยอัตโนมัติ สิ่งนี้อาจมีค่าใช้จ่ายสูงสำหรับ ViewGroups ที่ซับซ้อน: แต่ละ requestLayout กระตุ้นลำดับชั้นทั้งหมดจนถึง root view
กฎการเพิ่มประสิทธิภาพ: invalidate() แทน requestLayout() ทุกที่ที่เฉพาะรูปลักษณ์เปลี่ยน (สี, ความโปร่งใส, การหมุนโดยไม่เปลี่ยนขนาด) ใช้ requestLayout เฉพาะเมื่อเปลี่ยนขนาดหรือเนื้อหาที่มีผลต่อขนาด
View แบบกำหนดเองเป็นเครื่องมือที่ทรงพลังสำหรับการสร้าง UI ที่ไม่ซ้ำใคร แต่ต้องการการปฏิบัติตามกฎประสิทธิภาพอย่างเคร่งครัด นี่คือคำแนะนำหลักของ Google สำหรับการเพิ่มประสิทธิภาพ View Lifecycle
setLayerType(LAYER_TYPE_HARDWARE) สำหรับ View ที่มีแอนิเมชันและ setLayerType(LAYER_TYPE_NONE) หลังจากเสร็จสิ้นตัวบ่งชี้ความคืบหน้าแบบวงกลมอย่างง่ายที่มีการใช้งาน onMeasure, onDraw และ invalidate ที่ถูกต้อง
class CircularProgressView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.STROKE
strokeWidth = 8f
strokeCap = Paint.Cap.ROUND
}
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 8f
}
private var progress = 0f
private var viewWidth = 0
private var viewHeight = 0
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 100f)
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 100 * resources.displayMetrics.density.toInt()
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(width, height).coerceAtLeast(desiredSize)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val padding = progressPaint.strokeWidth / 2
val radius = (minOf(viewWidth, viewHeight) - padding) / 2
val cx = viewWidth / 2f
val cy = viewHeight / 2f
canvas.drawCircle(cx, cy, radius, backgroundPaint)
val sweepAngle = (progress / 100f) * 360f
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
-90f, sweepAngle, false, progressPaint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
}
}
แถบความคืบหน้าแบบวงกลม: onMeasure ส่งคืนขนาดสี่เหลี่ยมตาม MeasureSpec, onSizeChanged จดจำขนาด, onDraw วาดพื้นหลังและส่วนโค้งความคืบหน้า Invalidate ถูกเรียกเมื่อความคืบหน้าเปลี่ยน — onMeasure/onLayout ไม่ได้รับผลกระทบ Paint ถูกสร้างครั้งเดียวในคอนสตรัคเตอร์ ไม่ใช่ใน onDraw
ViewGroup แบบกำหนดเองที่จัดเรียง View ย่อยเป็นแถว (เหมือน Flexbox wrap)
class FlowLayout constructor(
context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
private val horizontalSpacing = 8.dpToPx(resources)
private val verticalSpacing = 8.dpToPx(resources)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
var totalHeight = paddingTop + paddingBottom
var rowWidth = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
if (rowWidth + child.measuredWidth > width - paddingRight) {
totalHeight += rowHeight + verticalSpacing
rowWidth = paddingLeft
rowHeight = 0
}
rowWidth += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
totalHeight += rowHeight
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean,
l: Int, t: Int, r: Int, b: Int) {
var rowTop = paddingTop
var rowLeft = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (rowLeft + child.measuredWidth > r - paddingRight) {
rowTop += rowHeight + verticalSpacing
rowLeft = paddingLeft
rowHeight = 0
}
child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
rowLeft += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
FlowLayout แทนที่ onMeasure: วัดแต่ละองค์ประกอบย่อย เลื่อนไปแถวใหม่เมื่อเกินความกว้าง คำนวณความสูงทั้งหมด onLayout วางองค์ประกอบย่อยตามพิกัดโดยคำนึงถึงการขึ้นบรรทัดใหม่ generateLayoutParams ส่งคืน MarginLayoutParams เพื่อรองรับ margin บน View ย่อย
View แบบกำหนดเองวาดเส้นโค้ง Bezier ที่เรียบ คำนวณ Path ล่วงหน้าและแคชมัน
class WaveView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#4A90D9")
style = Paint.Style.FILL
}
private val wavePath = Path()
private var isPathDirty = true
private var viewWidth = 0
private var viewHeight = 0
fun refreshWave() {
isPathDirty = true
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
isPathDirty = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (isPathDirty) {
wavePath.reset()
val amplitude = viewHeight * 0.1f
wavePath.moveTo(0f, viewHeight * 0.5f)
for (x in 0..viewWidth step 4) {
val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
wavePath.lineTo(x.toFloat(), y)
}
wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
wavePath.lineTo(0f, viewHeight.toFloat())
wavePath.close()
isPathDirty = false
}
canvas.drawPath(wavePath, wavePaint)
}
}
แคช Path: isPathDirty = true เฉพาะเมื่อขนาด View เปลี่ยนหรือเรียก refreshWave() ใน onDraw, Path จะถูกคำนวณใหม่เฉพาะเมื่อมัน "สกปรก" สิ่งนี้ป้องกันการคำนวณเส้นโค้ง Bezier ใหม่ในทุกเฟรมแอนิเมชัน ประหยัด CPU
คำถามที่พบบ่อย
View Lifecycle เป็นกระบวนการวาดแบบวงจร (onMeasure → onLayout → onDraw) ที่เป็นอิสระจากการสร้าง/ทำลาย Activity View ไม่มี onStart/onStop — มัน either มองเห็นได้ (แนบกับหน้าต่าง) หรือไม่ Activity Lifecycle จัดการสถานะขององค์ประกอบแอปพลิเคชัน View Lifecycle จัดการการวาด UI
requestLayout() เริ่มวงจรที่สมบูรณ์ onMeasure → onLayout → onDraw สำหรับต้นไม้ View ทั้งหมดจากราก ถ้า requestLayout() ถูกเรียกบ่อยครั้ง (เช่น ทุกเฟรมแอนิเมชัน) มันทำให้เกิด jank และเฟรมหลุด ตาม Google หนึ่ง requestLayout ใช้เวลาเฉลี่ย 2–5 ms บน ViewGroup ที่มี 10 องค์ประกอบ สำหรับแอนิเมชัน ใช้ invalidate()
onAttachedToWindow ถูกเรียกเมื่อ View แนบกับ Window — กลายเป็นส่วนหนึ่งของลำดับชั้นที่มองเห็น ในขณะนั้น View ได้รับการเร่ง HW และการเข้าถึงทรัพยากร Window (WindowManager, Display) onAttachedToWindow เป็นสถานที่ที่เหมาะสมสำหรับการลงทะเบียนตัวฟังแอนิเมชันและ BroadcastReceiver ที่มีชีวิตตราบเท่าที่ View มองเห็น
Overdraw คือสถานการณ์ที่พิกเซลถูกวาดหลายครั้งในเฟรมเดียว แต่ละรอบพิเศษสูญเสียเวลา GPU วิธีการลด: ตั้ง windowBackground ใน theme (อย่าวาดพื้นหลังใน layout), ใช้ canvas.clipRect(), หลีกเลี่ยงการรวมพื้นหลังที่ซ้อนกัน, ใช้ ConstraintLayout แทน LinearLayout ที่ซ้อนกัน Android Studio → Profile GPU Rendering → Overdraw แสดงแผนที่สีของ overdraw (น้ำเงิน = 1x, แดง = 3x+)
ใช่ ถ้า View มีพื้นหลัง super.onDraw() วาดพื้นหลังของ View ถ้า View แบบกำหนดเองของคุณไม่มีพื้นหลังหรือคุณวาดพื้นหลังของคุณเอง สามารถละ super.onDraw() ได้ — ประหยัดการวาดหนึ่งรอบ สำหรับ ViewGroup, super.dispatchDraw() เป็นสิ่งจำเป็น — มันวาด View ย่อย
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม