Topic 26 of 28 · Android Native Developer

Topic 12 : Custom UI Components

Lesson TL;DRTopic 12: Custom UI Components 📖 8 min read · 🎯 advanced · 🧭 Prerequisites: collectionsinjavaqueue, collectionsinjavaset Why this matters Up until now, you've been building screens using the compon...
8 min read·advanced·android · custom-views · composite-views · canvas

Topic 12: Custom UI Components

📖 8 min read · 🎯 advanced · 🧭 Prerequisites: collections-in-java-queue, collections-in-java-set

Why this matters

Up until now, you've been building screens using the components Android gives you out of the box — buttons, text fields, image views. That gets you far, but at some point every real app needs something the standard toolkit simply doesn't have: a circular progress ring, a custom rating widget, a profile card styled exactly the way your designer wants it. When that moment comes, you have two choices — hack something together with workarounds, or learn to build your own UI components from scratch. Today we do the second one. By the end of this lesson, you'll know how to draw custom views pixel by pixel and how to combine existing views into reusable composite components you can drop anywhere in your app.

What You'll Learn

  • Distinguish between Custom Views and Composite Views and know when to use each
  • Build a custom view from scratch using Canvas and Paint in Java
  • Create a composite view by inflating an XML layout inside a ViewGroup subclass
  • Expose clean setter APIs on custom components so they integrate naturally into any Activity

The Analogy

Think of Android's built-in views as IKEA furniture — functional, predictable, and assembled by millions of people. A Custom View is like commissioning a craftsman to carve a table from raw wood: you control every curve, every finish, every joint. A Composite View is like hiring an interior designer to bolt three IKEA pieces together with a custom frame so they always travel as one unit. Both approaches produce reusable furniture, but the craftsman route gives you maximum creative control while the assembly route gives you maximum speed when the raw materials already exist.

Chapter 1: The Two Types of Custom UI Components

Android custom UI components fall into two categories, each with a distinct construction method:

  1. Custom Views — built from scratch (or by extending an existing View) and rendered manually using the Canvas API.
  2. Composite Views — built by combining multiple standard views inside a ViewGroup subclass, inflated from an XML layout file.

Choosing between them is straightforward:

NeedApproach
Unique visual rendering (charts, gauges, drawings)Custom View
Reusable grouping of existing widgetsComposite View
Pixel-perfect animation or game-like graphicsCustom View
Profile cards, list item templates, form rowsComposite View

Chapter 2: Creating a Custom View — CustomCircleView

A Custom View subclasses View (or any existing view) and overrides onDraw(Canvas) to paint itself. Three constructors are required so the view can be instantiated both from code and from XML layouts.

2.1 CustomCircleView.java

package com.example.customview;

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

public class CustomCircleView extends View {

    private Paint paint;
    private int circleColor = Color.RED;

    public CustomCircleView(Context context) {
        super(context);
        init();
    }

    public CustomCircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setColor(circleColor);
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width  = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width / 2, height / 2, radius, paint);
    }

    public void setCircleColor(int color) {
        circleColor = color;
        paint.setColor(circleColor);
        invalidate(); // schedules a redraw pass
    }
}

Key points:

  • Paint — holds all drawing style information (color, stroke, anti-aliasing).
  • setAntiAlias(true) — smooths the circle edge on high-density screens.
  • invalidate() — tells the rendering system the view is dirty and must be redrawn; always call it after mutating drawing state.
  • Math.min(width, height) / 2 — keeps the circle inscribed within whatever dimensions the layout assigns.

2.2 Using CustomCircleView in a Layout

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <com.example.customview.CustomCircleView
        android:id="@+id/custom_circle_view"
        android:layout_width="200dp"
        android:layout_height="200dp" />

    <Button
        android:id="@+id/change_color_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Change Color" />

</LinearLayout>

Use the fully qualified class name com.example.customview.CustomCircleView so the layout inflater can find and instantiate the class.

2.3 Wiring It Up in MainActivity.java

package com.example.customview;

import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private CustomCircleView customCircleView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        customCircleView = findViewById(R.id.custom_circle_view);
        Button changeColorButton = findViewById(R.id.change_color_button);

        changeColorButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                customCircleView.setCircleColor(Color.BLUE);
            }
        });
    }
}

Pressing the button calls setCircleColor(Color.BLUE), which updates the Paint object and schedules a redraw via invalidate() — the circle flips from red to blue without any Activity restart.

Chapter 3: Creating a Composite View — ProfileView

A Composite View inflates a normal XML layout inside a ViewGroup subclass (commonly LinearLayout, FrameLayout, or ConstraintLayout) and exposes typed setters. The host Activity never needs to know which child views exist inside.

3.1 profile_view.xml

<!-- profile_view.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp">

    <ImageView
        android:id="@+id/profile_image"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginEnd="16dp"
        android:src="@drawable/ic_profile" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/profile_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Name"
            android:textSize="18sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/profile_email"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Email"
            android:textSize="14sp" />

    </LinearLayout>

</LinearLayout>

3.2 ProfileView.java

package com.example.customview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

public class ProfileView extends LinearLayout {

    private ImageView profileImage;
    private TextView  profileName;
    private TextView  profileEmail;

    public ProfileView(Context context) {
        super(context);
        init(context);
    }

    public ProfileView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ProfileView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        LayoutInflater inflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.profile_view, this, true);

        profileImage = findViewById(R.id.profile_image);
        profileName  = findViewById(R.id.profile_name);
        profileEmail = findViewById(R.id.profile_email);
    }

    public void setProfileImage(int resId) {
        profileImage.setImageResource(resId);
    }

    public void setProfileName(String name) {
        profileName.setText(name);
    }

    public void setProfileEmail(String email) {
        profileEmail.setText(email);
    }
}

inflater.inflate(R.layout.profile_view, this, true) — the third argument true attaches the inflated view tree directly to this (ProfileView), making the composite self-contained.

3.3 Using ProfileView in a Layout

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <com.example.customview.ProfileView
        android:id="@+id/profile_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

3.4 Populating ProfileView from MainActivity.java

package com.example.customview;

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

public class MainActivity extends AppCompatActivity {

    private ProfileView profileView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        profileView = findViewById(R.id.profile_view);
        profileView.setProfileImage(R.drawable.ic_profile);
        profileView.setProfileName("John Doe");
        profileView.setProfileEmail("john.doe@example.com");
    }
}

The Activity only calls three high-level setters — it has zero knowledge of the internal ImageView or TextView IDs. This encapsulation is the core benefit of the Composite View pattern.

Chapter 4: How the View Rendering Pipeline Works

Understanding what happens under the hood helps you avoid common mistakes like calling invalidate() from a background thread or forgetting super.onDraw().

flowchart TD
    A[View created / XML inflated] --> B[measure pass\nonMeasure]
    B --> C[layout pass\nonLayout]
    C --> D[draw pass\nonDraw]
    D --> E{State changed?}
    E -- invalidate called --> D
    E -- no change --> F[View idle]
  • measure pass — determines how big the view wants to be.
  • layout pass — determines where the view is positioned.
  • draw passonDraw(Canvas) executes; pixels are written to the screen.
  • Calling invalidate() re-triggers only the draw pass (cheap). Calling requestLayout() re-triggers all three (more expensive; use only when size or position must change).

🧪 Try It Yourself

Task: Extend CustomCircleView to support a customizable stroke ring around the circle.

  1. Add a second Paint object called strokePaint configured with Paint.Style.STROKE and a stroke width of 8dp (convert to pixels using getResources().getDisplayMetrics().density).
  2. In onDraw, draw the stroke ring after the filled circle so it appears on top.
  3. Add a setStrokeColor(int color) method that updates strokePaint and calls invalidate().

Starter snippet:

private Paint strokePaint;

private void init() {
    // ... existing paint setup ...

    strokePaint = new Paint();
    strokePaint.setStyle(Paint.Style.STROKE);
    strokePaint.setStrokeWidth(8 * getResources().getDisplayMetrics().density);
    strokePaint.setColor(Color.BLACK);
    strokePaint.setAntiAlias(true);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width  = getWidth();
    int height = getHeight();
    int radius = Math.min(width, height) / 2;
    canvas.drawCircle(width / 2, height / 2, radius, paint);
    canvas.drawCircle(width / 2, height / 2, radius, strokePaint); // add this
}

Success criterion: Run the app — the circle should display a black ring around its edge. Call setStrokeColor(Color.GREEN) from the button's click listener and confirm the ring turns green without the circle itself changing color.

🔍 Checkpoint Quiz

Q1. What is the primary difference between a Custom View and a Composite View in Android?

A) Custom Views use XML; Composite Views use Java only
B) Custom Views render via Canvas; Composite Views combine existing views inflated from XML
C) Custom Views can only be circles; Composite Views can be any shape
D) There is no difference — they are the same concept with different names

Q2. Given the following snippet, what happens when setCircleColor(Color.GREEN) is called?

public void setCircleColor(int color) {
    circleColor = color;
    paint.setColor(circleColor);
    // invalidate() is missing
}

A) The circle immediately redraws in green
B) The color field updates but the screen does not change until the next system redraw
C) An IllegalStateException is thrown
D) The app crashes with a NullPointerException

Q3. In ProfileView.init(), the call is inflater.inflate(R.layout.profile_view, this, true). What does the third argument true do?

A) Enables animations on the inflated view
B) Attaches the inflated view tree to this as the parent, making it part of the composite
C) Marks the layout as read-only after inflation
D) Forces the layout to use wrap_content for both dimensions

Q4. You need to build a custom radial progress bar that animates smoothly. Which approach is more appropriate, and why?

A1. B — Custom Views draw themselves pixel by pixel on a Canvas; Composite Views inflate existing widget trees and expose a clean API around them.

A2. B — Without invalidate(), the Paint object holds the new color but no redraw is scheduled. The circle will only visually update if the system happens to trigger a draw pass for another reason (e.g., the screen refreshes due to another view changing). Always call invalidate() after mutating drawing state.

A3. B — The boolean attachToRoot parameter tells the inflater to add the inflated hierarchy as a child of the provided parent (this). Passing true is correct for Composite Views because the ViewGroup subclass is the root; passing false would inflate the layout detached and nothing would appear.

A4. A Custom View is the right choice. Smooth animation requires direct Canvas control: you compute the sweep angle each frame, call invalidate(), and the system schedules the next draw. Composite Views are built from pre-existing widgets that have no concept of arc drawing or frame-by-frame canvas control.

🪞 Recap

  • Android custom UI splits into Custom Views (Canvas-based, drawn from scratch) and Composite Views (XML-inflated widget groups).
  • Override onDraw(Canvas) to render a Custom View; always call invalidate() after any state change that affects appearance.
  • Three constructors — (Context), (Context, AttributeSet), (Context, AttributeSet, int) — are required so the view works both in code and in XML layouts.
  • A Composite View inflates its XML layout with attachToRoot = true inside init(), then exposes typed setters to hide internal child-view details from callers.
  • Use fully qualified class names (e.g., com.example.customview.CustomCircleView) when referencing custom components in XML layout files.

📚 Further Reading

Like this topic? It’s one of 28 in Android Native Developer.

Block your seat for ₹2,500 and join the next cohort.