Custom View in Android — essence, drawing and event handling

Author: IT Sectr Published: 2026-07-20 Reading time: 9 min

Custom View is a custom interface component in Android, created by inheriting from the View class or its subclasses (Button, TextView, ImageView) and overriding key methods: onDraw for drawing, onMeasure for measuring sizes and onTouchEvent for handling touches. According to the Google Android Developer Guide (2024), Custom View is used when the standard Android SDK components do not provide the required behavior or appearance — for example, custom animation, non-standard shapes, specialized charts and game elements.

Key Takeaways

  • Custom View — created by inheriting from View or its subclasses with overriding onDraw, onMeasure, onTouchEvent.
  • Drawing is performed via Canvas API — geometric shapes, text, paths, Bitmap and animation.
  • onMeasure handles correct View sizes considering layout parameters and padding.
  • Custom XML attributes are declared in res/values/attrs.xml and read in the View constructor.
  • Touch handling via onTouchEvent allows adding gesture support, drag-and-drop and multi-touch.

What is Custom View in Android?

Custom View is a class that inherits android.view.View (or one of its subclasses) and overrides system methods to implement custom drawing, measurement and event handling logic. Custom View is a fundamental Android mechanism for creating unique UI components.

Android provides two approaches to creating Custom View: inheriting from View (fully custom drawing via onDraw) and inheriting from an existing View subclass (e.g., overriding Button or TextView to add functionality while preserving base behavior).

Basic constructor of a Custom View must accept Context and AttributeSet — this allows the system to create the View from XML markup. If the View will only be used from code, a constructor with Context is sufficient. A third constructor with style is needed for Android theme support.

kotlin
class CustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        strokeWidth = 4f
        style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(width / 2f,
            height / 2f,
            50f, paint)
    }
}

Minimal Custom View consists of a constructor and onDraw. However, for correct work in the Android Layout system, onMeasure must also be overridden — otherwise the View may display with zero sizes or incorrectly respond to wrap_content layout parameters.

According to Google I/O 2023, custom Views are used in 65% of the top 100 Google Play apps. The most common reasons for creating a Custom View: custom animations (35%), custom charts and diagrams (25%), specialized control elements (20%) and branded components (20%).

onMeasure and onLayout: Size Management

onMeasure is the method where the View reports its desired dimensions based on the passed MeasureSpec (parent constraints). Without a proper onMeasure implementation, the View may have zero height with wrap_content or occupy the entire screen area with match_parent.

MeasureSpec consists of a mode (EXACTLY, AT_MOST, UNSPECIFIED) and a value. EXACTLY — the parent set an exact size (match_parent or fixed value). AT_MOST — the parent set a maximum (wrap_content). UNSPECIFIED — no restrictions (ScrollView, ListView).

kotlin
override fun onMeasure(
    widthMeasureSpec: Int,
    heightMeasureSpec: Int
) {
    val desiredWidth = paddingLeft + paddingRight
        + DEFAULT_WIDTH
    val desiredHeight = paddingTop + paddingBottom
        + DEFAULT_HEIGHT

    val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
    val modeWidth = MeasureSpec.getMode(widthMeasureSpec)

    val resultWidth = when (modeWidth) {
        MeasureSpec.EXACTLY -> measuredWidth
        MeasureSpec.AT_MOST ->
            desiredWidth.coerceAtMost(measuredWidth)
        else -> desiredWidth
    }

    val resultHeight = resolveSize(desiredHeight,
        heightMeasureSpec)

    setMeasuredDimension(resultWidth, resultHeight)
}

resolveSize is an Android utility method that simplifies onMeasure implementation. It takes the desired size and MeasureSpec and returns the correct value: for EXACTLY — the exact size, for AT_MOST — the minimum of desired and maximum, for UNSPECIFIED — the desired size.

onLayout for View (not ViewGroup) is typically not overridden — it is called by the parent for positioning child elements. For ViewGroup, onLayout is mandatory — you need to call layout() for each child View.

onDraw and Canvas API: Drawing

onDraw is the heart of Custom View. This method is called by Android on every View redraw. A Canvas object is passed to it, through which all drawing is performed: geometric shapes, text, paths, Bitmap and animations.

Canvas API provides methods for all basic operations: drawCircle, drawRect, drawLine, drawPath, drawText, drawBitmap, drawArc, drawOval. To configure the style, Paint is used — an object that defines color, thickness, fill style, font, shadows and effects.

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

    // Background
    canvas.drawRect(0f, 0f,
        width.toFloat(), height.toFloat(), bgPaint)

    // Text
    canvas.drawText("Custom View",
        paddingLeft.toFloat(),
        paddingTop.toFloat() + textPaint.textSize,
        textPaint)

    // Circle
    canvas.drawCircle(
        width / 2f,
        height / 2f,
        radius, circlePaint)

    // Path (Bezier curve)
    val path = Path().apply {
        moveTo(0f, height.toFloat())
        quadTo(width / 2f, 0f,
            width.toFloat(), height.toFloat())
    }
    canvas.drawPath(path, pathPaint)
}

Paint is the drawing style configurator. Anti-aliasing (Paint.ANTI_ALIAS_FLAG) is mandatory for smooth edges. Subpixel text (SUBPIXEL_TEXT_FLAG) improves text quality. Style.FILL, STROKE and FILL_AND_STROKE determine whether the shape will be filled, outlined, or both.

Important: do not create Paint, Path, Rect objects in onDraw — this leads to allocations in the drawing loop and triggers garbage collection, which reduces FPS. Create all drawing objects in the constructor or in the init method. For animation, use ValueAnimator or ObjectAnimator with invalidate() to trigger redrawing.

Custom XML Attributes in attrs.xml

Custom attributes allow configuring a Custom View from XML markup just like standard android:layout_width or android:background. Attributes are declared in the res/values/attrs.xml file with name, type and optional default value.

Attribute types: string, integer, float, boolean, color, dimension, enum, flag, fraction, reference (resource reference). For each type, Android automatically parses the value from XML and passes it to TypedArray.

xml
<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomView">
        <attr name="circleColor"
            format="color" />
        <attr name="circleRadius"
            format="dimension" />
        <attr name="labelText"
            format="string" />
        <attr name="showAnimation"
            format="boolean" />
    </declare-styleable>
</resources>

In the Custom View constructor, attributes are read via context.obtainStyledAttributes, which returns a TypedArray. TypedArray provides typed access methods: getColor, getDimension, getString, getBoolean, getInt. After reading, recycle() must be called on the TypedArray to release resources.

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

    circleColor = typedArray.getColor(
        R.styleable.CustomView_circleColor,
        Color.BLUE
    )
    circleRadius = typedArray.getDimension(
        R.styleable.CustomView_circleRadius,
        50f
    )
    labelText = typedArray.getString(
        R.styleable.CustomView_labelText
    ) ?: "Default"

    typedArray.recycle()
}

Usage in XML: add the app namespace (xmlns:app="http://schemas.android.com/apk/res-auto") and use custom attributes as app:circleColor="@color/red". Android Studio will autocomplete and validate attribute types if the declaration in attrs.xml is correct.

Touch Handling with onTouchEvent

onTouchEvent is the method called on each touch of the View. It receives a MotionEvent object with information about the event type (ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL), coordinates, pressure and number of fingers (multi-touch).

For handling complex gestures (swipe, pinch, long press), use GestureDetector or ScaleGestureDetector in combination with onTouchEvent. GestureDetector simplifies recognition of onSingleTapUp, onFling, onLongPress, onDoubleTap and other standard gestures.

kotlin
private val gestureDetector = GestureDetector(
    context, object : GestureDetector.SimpleOnGestureListener() {
    override fun onSingleTapUp(e: MotionEvent): Boolean {
        handleTap(e.x, e.y)
        return true
    }
})

override fun onTouchEvent(event: MotionEvent): Boolean {
    val handled = gestureDetector.onTouchEvent(event)

    when (event.action) {
        MotionEvent.ACTION_MOVE -> {
            currentX = event.x
            currentY = event.y
            invalidate()
            return true
        }
    }

    return handled || super.onTouchEvent(event)
}

Hit-testing — determining whether a touch landed in a specific area of the View. For rectangular areas, use Rect.contains(x, y). For circles — checking distance from the center: sqrt(dx^2 + dy^2) < radius. For arbitrary shapes — Path.op() or Region.contains().

Multi-touch is handled via MotionEvent.getPointerCount() and getPointerId(index). Each finger gets a unique ID that is preserved from ACTION_DOWN to ACTION_POINTER_UP. ACTION_MOVE can contain data for all active fingers — use getHistoricalX/Y for movement interpolation.

Custom View Performance Optimization

Custom View can become a performance bottleneck if onDraw is called frequently (animation, scrolling) or contains heavy operations. Android provides several mechanisms for optimizing custom component rendering.

Layer type for complex drawing

View.setLayerType allows switching View rendering to a software (LAYER_TYPE_SOFTWARE) or hardware (LAYER_TYPE_HARDWARE) layer. Software layer is useful for complex Canvas graphics that are not supported by hardware acceleration — for example, drawTextOnPath or complex Path effects.

Invalidate only the needed area

invalidate(Rect) redraws only the specified area of the View, not the entire component. This is critically important for large Custom Views (graphics, maps, drawing canvases), where full redraw of each frame causes FPS drops. Use postInvalidateOnAnimation() for vsync synchronization.

kotlin
class EfficientCustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    // Objects created once in constructor
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val rect = Rect()
    private val position = PointF()

    fun updatePosition(x: Float, y: Float) {
        position.set(x, y)
        // Redraw only area around the point
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

Hardware acceleration is enabled by default on Android 3.0+ (API 11+). Canvas operations drawCircle, drawRect, drawBitmap are hardware accelerated and run on the GPU. However, drawTextOnPath, drawVertices, complex clipping operations are not accelerated — use LAYER_TYPE_SOFTWARE for them.

According to Android Performance Patterns (Google, 2023), the main causes of Custom View lag — allocations in onDraw (creating objects each frame), redrawing the entire View when one element changes and the absence of LayerType.HARDWARE for static content. Fixing these three issues gives an FPS boost from 30 to 60 in most scenarios.

MethodPurposeCall Frequency
onDrawDrawing View contentOn each invalidate()
onMeasureDetermining View sizesOn layout change
onLayoutPositioning child ViewsAfter onMeasure
onTouchEventTouch handlingOn each touch
onSizeChangedReaction to size changeOn first layout and change

Frequently Asked Questions

When to use Custom View instead of Compose?

Custom View (Canvas API) is justified when maximum custom drawing performance is needed (graphics, video editors, maps), integration with existing View-based code or support for Android versions below API 21. Jetpack Compose is a modern approach for most new projects, using declarative UI and better suited for dynamic interfaces.

Is it mandatory to override onMeasure?

Yes, if the View uses wrap_content in XML layout. Without onMeasure, wrap_content will behave like match_parent because the default View.onMeasure implementation does not set a default size. If the View always has a fixed size or match_parent, onMeasure can be left unoverridden.

How to animate a Custom View?

Use ValueAnimator or ObjectAnimator to change View properties (color, radius, position) and call invalidate() in the animation callback for redrawing. ValueAnimator runs on the main thread and is synchronized with vsync. For complex animations (physics, particles), use Choreographer.FrameCallback or the Android Animation Framework.

How to handle multi-touch in Custom View?

In onTouchEvent, use MotionEvent.getPointerCount() to determine the number of fingers and getPointerId(i) to track each finger by unique ID. ACTION_POINTER_DOWN / ACTION_POINTER_UP — events for finger addition/removal. For pinch-to-zoom, use ScaleGestureDetector paired with onTouchEvent — it simplifies pinch detection.

Can Custom View be used in Jetpack Compose?

Yes, via AndroidView — a composable function that embeds a View-based element into the Compose hierarchy. AndroidView takes a factory for creating the View and an update callback for state synchronization. This allows reusing existing Custom Views in new Compose projects without full rewriting.

Summary

  • Custom View — a custom Android component created by inheriting from View with overridden onDraw, onMeasure and onTouchEvent for custom drawing and handling.
  • onDraw and Canvas API — the foundation of drawing: geometric shapes, text, Bitmap, paths — configured via Paint for color, style and font.
  • onMeasure is important for correct wrap_content behavior; uses MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) and resolveSize for dimension calculation.
  • Custom attributes via attrs.xml and TypedArray allow configuring Custom View from XML markup with Android Studio autocomplete support.
  • onTouchEvent with GestureDetector provides touch, gesture and multi-touch handling, while hit-testing determines the interaction area.
  • Performance: create objects once in the constructor, use invalidate(Rect) for partial redrawing and LayerType for complex effects.
  • Custom View integrates with Jetpack Compose via AndroidView, allowing reuse of ready-made components in new Compose projects.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also