Mobile Development 31 min read

Android Custom View Implementation and Canvas Drawing Tutorial

This article introduces how to create custom Android Views by overriding onMeasure, onLayout, and onDraw, explains the three-stage view rendering process, and provides detailed examples of Canvas drawing methods—including drawing points, lines, shapes, text, images, and bitmap manipulation—along with code snippets and usage tips.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Android Custom View Implementation and Canvas Drawing Tutorial

The article explains the implementation of custom Views in Android, focusing on the Canvas drawing methods and the three main stages of view rendering: measure, layout, and draw. It emphasizes that developers typically override onMeasure(), onLayout(), and onDraw() to control the view's appearance.

It describes the Canvas as a drawing surface and outlines its three essential components: Canvas, the coordinate system, and Paint. A quick reference table lists common Canvas operations such as drawColor, drawRect, drawCircle, drawText, drawPath, and matrix transformations.

Drawing a single point

/**
 * Draw a single point using drawPoint
 */
class DrawPointView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPoint(
            0f + paddingStart, // x coordinate
            0f + paddingTop, // y coordinate
            getPaint() // Paint object
        )
    }
    private fun getPaint(): Paint {
        return Paint().apply {
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
            strokeJoin = Paint.Join.ROUND
            isAntiAlias = true
            textAlign = Paint.Align.CENTER
            textSize = resources.dpToPx(30)
            color = Color.GREEN
            strokeWidth = resources.dpToPx(16)
        }
    }
}

Drawing multiple points

/**
 * Draw multiple points using drawPoints
 */
class DrawPointsView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPoints(
            listOf(
                0f + paddingStart, 0f + paddingTop,
                (width - paddingEnd).toFloat(), 0f + paddingTop,
                (width - paddingEnd) / 2f, (height - paddingBottom) / 2f,
                0f + paddingStart, (height - paddingBottom).toFloat(),
                (width - paddingEnd).toFloat(), (height - paddingBottom).toFloat()
            ).toFloatArray(),
            projectResources.paintPoint
        )
    }
}

Drawing lines and multiple lines

// Single line
class DrawLineView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawLine(
            0f + paddingStart, 0f + paddingTop,
            (width - paddingEnd).toFloat(), (height - paddingBottom).toFloat(),
            projectResources.paint
        )
    }
}

// Multiple lines
class DrawLinesView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawLines(
            listOf(
                0f + paddingStart, 0f + paddingTop, width / 2f, (height - paddingBottom).toFloat(),
                width / 2f, (height - paddingBottom).toFloat(), (width - paddingEnd).toFloat(), 0f + paddingStart
            ).toFloatArray(),
            projectResources.paint
        )
    }
}

Drawing shapes

// Circle
class DrawCircleView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawCircle(
            width / 2f,
            height / 2f,
            height / 2f - paddingTop,
            projectResources.paint
        )
    }
}

// Rectangle
class DrawRectView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val rect by lazy {
        Rect(
            0 + paddingStart,
            0 + paddingTop,
            width - paddingEnd,
            height - paddingBottom
        )
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawRect(rect, projectResources.paint)
    }
}

// Rounded rectangle
class DrawRoundRectView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val rect by lazy {
        RectF(
            0f + paddingStart,
            0f + paddingTop,
            (width - paddingEnd).toFloat(),
            (height - paddingBottom).toFloat()
        )
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawRoundRect(rect, resources.dpToPx(10), resources.dpToPx(10), projectResources.paint)
    }
}

// Oval
class DrawOvalView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val rect by lazy {
        RectF(
            0f + paddingStart,
            0f + paddingTop,
            width.toFloat() - paddingEnd,
            height.toFloat() - paddingBottom
        )
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawOval(rect, projectResources.paint)
    }
}

// Arc (sector)
class DrawArcView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val rect by lazy {
        RectF(0f + paddingStart, 0f + paddingTop, height.toFloat() - paddingBottom, height.toFloat() - paddingBottom)
    }
    private val rect2 by lazy {
        RectF(0f + paddingStart, 0f + paddingTop, width.toFloat() - paddingEnd, height.toFloat() - paddingBottom)
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val shader = LinearGradient(
            0f, 0f, 100f, 100f,
            intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.LTGRAY),
            null,
            Shader.TileMode.REPEAT
        )
        projectResources.paint.shader = shader
        if (width == 0 || height == 0) return
        canvas.drawArc(rect, 0f, 330f, true, projectResources.paint)
        canvas.drawArc(rect2, 0f, 30f, true, projectResources.paint)
    }
}

Text drawing

// Simple text
class DrawTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    companion object {
        private const val TEXT = "三国演义-西游记-"
    }
    private val textBound by lazy {
        Rect().also { projectResources.paint.getTextBounds(TEXT, 0, TEXT.length, it) }
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawText(
            TEXT,
            width / 2f,
            height / 2f - textBound.exactCenterY(),
            projectResources.paint
        )
    }
}

// Wrapped text using StaticLayout
class DrawStaticLayoutTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    companion object {
        private const val TEXT = "Too long a line to display in one line, and let it auto wrap go to next line.\nCan handle \"\\n\" as well."
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        val staticLayout = StaticLayout.Builder.obtain(TEXT, 0, TEXT.length, projectResources.textPaint, width).build()
        canvas.save()
        canvas.translate(width / 2f, (height - staticLayout.height) / 2f)
        staticLayout.draw(canvas)
        canvas.restore()
    }
}

// Position‑specific text
class DrawPosTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    companion object { private const val TEXT = "Testing 123" }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPosText(
            TEXT.toCharArray(),
            1,
            3,
            floatArrayOf(
                width / 1.5f, height / 1.5f,
                width / 2f, height / 2f,
                width / 3f, height / 3f
            ),
            projectResources.paint
        )
    }
}

// Text on a path
class DrawTextOnPathView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    companion object { private const val TEXT = "Test 123" }
    private val path by lazy {
        Path().apply {
            moveTo(0f + paddingStart, 0f + paddingTop)
            lineTo(width / 2f, height - paddingBottom.toFloat())
            lineTo(width - paddingEnd.toFloat(), 0f + paddingTop)
        }
    }
    private val textBound by lazy {
        Rect().also { projectResources.paint.getTextBounds(TEXT, 0, TEXT.length, it) }
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawPath(path, projectResources.paintLight)
        canvas.drawTextOnPath(
            TEXT,
            path,
            0f,
            -textBound.exactCenterY(),
            projectResources.paint
        )
    }
}

Bitmap drawing and color filling

// Draw bitmap directly
class DrawBitmapView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val bitmap by lazy { BitmapFactory.decodeResource(resources, R.drawable.image) }
    private val rect by lazy { Rect(0 + paddingLeft, 0 + paddingTop, width - paddingRight, height - paddingBottom) }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawBitmap(bitmap, null, rect, null)
    }
}

// Fill canvas with a single color
class DrawColorView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawColor(context.getColor(R.color.colorPrimary))
    }
}

// Fill canvas with RGB values
class DrawRGBView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawRGB(255, 0, 0)
    }
}

// Fill canvas with ARGB (transparent overlay)
class DrawARGBView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val bitmap by lazy { BitmapFactory.decodeResource(resources, R.drawable.image) }
    private val rect by lazy { Rect(0 + paddingLeft, 0 + paddingTop, width - paddingRight, height - paddingBottom) }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawBitmap(bitmap, null, rect, null)
        canvas.drawARGB(128, 255, 0, 0)
    }
}

Advanced bitmap manipulation

// Bitmap mesh deformation
class DrawBitmapMeshView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val bitmap by lazy { BitmapFactory.decodeResource(resources, R.drawable.santou) }
    private val firstX by lazy { 0f + paddingLeft }
    private val firstY by lazy { 0f + paddingTop }
    private val secondX by lazy { width / 5f }
    private val secondY by lazy { height / 3f }
    private val thirdX by lazy { width.toFloat() - paddingRight }
    private val thirdY by lazy { height.toFloat() - paddingBottom }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width == 0 || height == 0) return
        canvas.drawBitmapMesh(
            bitmap, 2, 2,
            floatArrayOf(
                firstX, firstY, secondX, firstY, thirdX, firstY,
                firstX, secondY, secondX, secondY, thirdX, secondY,
                firstX, thirdY, secondX, thirdY, thirdX, thirdY
            ),
            0,
            null,
            0,
            null
        )
    }
}

The article also discusses common pitfalls when handling large images, such as OutOfMemory errors, and provides utility methods for image compression, saving, and cache cleanup in Java/Kotlin. It concludes with a custom RoundImageView implementation that extends ImageView to display circular images with optional borders and fill colors.

graphicsAndroidcanvasKotlinCustom View
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.