Implementing ListView to support single and multi select

Listview is a commonly used UI element in Android application design. ListView allows users to select one or more items on the list. However ListView allows you to select one item or multiple items but not both simultaneously. So if the application needs an interface similar to GMail app implementation you are out of luck with the default implementation. That brings us to the point of this writeup, to implement a UI widget which responds to single selection as well as multiple selection.
Our approach will be to implement a custom view and a parent activity
which responds appropriately. Our custom view will have two lines of text and a checkbox
and will look like as bellow.


When the user touches the checkbox region, we will repaint the view and add/remove
a checkmark as user feedback and report the same to the parent as well. If the
touch is on non-checkbox area then we treat it as a item select event and let
the listeners handle the event accordingly.
As all custom view implementations we will extend View.
public class CustomItemView extends View
We have to override, onMeasure, onDraw, onLayout, onTouchEvent. When onMeasure
gets called we have compute how much space we would need, during onLayout we
will compute where each component will be drawn. As the method name suggest, onDraw, we draw the view and onTouchEvent we determine how to respond.
onMeasure method is called to get the measurements for this view, so this method gives us the chance to calculate the dimensions needed. Measure mode MeasureSpec.EXACTLY or MeasureSpec.AT_MOST gives us hints on how much width or height is available to work with. Code would be as below.
protected void onMeasure(int widthMeasureSpec,
        int heightMeasureSpec) {
private int measureHeight(int heightMeasureSpec) {
    int specMode = MeasureSpec.getMode(heightMeasureSpec);
    int specSize = MeasureSpec.getSize(heightMeasureSpec);

    int height;

    if (specMode == MeasureSpec.EXACTLY)
        height = specSize;
    else {
        height = mScaledDensity;

        if (specMode == MeasureSpec.AT_MOST)
            height = Math.min(height, specSize);
    return height;

private measureWidth(int widthMeasureSpec) {
    int specMode = MeasureSpec.getMode(widthMeasureSpec);
    int specSize = MeasureSpec.getSize(widthMeasureSpec);
    int width;

    mTextWidth = specSize - getPaddingLeft() -
	    mCheckOff.getMinimumWidth() - getPaddingRight();

    if (specMode == MeasureSpec.EXACTLY)
        width = specSize;
    else {
        CharSequence temp = TextUtils.ellipsize(mLineOne,
		mTextPaint, mTextWidth, mEllipsize);
        width = (int) mTextPaint.measureText((String) temp)
                + getPaddingLeft() + getPaddingRight()
                + mCheckOff.getMinimumWidth();

        if (specMode == MeasureSpec.AT_MOST)
            width = Math.min(width, specSize);
    return width;
onLayout is called next, the inputs contains the absolute position of our view. This is good opportunity to calculate the coordinates of our controls. These coordinates will be used to eventually during onDraw method.
protected void onLayout(boolean changed, int left, int top,
    int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    mViewWidth = right - left;
    mViewHeight = bottom - top;

private void computeCooridates() {
   int leftPadding = getPaddingLeft();
    int rightPadding = getPaddingRight();
    int bottomPadding = getPaddingBottom();
    int topPadding = getPaddingTop();

    int size = mCheckOff.getMinimumWidth();
    mLineOneX = leftPadding;
    mLineOneY = topPadding - (int) mTextPaint.ascent();

    mCheckboxX = mViewWidth - rightPadding - size;
    mCheckboxY = topPadding + mCheckboxTop;
    mCheckboxRect = new Rect(mCheckboxX, mCheckboxY,
            mCheckboxX + size, mCheckboxY + size);

    int descent = (int) mTextPaint.descent();

    mLineTwoX = leftPadding;
    mLineTwoY = mViewHeight - descent - bottomPadding;
Next method to the called is onDraw. We draw the first line followed by the checkbox bitmap and lastly the second line. We have two bitmaps for the two states of the checkbox, based on the state of the checkbox we paint the appropriate bitmap.
protected void onDraw(Canvas canvas) {

    String desc = (String) TextUtils.ellipsize(mLineOne,
        mTextPaint, mTextWidth, mEllipsize);
    canvas.drawText(desc, mLineOneX, mLineOneY, mTextPaint);

    BitmapDrawable bitmap = mIsChecked ? mCheckOn : mCheckOff;

    canvas.drawBitmap(bitmap.getBitmap(), null, mCheckboxRect,
            (Paint) mTextPaint);

    canvas.drawText(mLineTwo, mLineTwoX, mLineTwoY, mTextPaint);
Finally we override the onTouchEvent method to respond to UI interactions by the user. Primarily we are concerned with MotionEvent.ACTION_DOWN and MotionEvent.ACTION_UP. On ACTION_DOWN, check if the touch coordinates fall within the range of the checkbox region, if yes, then set the flag. If the flag is set when we get ACTION_UP, then we have to toggle the
checkbox, notify the listener and invalidate the view. We return true when no more processing of the event is needed and false otherwise.
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        int x = (int) event.getX();
        if (x >= mCheckboxX) {
            mKeyDown = true;
            return true;
    case MotionEvent.ACTION_UP:
        if (mKeyDown) {
            mKeyDown = false;
            return true;

    return super.onTouchEvent(event);

private void toggleCheckbox() {
    mIsChecked = !mIsChecked;
    if (mItemToggleListener != null)
        mItemToggleListener.toggle(mItemId, mIsChecked);
In our parent activity, implement OnItemClickListener and OnItemToggleListener. When
onItemClick is called handle single click and when onItemToggle is called maintain our of map of selected items. Out of the box adapter implementations cannot handle a custom view, so finally we implement a custom adapter.


Download Android project source code from Dropbox

Leave a comment

Your email address will not be published. Required fields are marked *

Spam protection by WP Captcha-Free