Mastering onDraw: Custom Views with Canvas and Paint in Kotlin XML Android

In Android development, custom views are a powerful way to create unique UI components that go beyond the standard widgets. By overriding the onDraw method, you can take full control over the rendering of your view using the Canvas and Paint classes. This article explores how to override onDraw in a custom view, focusing on Kotlin XML development to build distinctive UI elements for Android.

Understanding Custom Views

A custom view is a UI component that you create from scratch or by extending existing Android views. Custom views are useful when you need to implement unique designs or behaviors not available in the standard Android UI toolkit. The core of creating custom views lies in handling how the view is drawn on the screen, primarily through the onDraw method.

Why Override onDraw?

  • Custom Graphics: Draw custom shapes, images, and text.
  • Animation: Implement animations by changing drawing parameters.
  • Data Visualization: Display data in a custom graphical format.
  • Control: Gain fine-grained control over the view’s appearance and behavior.

How to Override onDraw in Kotlin

To create a custom view with onDraw in Kotlin, follow these steps:

Step 1: Create a Custom View Class

Extend the View class (or any subclass like TextView or ImageView) and create your custom view:


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View

class CustomView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        paint.color = Color.BLUE
        paint.style = Paint.Style.FILL
    }

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

        // Draw a rectangle
        val left = 100f
        val top = 100f
        val right = 300f
        val bottom = 300f

        canvas.drawRect(left, top, right, bottom, paint)
    }
}

Explanation:

  • CustomView: A class extending View. It takes a Context and an optional AttributeSet as parameters for inflation from XML.
  • paint: An instance of Paint is created and configured with anti-aliasing and a fill style.
  • onDraw: Overrides the onDraw method to perform custom drawing operations. In this case, it draws a blue rectangle.

Step 2: Add the Custom View to Your Layout XML

Include the custom view in your XML layout file:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.customview.CustomView
        android:id="@+id/customView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Make sure to replace com.example.customview.CustomView with the actual package and class name of your custom view.

Step 3: Use the Custom View in Your Activity or Fragment

You can now reference and use the custom view in your Activity or Fragment:


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

When you run your app, the custom view will draw a blue rectangle on the screen.

Advanced Drawing with Canvas and Paint

The Canvas and Paint classes offer numerous methods for drawing shapes, text, and images. Here are a few examples:

Drawing Text


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

    paint.color = Color.BLACK
    paint.textSize = 40f
    canvas.drawText("Hello, Custom View!", 100f, 100f, paint)
}

Drawing a Circle


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

    paint.color = Color.GREEN
    canvas.drawCircle(300f, 300f, 100f, paint)
}

Drawing a Line


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

    paint.color = Color.RED
    paint.strokeWidth = 5f
    canvas.drawLine(100f, 100f, 500f, 500f, paint)
}

Handling Attributes from XML

To make your custom view more flexible, you can define custom attributes that can be set in the XML layout. Here’s how to do it:

Step 1: Define Custom Attributes

Create a res/values/attrs.xml file to define your custom attributes:


<resources>
    <declare-styleable name="CustomView">
        <attr name="rectColor" format="color"/>
        <attr name="textColor" format="color"/>
        <attr name="textSize" format="dimension"/>
    </declare-styleable>
</resources>

Step 2: Retrieve Attributes in Your Custom View

Modify your CustomView class to retrieve these attributes:


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View

class CustomView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var rectColor = Color.BLUE
    private var textColor = Color.BLACK
    private var textSize = 40f

    init {
        val typedArray = context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.CustomView,
            0,
            0
        )

        try {
            rectColor = typedArray.getColor(R.styleable.CustomView_rectColor, Color.BLUE)
            textColor = typedArray.getColor(R.styleable.CustomView_textColor, Color.BLACK)
            textSize = typedArray.getDimension(R.styleable.CustomView_textSize, 40f)
        } finally {
            typedArray.recycle()
        }

        paint.style = Paint.Style.FILL
    }

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

        // Draw a rectangle
        paint.color = rectColor
        val left = 100f
        val top = 100f
        val right = 300f
        val bottom = 300f
        canvas.drawRect(left, top, right, bottom, paint)

        // Draw text
        paint.color = textColor
        paint.textSize = textSize
        canvas.drawText("Hello, Custom View!", 100f, 400f, paint)
    }
}

Step 3: Use Attributes in Your Layout XML

Now you can use the custom attributes in your XML layout:


<com.example.customview.CustomView
    android:id="@+id/customView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:rectColor="#FF0000"
    app:textColor="#00FF00"
    app:textSize="60sp"/>

Example: Creating a Simple Graph View

Let’s create a simple graph view that displays a line graph based on provided data:


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View

class GraphView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val dataPoints = listOf(50f, 120f, 80f, 150f, 90f)

    init {
        paint.color = Color.BLACK
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 5f
    }

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

        val width = width.toFloat()
        val height = height.toFloat()
        val spacing = width / (dataPoints.size - 1)

        val path = Path()
        path.moveTo(0f, height - dataPoints[0])

        for (i in 1 until dataPoints.size) {
            val x = i * spacing
            val y = height - dataPoints[i]
            path.lineTo(x, y)
        }

        canvas.drawPath(path, paint)
    }
}

This GraphView draws a simple line graph using the provided dataPoints. The points are connected by lines, and the graph adapts to the view’s width and height.

Performance Considerations

  • Optimize Drawing Operations: Minimize the number of drawing calls and avoid creating new objects in onDraw if possible.
  • Use Hardware Acceleration: Ensure hardware acceleration is enabled in your app for better performance.
  • Invalidate Strategically: Call invalidate() only when the view’s appearance needs to change.

Conclusion

Overriding onDraw in a custom view allows you to create highly customized and unique UI components in Android. By using the Canvas and Paint classes effectively, you can implement complex graphics, animations, and data visualizations. Understanding how to handle custom attributes and optimize drawing operations ensures that your custom views are both flexible and performant. With Kotlin and XML development, building distinctive and engaging user interfaces has never been more accessible.