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 (callsonDraw). Use when the content changes but the size stays the same.requestLayout()— re-measures and re-draws (callsonMeasurethenonDraw). 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 = 75fCompound 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
Viewwith@JvmOverloads constructor onMeasure()— report desired size withsetMeasuredDimension()Paint— define style; create once, not insideonDraw()onDraw(canvas)— draw circles, arcs, text, bitmapsinvalidate()to redraw,requestLayout()to re-measure- Custom attributes via
attrs.xmlfor 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.