The View Lifecycle: Measure, Layout, Draw
How Android Draws Views
Every view goes through three phases before it appears on screen:
- Measure — the parent asks the view how big it wants to be (
onMeasure) - Layout — the parent assigns the view its position (
onLayout— only for ViewGroups) - Draw — the view renders itself onto the Canvas (
onDraw)
This cycle runs whenever the view's size or content changes. Understanding it is critical for custom views because mistakes in any phase cause incorrect rendering.
The Measure Phase
onMeasure receives width and height constraints from the parent. You must call setMeasuredDimension() with the resolved size:
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 0f
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 200 // fixed size in pixels
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize // match_parent or fixed dp
MeasureSpec.AT_MOST -> minOf(desiredSize, widthSize) // wrap_content
else -> desiredSize // unspecified
}
setMeasuredDimension(width, width) // square view
}
}
MeasureSpec modes:
EXACTLY— parent says be exactly this size (match_parent or fixed dp)AT_MOST— parent says be at most this size (wrap_content)UNSPECIFIED— no constraint (rare, used in ScrollView)
The Draw Phase
onDraw receives a Canvas you can draw on:
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val cx = width / 2f
val cy = height / 2f
radius = minOf(width, height) / 2f - 10f
canvas.drawCircle(cx, cy, radius, paint)
}
When to Call invalidate() vs requestLayout()
| Method | Triggers | When to Use |
|---|---|---|
invalidate() |
Redraw only (onDraw) | Content changed but size didn't |
requestLayout() |
Re-measure + re-layout + redraw | Size or position changed |
Calling requestLayout() when only the color changed wastes CPU time on re-measurement. Call invalidate() when the view's content changes but its bounds stay the same.
Canvas Drawing and Touch Handling
Canvas Primitives
The Canvas provides methods for common shapes and paths:
override fun onDraw(canvas: Canvas) {
// Rectangle with rounded corners
val rect = RectF(10f, 10f, width - 10f, height - 10f)
canvas.drawRoundRect(rect, 16f, 16f, paint)
// Line
val linePaint = Paint().apply {
color = Color.RED
strokeWidth = 4f
}
canvas.drawLine(0f, height / 2f, width.toFloat(), height / 2f, linePaint)
// Path — custom shapes
val path = Path().apply {
moveTo(width / 2f, 0f)
lineTo(width.toFloat(), height.toFloat())
lineTo(0f, height.toFloat())
close()
}
canvas.drawPath(path, paint)
}
Handling Touch Events
Override onTouchEvent to respond to user interaction:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Finger touched down
lastX = event.x
lastY = event.y
performClick() // accessibility
return true
}
MotionEvent.ACTION_MOVE -> {
// Finger moved
val dx = event.x - lastX
val dy = event.y - lastY
// Move your drawing element by dx, dy
lastX = event.x
lastY = event.y
invalidate() // redraw at new position
return true
}
MotionEvent.ACTION_UP -> {
// Finger lifted
return true
}
}
return super.onTouchEvent(event)
}
Always call performClick() on ACTION_DOWN for accessibility support (TalkBack).
Custom Attributes
Read custom attributes defined in res/values/attrs.xml:
init {
attrs?.let {
val typedArray = context.obtainStyledAttributes(it, R.styleable.CircleView)
val color = typedArray.getColor(R.styleable.CircleView_circleColor, Color.BLUE)
val strokeWidth = typedArray.getDimension(R.styleable.CircleView_strokeWidth, 4f)
paint.color = color
paint.strokeWidth = strokeWidth
typedArray.recycle()
}
}
Performance Tips
- Never allocate in onDraw — create Paint and Path objects in
initor constructors - Use hardware acceleration — most Canvas operations are hardware-accelerated by default
- Call invalidate() sparingly — each call triggers a full redraw
- Cache expensive computations — if a value doesn't change between draws, compute it once
- Profile with GPU rendering — Developer Options → Profile GPU Rendering shows frame timing
Quiz
1. What is the correct order of the view drawing lifecycle?
2. When should you call requestLayout() instead of invalidate()?
3. Why must you call performClick() in onTouchEvent?
4. Why must you avoid allocating objects in onDraw()?
Flashcards
Question
What are the three phases of the view drawing lifecycle?
Click to reveal answer
Answer
Measure (determine size), Layout (assign position), Draw (render on Canvas).
Question
What is the difference between invalidate() and requestLayout()?
Click to reveal answer
Answer
invalidate() triggers redraw only (content changed). requestLayout() triggers re-measure + re-layout + redraw (size/position changed).
Question
What are the three MeasureSpec modes?
Click to reveal answer
Answer
EXACTLY (fixed size), AT_MOST (wrap_content), UNSPECIFIED (no constraint, used in ScrollView).
Question
Why must you call typedArray.recycle() after reading custom attributes?
Click to reveal answer
Answer
The TypedArray is a shared resource. Recycling it returns it to the pool for reuse, preventing resource leaks.
Revision Notes
Key Takeaways
- 1. Measure → Layout → Draw is the view lifecycle — mistakes in any phase cause incorrect rendering
- 2. allocatePaint and Path objects in init, never in onDraw — allocation causes GC pauses and frame drops
- 3. invalidate() for content changes, requestLayout() for size/position changes — using the wrong one wastes performance
- 4. Always call performClick() in onTouchEvent for accessibility compliance
- 5. Use TypedArray with obtainStyledAttributes to read custom XML attributes, then recycle
Interview Tips
- • Walk through the measure → layout → draw lifecycle and what each phase does
- • Explain MeasureSpec modes (EXACTLY, AT_MOST, UNSPECIFIED) with examples
- • Discuss why onDraw allocation is harmful — GC pauses cause dropped frames
- • Be ready to handle touch events with MotionEvent and explain performClick for accessibility
Cheat Sheet
Custom Views Cheat Sheet
Lifecycle:
- onMeasure(w, h) → call setMeasuredDimension()
- onLayout() → only for ViewGroups
- onDraw(canvas) → draw shapes, paths, text
MeasureSpec modes:
- EXACTLY: match_parent or fixed dp → use size directly
- AT_MOST: wrap_content → use min(desired, available)
- UNSPECIFIED: no constraint → use desired size
Key rules:
- Never allocate Paint/Path in onDraw() — create in init
- Call performClick() for accessibility
- Use invalidate() for content changes, requestLayout() for size changes
- Call typedArray.recycle() after reading custom attrs
Canvas methods:
- drawCircle, drawRect, drawRoundRect, drawLine, drawPath, drawText
- Use RectF (not Rect) for rounded corners