Android gives you a rich set of built-in UI components, but every so often one of them doesn't behave the way you need. The standard Checkbox had a persistent click-registration issue inside list views — the animation would play but the state wouldn't update. Rather than working around it, I built a replacement from scratch by extending View directly. Here's how.

## Project Setup

Create a new Android Application project with an Empty Activity in Android Studio.


## The Base Class

Start by creating MaterialCheckbox, extending View and wiring up the three constructors:

package com.github.angads25.customviewdemo;

import android.content.Context;
import android.graphics.Canvas;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

public class MaterialCheckbox extends View {
    private Context context;

    public MaterialCheckbox(Context context) {
        super(context);
        initView(context);
    }

    public MaterialCheckbox(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    private void initView(Context context) {
        this.context = context;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

## Measuring: Forcing a Square

A checkbox should always be square. Override onMeasure to take the smaller of the two dimensions and apply it to both:

private int minDim;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    minDim = Math.min(width, height);
    setMeasuredDimension(minDim, minDim);
}

## State and Paint

Add checked state tracking and a Paint object in initView:

private boolean checked;
private Paint paint;

private void initView(Context context) {
    this.context = context;
    checked = false;
    paint = new Paint();
}

public boolean isChecked() {
    return checked;
}

public void setChecked(boolean checked) {
    this.checked = checked;
    invalidate();
}

Note

Calling invalidate() in the setter tells the framework to redraw the view with the new state.


## Drawing the Checkbox

### Unchecked state

Draw a grey rounded rectangle, then a white inner rectangle to create the border effect:

private RectF bounds;

// inside onDraw, unchecked branch:
paint.reset();
paint.setAntiAlias(true);
bounds.set(minDim / 10, minDim / 10,
           minDim - (minDim / 10), minDim - (minDim / 10));
paint.setColor(Color.parseColor("#C1C1C1"));
canvas.drawRoundRect(bounds, minDim / 8, minDim / 8, paint);

bounds.set(minDim / 5, minDim / 5,
           minDim - (minDim / 5), minDim - (minDim / 5));
paint.setColor(Color.parseColor("#FFFFFF"));
canvas.drawRect(bounds, paint);

### Checked state

Draw a coloured rounded rectangle (using colorAccent from your theme), then overlay the tick path:

// checked branch:
paint.reset();
paint.setAntiAlias(true);
bounds.set(minDim / 10, minDim / 10,
           minDim - (minDim / 10), minDim - (minDim / 10));

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    paint.setColor(getResources().getColor(R.color.colorAccent, context.getTheme()));
} else {
    paint.setColor(getResources().getColor(R.color.colorAccent));
}
canvas.drawRoundRect(bounds, minDim / 8, minDim / 8, paint);

paint.setColor(Color.parseColor("#FFFFFF"));
paint.setStrokeWidth(minDim / 10);
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(tick, paint);

## Drawing the Tick Mark

The tick is a Path built from two line segments. Construct it once in onMeasure after minDim is known:

private Path tick;

// in initView:
tick = new Path();

// in onMeasure, after minDim is set:
tick.moveTo(minDim / 4, minDim / 2);
tick.lineTo(minDim / 2.5f, minDim - (minDim / 3));
tick.moveTo(minDim / 2.8f, minDim - (minDim / 3.25f));
tick.lineTo(minDim - (minDim / 4), minDim / 3);

The path coordinates map to this layout — the first segment is the short downward stroke, the second is the long upward stroke:

Path point diagram


## Checked/Unchecked States

Here is what the two states look like side by side:

Checkbox design — unchecked and checked


## Click Listener

Define an interface for state-change callbacks:

public interface OnCheckedChangeListener {
    void onCheckedChanged(MaterialCheckbox checkbox, boolean isChecked);
}

Wire it up in initView:

private OnCheckedChangeListener onCheckedChangeListener;

public void setOnCheckedChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
    this.onCheckedChangeListener = onCheckedChangeListener;
}

// inside initView:
OnClickListener onClickListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
        setChecked(!checked);
        if (onCheckedChangeListener != null) {
            onCheckedChangeListener.onCheckedChanged(MaterialCheckbox.this, isChecked());
        }
    }
};
setOnClickListener(onClickListener);

## Using the View

### In XML

<com.github.angads25.customviewdemo.MaterialCheckbox
    android:id="@+id/checkbox"
    android:layout_width="32dp"
    android:layout_height="32dp"
    android:layout_margin="8dp" />

### In Java

MaterialCheckbox checkbox = (MaterialCheckbox) findViewById(R.id.checkbox);
checkbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(MaterialCheckbox checkbox, boolean isChecked) {
        // isChecked is the new state
    }
});

Here is a demo of the finished view in action:

Demo of MaterialCheckbox in a list

The full source is on GitHub.

support

Found this useful? You can buy me a coffee.

☕ buy me a coffee