Skip to content
advanced Phase 3 · UI with XML & Views

Custom Views

Create custom views with onDraw, measure, and layout for specialized UI components.

1h
2 problems
Topic Progress 0%

The View Lifecycle: Measure, Layout, Draw

How Android Draws Views

Every view goes through three phases before it appears on screen:

  1. Measure — the parent asks the view how big it wants to be (onMeasure)
  2. Layout — the parent assigns the view its position (onLayout — only for ViewGroups)
  3. 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

  1. Never allocate in onDraw — create Paint and Path objects in init or constructors
  2. Use hardware acceleration — most Canvas operations are hardware-accelerated by default
  3. Call invalidate() sparingly — each call triggers a full redraw
  4. Cache expensive computations — if a value doesn't change between draws, compute it once
  5. Profile with GPU rendering — Developer Options → Profile GPU Rendering shows frame timing

Quiz

1. What is the correct order of the view drawing lifecycle?

Question 1 options

2. When should you call requestLayout() instead of invalidate()?

Question 2 options

3. Why must you call performClick() in onTouchEvent?

Question 3 options

4. Why must you avoid allocating objects in onDraw()?

Question 4 options

Flashcards

Question

What are the three phases of the view drawing lifecycle?

Answer

Measure (determine size), Layout (assign position), Draw (render on Canvas).

Question

What is the difference between invalidate() and requestLayout()?

Answer

invalidate() triggers redraw only (content changed). requestLayout() triggers re-measure + re-layout + redraw (size/position changed).

Question

What are the three MeasureSpec modes?

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?

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:

  1. onMeasure(w, h) → call setMeasuredDimension()
  2. onLayout() → only for ViewGroups
  3. 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