0Pricing
Android Academy · Lesson

Custom Views

Draw custom UI components from scratch. Extend View, implement onMeasure and onDraw with Canvas and Paint, define custom XML attributes, and build compound views.

Custom Views is a free Android Academy lesson on CoddyKit — lesson 3 of 5. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 5 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Custom Views?

Sometimes the built-in Android views don't do exactly what you need. Custom views let you:

  • Draw shapes, graphs, progress rings, or charts that don't exist in the SDK
  • Create branded UI components used consistently throughout the app
  • Combine several views into one reusable component (Compound View)
  • Achieve performance by eliminating nested layout hierarchies

Extending View

Create a custom view by extending View (or a subclass like ImageView). Override the required constructor:

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

    var progress: Float = 0f
        set(value) {
            field = value.coerceIn(0f, 100f)
            invalidate()   // trigger redraw
        }

    // onMeasure and onDraw go here
}

onMeasure()

onMeasure() tells the parent how big this view wants to be. Always call setMeasuredDimension() at the end:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val desiredSize = 200  // dp → pixels
    val px = (desiredSize * resources.displayMetrics.density).toInt()

    val width  = resolveSize(px, widthMeasureSpec)
    val height = resolveSize(px, heightMeasureSpec)

    // Square: same width and height
    val size = minOf(width, height)
    setMeasuredDimension(size, size)
}

Paint — The Drawing Tool

A Paint object holds the style information for drawing (color, stroke width, anti-aliasing). Create it once — never inside onDraw():

class CircleProgressView ... : View(...) {

    // Create Paint in init, not in onDraw!
    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 20f
        strokeCap = Paint.Cap.ROUND
    }

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#6750A4")
        style = Paint.Style.STROKE
        strokeWidth = 20f
        strokeCap = Paint.Cap.ROUND
    }
}

onDraw() — Drawing the View

onDraw(canvas) is called every time the view needs to be redrawn. Use the Canvas to draw shapes, text, and bitmaps:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    val cx = width / 2f
    val cy = height / 2f
    val radius = (minOf(width, height) / 2f) - 30f

    // Draw background circle
    canvas.drawCircle(cx, cy, radius, backgroundPaint)

    // Draw progress arc
    val oval = RectF(cx - radius, cy - radius, cx + radius, cy + radius)
    val sweepAngle = 360f * (progress / 100f)
    canvas.drawArc(oval, -90f, sweepAngle, false, progressPaint)
}

invalidate() vs requestLayout()

Two methods to trigger an update:

  • invalidate() — redraws the view (calls onDraw). Use when the content changes but the size stays the same.
  • requestLayout() — re-measures and re-draws (calls onMeasure then onDraw). Use when the view's size changes.

Custom Attributes (attrs.xml)

Declare custom XML attributes so the view can be configured from layout files:

<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CircleProgressView">
        <attr name="progressColor" format="color" />
        <attr name="trackColor"    format="color" />
        <attr name="strokeWidth"   format="dimension" />
        <attr name="progress"      format="float" />
    </declare-styleable>
</resources>

Reading Custom Attributes

Read attrs in the constructor using obtainStyledAttributes:

init {
    context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView).use { ta ->
        progressPaint.color = ta.getColor(
            R.styleable.CircleProgressView_progressColor,
            Color.parseColor("#6750A4")
        )
        backgroundPaint.color = ta.getColor(
            R.styleable.CircleProgressView_trackColor,
            Color.LTGRAY
        )
        val sw = ta.getDimension(R.styleable.CircleProgressView_strokeWidth, 20f)
        progressPaint.strokeWidth = sw
        backgroundPaint.strokeWidth = sw
        progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f)
    }
}

Using the Custom View in XML

Use your custom view in layout XML with the full class path and custom attributes:

<!-- In any layout XML -->
<com.example.myapp.CircleProgressView
    android:id="@+id/progressView"
    android:layout_width="120dp"
    android:layout_height="120dp"
    app:progress="65"
    app:progressColor="@color/primary"
    app:trackColor="@color/surface_variant"
    app:strokeWidth="12dp" />

<!-- Control in code: -->
binding.progressView.progress = 75f

Compound Views

A compound view combines existing views into one reusable component. Extend a layout class like LinearLayout:

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

    private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)

    init {
        orientation = HORIZONTAL
        binding.ivSearch.setOnClickListener {
            onSearchClick?.invoke(binding.etSearch.text.toString())
        }
    }

    var onSearchClick: ((String) -> Unit)? = null

    fun setHint(hint: String) { binding.etSearch.hint = hint }
}

Drawing Text

Draw text on a Canvas with drawText(). Center it precisely using Paint.getFontMetrics():

private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.BLACK
    textSize = 48f
    textAlign = Paint.Align.CENTER
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val cx = width / 2f
    val cy = height / 2f

    // Vertically center the text:
    val fm = textPaint.fontMetrics
    val textY = cy - (fm.ascent + fm.descent) / 2f
    canvas.drawText("${progress.toInt()}%", cx, textY, textPaint)
}

Quick Check

Which method should you call to trigger a redraw of your custom view when the data changes but the size stays the same?

Recap: Custom Views

You can now draw anything on screen:

  • Extend View with @JvmOverloads constructor
  • onMeasure() — report desired size with setMeasuredDimension()
  • Paint — define style; create once, not inside onDraw()
  • onDraw(canvas) — draw circles, arcs, text, bitmaps
  • invalidate() to redraw, requestLayout() to re-measure
  • Custom attributes via attrs.xml for XML configuration
  • Compound view — extend a layout to combine existing views

Next: bring your UI to life with Animations & Transitions.

Frequently asked questions

Is the “Custom Views” lesson free?

Yes — the full text of “Custom Views” is free to read here on the web, and the Android Academy course includes 5 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “Custom Views”?

Draw custom UI components from scratch. Extend View, implement onMeasure and onDraw with Canvas and Paint, define custom XML attributes, and build compound views. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 5, so you can start here or from the beginning and move at your own pace.

How long does the “Custom Views” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Material Design Basics
  2. Themes & Styles
  3. Custom Views
  4. Animations & Transitions
  5. Bottom Navigation & Tabs
← Back to Android Academy