Android SharedPreferences Tutorial and Example

Building a custom Android view with Canvas and Paint

A custom Android view gives you direct control over pixels, shapes, colours, text, and touch interaction. Instead of assembling a screen from standard widgets, you can create a drawing surface for a chart, game board, signature pad, gauge, map marker, or animated loading indicator.

The main tools are Canvas, which provides drawing methods, and Paint, which controls how those methods look. This approach is useful when a standard TextView, ImageView, or layout cannot deliver the exact visual result you need. The examples below use Kotlin, although the same Android APIs are available in Java.

How Canvas and Paint work together

A View receives a Canvas when Android calls its onDraw() method. The canvas represents the area where your view can render content. It includes methods such as drawCircle(), drawRect(), drawLine(), drawText(), and drawPath().

Paint stores drawing properties. You can set its colour, stroke width, text size, style, typeface, anti-aliasing, and shader. A single custom view may use several paint objects, such as one for a filled background, one for outlines, and another for labels.

Create a basic view with a constructor and an overridden drawing method:

class TemperatureView(context: Context) : View(context) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

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

        paint.color = Color.rgb(30, 136, 229)
        paint.style = Paint.Style.FILL

        canvas.drawCircle(
            width / 2f,
            height / 2f,
            100f,
            paint
        )
    }
}

The ANTI_ALIAS_FLAG smooths the edges of circles, paths, and diagonal lines. Without it, curved shapes can appear jagged, especially on smaller screens.

Adding the custom view to a layout

You can add a custom view directly to an XML layout. Give it a fully qualified class name so Android can inflate it when the activity starts:

<com.example.weatherapp.TemperatureView
    android:id="@+id/temperatureView"
    android:layout_width="match_parent"
    android:layout_height="220dp" />

You can also create the view programmatically. This is helpful when a screen generates several drawing surfaces or when the view is part of a reusable component:

val temperatureView = TemperatureView(this)
setContentView(temperatureView)

Custom drawing uses the size supplied by the parent layout. Avoid assuming that a view is always 1080 pixels wide or that density is one. A phone in Sydney may have a very different resolution and pixel density from a tablet used in Perth, so calculate positions from width, height, and display-independent units.

Drawing shapes, text, and paths

Canvas coordinates begin at the top-left corner. The x-coordinate increases towards the right, while the y-coordinate increases downwards. This differs from a mathematical graph, where positive y usually points upwards.

A simple dashboard can combine a background rectangle, a progress arc, and a text label:

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

    paint.color = Color.WHITE
    paint.style = Paint.Style.FILL
    canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)

    paint.color = Color.rgb(0, 121, 107)
    paint.style = Paint.Style.STROKE
    paint.strokeWidth = 18f
    paint.strokeCap = Paint.Cap.ROUND

    val bounds = RectF(40f, 30f, width - 40f, height - 30f)
    canvas.drawArc(bounds, 180f, 120f, false, paint)

    paint.style = Paint.Style.FILL
    paint.textSize = 42f
    canvas.drawText("72%", 100f, 150f, paint)
}

For freeform graphics, use Path. A path can describe a line chart, a mountain silhouette, or a custom icon. Call moveTo() to set the starting point and lineTo() to add segments. Use close() when you want to join the final point back to the first.

Text positioning requires care. drawText() uses the baseline rather than the top of the letters. For accurate centring, inspect Paint.FontMetrics and adjust the y-coordinate. This is important for labels such as tram stops on a Melbourne transport diagram, where inconsistent vertical alignment can make a compact visualisation difficult to read.

Managing density, size, and layout changes

Canvas values are measured in pixels. Android layouts, however, commonly use density-independent pixels, or dp. Convert dimensions before drawing:

private fun dp(value: Float): Float {
    return value * resources.displayMetrics.density
}

A 16dp margin therefore becomes an appropriate number of physical pixels on different devices. Use sp-based calculations for text when the design should respect the user’s font scale settings.

Do not calculate geometry only once in the constructor. The device may rotate, enter split-screen mode, or resize for a foldable display. Use width and height inside onDraw(), or update cached measurements in onSizeChanged():

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Recalculate bounds that depend on the view size here.
}

This makes the same component adaptable to a small Android handset in Adelaide and a larger tablet used for a field-service application in regional Queensland.

Handling touch input

A drawing view becomes interactive when it overrides onTouchEvent(). The event includes the action type and the pointer coordinates. Returning true tells Android that the view wants to continue receiving the gesture.

override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            parent.requestDisallowInterceptTouchEvent(true)
            return true
        }

        MotionEvent.ACTION_MOVE -> {
            invalidate()
            return true
        }

        MotionEvent.ACTION_UP,
        MotionEvent.ACTION_CANCEL -> {
            parent.requestDisallowInterceptTouchEvent(false)
            return true
        }
    }

    return super.onTouchEvent(event)
}

For a signature pad, store touch points in a Path and draw that path during onDraw(). For a custom slider or map, convert the x-coordinate into a value, clamp it to the valid range, update the state, and call invalidate().

Touch targets should be large enough for comfortable use. Australian apps often need to work outdoors, in bright Queensland sunlight, or while someone is using a phone one-handed on a Sydney train. Clear contrast, generous hit areas, and visible pressed states make the interaction more reliable.

Improving performance and accessibility

Android may call onDraw() frequently, particularly during animation or scrolling. Keep drawing operations lightweight. Create Paint, Path, and reusable geometry objects once instead of allocating them on every frame. Avoid decoding bitmaps, querying databases, or performing network work inside onDraw().

Call invalidate() only when the visual state changes. If the view animates continuously, use ValueAnimator or postInvalidateOnAnimation() so updates align with the display refresh cycle. A complex custom chart should also limit unnecessary shadows, bitmap operations, and layered transparency.

A visual canvas has no automatic accessibility semantics. Add a meaningful content description for simple graphics, or expose virtual accessible elements for complex charts and controls. Important information should not rely on colour alone. For example, a bushfire risk indicator should pair colour with text or symbols so users can distinguish states under different lighting and vision conditions.

Practical recommendations for a reliable custom view

A custom view is easier to maintain when its drawing code, state, and interaction rules remain clearly separated. Store values such as progress, selected item, or current path in properties, then let onDraw() render the current state. This structure makes the view easier to test and reuse across projects.

Before releasing an app, test the component on several screen sizes, font scales, orientations, and Android versions. A graphic that looks balanced on a developer’s device may clip text or place controls too close to the edge elsewhere. The following practices provide a useful baseline:

  • Use density-aware measurements rather than hard-coded pixel assumptions.
  • Reuse Paint, Path, and RectF objects where possible.
  • Keep onDraw() focused on rendering, not business logic.
  • Call invalidate() after changing a property that affects the display.
  • Provide text, contrast, and content descriptions for accessibility.
  • Test touch behaviour with different pointer sizes and screen orientations.
  • Use Android Studio’s profiling tools when animation becomes slow.

Building a reusable drawing component

A good custom view should expose simple properties instead of forcing each activity to manipulate its internal drawing logic. For example, a progress component might provide setProgress(value), while a chart might accept a list of data points and a selected index. Validate incoming values and redraw only after the state has changed.

You can also define custom XML attributes for colours, stroke widths, and labels. This allows the same view to be configured in multiple layouts without duplicating Kotlin code. Keep default values sensible, especially when the view is previewed in Android Studio’s layout editor.

Canvas and Paint provide a foundation for highly tailored Android interfaces. With responsive measurements, efficient rendering, touch support, and accessible labels, a small drawing class can become a dependable component for dashboards, games, diagrams, signatures, and data visualisations.