Creating A Custom View In Android From Scratch
A custom Android View gives you direct control over pixels, touch input and drawing behaviour. Instead of assembling only standard widgets, you can create a reusable component for a speedometer, signature pad, timetable, map marker, chart or branded control that behaves exactly as your app requires.
The process is approachable when you separate it into clear stages: define the view’s purpose, prepare its state, measure its space, draw its appearance and respond to interaction. Kotlin and Android Studio provide the foundation, while careful testing ensures the result feels natural on the wide range of phones used across Australia.
Why Build A Custom View
A custom View is useful when existing components cannot express a visual idea cleanly. A retail app might need a product-rating dial, a fitness app may display a circular hydration goal, and a public transport app could show the progress of a train journey. These designs often need custom geometry rather than a collection of nested layouts.
Creating the component yourself can also improve performance. One drawing surface may replace a complicated hierarchy of ImageView, TextView and layout objects. That can reduce unnecessary measurement work and make animation smoother, especially on affordable Android devices commonly sold through Australian retailers.
Start with a small responsibility. A view that draws one progress ring and reports one tap is easier to maintain than a single component that handles networking, database updates and navigation. Keep data and business rules in a ViewModel or other suitable layer, then pass the custom view only the values it needs to render.
For visual direction, browse UI inspiration before writing code, then translate the chosen idea into measurable shapes, colours and interaction states rather than copying an entire interface.
Set Up The View Class
Create a Kotlin class that extends View and accepts a Context. The two-argument constructor is useful when the view is inflated from XML, while the three-argument version receives style information. In a production project, an @JvmOverloads constructor can make these combinations convenient.
class ProgressDial @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var progress = 0.65f
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 24f
strokeCap = Paint.Cap.ROUND
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Drawing code goes here
}
}
Keep Paint objects as properties instead of creating them during every redraw. Configure anti-aliasing, stroke width, colour and text alignment once where possible. If colours or dimensions should be configurable, expose them through custom XML attributes or resource files rather than scattering hard-coded values through the class.
Use invalidate() whenever a visual property changes. Android will schedule another call to onDraw(). Avoid calling invalidate() continuously unless an animation is active, because repeated redraws consume battery and processor time.
Measure And Draw Precisely
The onDraw() method receives a Canvas, which provides operations such as drawCircle, drawArc, drawLine, drawPath and drawText. The view’s usable centre is usually calculated from its width and height, while a radius is chosen from the smaller dimension so the shape remains square on different screens.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centreX = width / 2f
val centreY = height / 2f
val radius = minOf(centreX, centreY) - paint.strokeWidth
paint.color = Color.LTGRAY
canvas.drawCircle(centreX, centreY, radius, paint)
paint.color = Color.rgb(0, 121, 107)
canvas.drawArc(
centreX - radius,
centreY - radius,
centreX + radius,
centreY + radius,
-90f,
progress * 360f,
false,
paint
)
}
For a polished component, account for padding and stroke width. A thick line drawn directly against the edge can be clipped, particularly when the view is displayed in a compact toolbar. Use width - paddingLeft - paddingRight and the equivalent height calculation when the content must respect XML padding.
Measurement becomes important when the parent layout gives the view flexible or restricted space. Override onMeasure() when your component has a preferred size or must remain square. Use MeasureSpec.getMode() and MeasureSpec.getSize() to honour the parent’s constraints, then call setMeasuredDimension() with the final width and height.
Add Touch And Accessibility
Touch interaction belongs in onTouchEvent(). Track the initial position on ACTION_DOWN, update state during ACTION_MOVE, and finish the gesture on ACTION_UP or ACTION_CANCEL. Return true when the view has accepted the gesture; otherwise Android may pass the event to another component.
A slider-like control can convert the finger’s horizontal position into a value between zero and one:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE,
MotionEvent.ACTION_UP -> {
progress = (event.x / width).coerceIn(0f, 1f)
invalidate()
performClick()
return true
}
}
return super.onTouchEvent(event)
}
override fun performClick(): Boolean {
super.performClick()
return true
}
Do not rely on colour alone to communicate status. Provide a content description, a meaningful accessibility class or a custom accessibility delegate where appropriate. Ensure the touch target is large enough for comfortable use. This matters for commuters checking an app one-handed on a Sydney train, as well as people using Android accessibility services.
Australian developers should also consider the Disability Discrimination Act 1992 and the accessibility expectations of government, education and enterprise clients. A visually impressive custom control still needs readable labels, suitable contrast, predictable focus behaviour and an alternative way to perform its action.
Test Across Real Devices
A custom component can look correct in Android Studio’s preview and still fail on a physical phone. Test portrait and landscape layouts, font scaling, dark mode, right-to-left settings where relevant, and different density buckets. Include a low-cost handset as well as a current flagship to reveal performance or clipping problems.
When testing an app intended for Australia, check common local conditions: bright outdoor light in Brisbane or Perth, one-handed use during Melbourne tram trips, and intermittent connectivity during regional travel. If the component displays money, use Australian dollar formatting and avoid assuming that a decimal point or date format will match every locale setting.
A simple review list keeps testing focused:
Visual Checks Before Release
- Confirm shapes stay inside the view’s bounds.
- Test small, large and accessibility font scales.
- Compare light and dark colour contrast.
- Rotate the device during an active interaction.
Automated tests can verify that a progress value is clamped, a tap changes state and an invalid size does not produce a crash. Screenshot tests are helpful for stable visual components, while Espresso or Compose interoperability tests can check how the view behaves inside a larger screen.
Practical Engineering Checks
- Reuse
Paint,Pathand geometry objects where possible. - Confirm
ACTION_CANCELleaves the component in a safe state. - Check TalkBack labels and keyboard focus.
- Measure redraw and animation performance.
If the app collects user-entered information through the custom view, document why it is needed and store only what is necessary. The Australian Privacy Act 1988 and the Australian Privacy Principles may apply depending on the organisation and activity, particularly when a custom drawing surface captures signatures, health details or identity information.
A strong custom View begins with a narrow purpose and grows through measured refinement. Build the smallest working version, inspect it on real Australian devices, then add animation, styling and configuration only when each feature improves the user experience. Place the class in a reusable module, document its public properties and connect it to the rest of the app through clear callbacks or state flows. With that foundation in place, you can turn a rough canvas experiment into a dependable Android component ready for release.