androidengineers.Book a session

Media & Graphics

Custom Views & Canvas Perf

article20 minHard

Custom Views let you draw arbitrary graphics with Canvas, handle touch events precisely, and achieve visual effects that standard widgets can't provide. The main challenge is keeping rendering fast — every frame budget is 16ms at 60fps.

Custom View Anatomy

class SpeedometerView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // Step 1: Define paints ONCE — never create Paint objects in onDraw
    private val arcPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.STROKE
        strokeWidth = 20f
        strokeCap = Paint.Cap.ROUND
        color = Color.parseColor("#2196F3")
    }
    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        textSize = 48f
        color = Color.WHITE
        textAlign = Paint.Align.CENTER
    }

    private val arcRect = RectF()

    // Step 2: Compute layout-dependent values in onSizeChanged — not onDraw
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        val padding = 40f
        arcRect.set(padding, padding, w - padding, h - padding)
    }

    // Step 3: Draw only — no allocations, no calculations
    var speedPercent: Float = 0f
        set(value) {
            field = value.coerceIn(0f, 1f)
            invalidate()  // ← triggers onDraw on next frame
        }

    override fun onDraw(canvas: Canvas) {
        // Background arc
        arcPaint.color = Color.parseColor("#37474F")
        canvas.drawArc(arcRect, 135f, 270f, false, arcPaint)

        // Speed arc
        arcPaint.color = Color.parseColor("#2196F3")
        canvas.drawArc(arcRect, 135f, 270f * speedPercent, false, arcPaint)

        // Speed text
        val cx = width / 2f
        val cy = height / 2f
        canvas.drawText("${(speedPercent * 100).toInt()} km/h", cx, cy, textPaint)
    }
}

Avoiding Overdraw

Overdraw = drawing pixels that are later covered by other pixels. Inspect with Developer Options > Debug GPU Overdraw.

// ❌ Drawn by parent — skip the background if parent already fills it
override fun onDraw(canvas: Canvas) {
    canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), backgroundPaint) // overdraw!
    canvas.drawCircle(...)
}

// ✅ Skip the background rect if View's background drawable handles it
class MyView : View(...) {
    init {
        setWillNotDraw(false)  // ensure onDraw is called even for subclasses
    }
}

Hardware Layers

Hardware layers accelerate repeated animations by caching the view on the GPU:

// Useful when animating a view that draws slowly
view.animate()
    .alpha(0f)
    .withLayer()  // promotes to hardware layer during animation
    .start()

// Or manually
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)  // set before animation
// ... animate ...
view.setLayerType(View.LAYER_TYPE_NONE, null)       // clear after animation

Warning: hardware layers use GPU memory. Don't leave them permanently on static views.

Custom ViewGroup: Measuring and Layout

class FlowLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val maxWidth = MeasureSpec.getSize(widthMeasureSpec)
        var currentRowWidth = 0
        var currentRowHeight = 0
        var totalHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChild(child, widthMeasureSpec, heightMeasureSpec)

            if (currentRowWidth + child.measuredWidth > maxWidth) {
                totalHeight += currentRowHeight  // wrap to next row
                currentRowWidth = 0
                currentRowHeight = 0
            }
            currentRowWidth += child.measuredWidth
            currentRowHeight = maxOf(currentRowHeight, child.measuredHeight)
        }
        totalHeight += currentRowHeight

        setMeasuredDimension(maxWidth, totalHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var x = 0; var y = 0; var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (x + child.measuredWidth > width) { y += rowHeight; x = 0; rowHeight = 0 }
            child.layout(x, y, x + child.measuredWidth, y + child.measuredHeight)
            x += child.measuredWidth
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
    }
}

Path and Clip Operations

override fun onDraw(canvas: Canvas) {
    // Clip to a rounded rect — cheap GPU operation
    val clipPath = Path().apply {
        addRoundRect(0f, 0f, width.toFloat(), height.toFloat(), 24f, 24f, Path.Direction.CW)
    }
    canvas.clipPath(clipPath)

    // Draw background image (now clipped to rounded rect)
    canvas.drawBitmap(bitmap, 0f, 0f, null)
}

Key Performance Rules

RuleWhy
Never allocate in onDrawGC pauses → dropped frames
Compute dimensions in onSizeChangedonDraw is called every frame; onSizeChanged only when dimensions change
invalidate() only when state changesUnnecessary invalidations waste GPU time
invalidate(Rect) for partial updatesNarrow the dirty region when only part of the view changes
Hardware layer during animationAvoids re-drawing complex views on every frame
Avoid clipPath on older APIsclipPath disabled hardware acceleration below API 18

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Custom Views & Canvas Perf | Android System Design | Android Engineers