Home > android, ui > Creating a checkable image button

Creating a checkable image button

To continue with the theme of custom styleable controls, here is how to implement your own image button that behaves like a radio button or a check box.

I wanted to make it easy (for myself) to create a bunch of these buttons, differing only by the image they use, and to avoid having to create state-dependent variations of those images.

In other words, the background uses state-dependent drawables, while the foreground image is fixed (per button).

This is what it looks like:

Creating these in XML was easy:

<your.package.name.CheckableImageButton
	android:id="@+id/checkable_image_1"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:background="@drawable/checkable_image_selector"
	android:src="@drawable/checkable_icon_1" />

<your.package.name.CheckableImageButton
	android:id="@+id/checkable_image_2"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:background="@drawable/checkable_image_selector"
	android:src="@drawable/checkable_icon_2" />

The images checkable_icon_1, checkable_icon_2 only differ by color, and are plain PNG files.

Declaring the styleable

<!-- Goes into res/values/styles.xml -->
<declare-styleable name="CheckableImageButton">
	<attr name="is_checked" format="boolean" />
        
	<attr name="personality">
            	<enum name="radio" value="0" />
            	<enum name="check" value="1" />
        	</attr>
</declare-styleable>  

The personality value will be used to make the button behave like a check box (which toggles its state when touched) or a radio button (which always goes into checked state, regardless of current state).

The code

Android’s ImageButton doesn’t support the notion of a checked state, but otherwise is pretty close.

Here is my subclass:

public class CheckableImageButton extends ImageButton implements Checkable {

	private boolean mChecked;
	private boolean mBroadcasting;
	private int mPersonality;
	private OnCheckedChangeListener mOnCheckedChangeListener;

	private static final int[] CHECKED_STATE_SET = { R.attr.is_checked };

	private static final int PERSONALITY_RADIO_BUTTON = 0;
	private static final int PERSONALITY_CHECK_BOX = 1;

	public CheckableImageButton(Context context) {
		this(context, null);
	}

	public CheckableImageButton(Context context, AttributeSet attrs) {
		super(context, attrs);

		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CheckableImageButton);

		mPersonality = a.getInt(R.styleable.CheckableImageButton_personality, PERSONALITY_RADIO_BUTTON);
		boolean checked = a.getBoolean(R.styleable.CheckableImageButton_is_checked, false);
		setChecked(checked);

		a.recycle();
	}

	public void toggle() {
		setChecked(!mChecked);
	}

	@Override
	public boolean performClick() {
		if (mPersonality == PERSONALITY_RADIO_BUTTON) {
			setChecked(true);
		} else if (mPersonality == PERSONALITY_CHECK_BOX) {
			toggle();
		}
		return super.performClick();
	}

	public boolean isChecked() {
		return mChecked;
	}

	/**
	 * <p>
	 * Changes the checked state of this button.
	 * </p>
	 * 
	 * @param checked
	 *            true to check the button, false to uncheck it
	 */
	public void setChecked(boolean checked) {
		if (mChecked != checked) {
			mChecked = checked;
			refreshDrawableState();

			// Avoid infinite recursions if setChecked() is called from a listener
			if (mBroadcasting) {
				return;
			}

			mBroadcasting = true;
			if (mOnCheckedChangeListener != null) {
				mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
			}

			mBroadcasting = false;
		}
	}

	/**
	 * Register a callback to be invoked when the checked state of this button changes.
	 * 
	 * @param listener
	 *            the callback to call on checked state change
	 */
	public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
		mOnCheckedChangeListener = listener;
	}

	/**
	 * Interface definition for a callback.
	 */
	public static interface OnCheckedChangeListener {
		/**
		 * Called when the checked state of a button has changed.
		 * 
		 * @param button
		 *            The button view whose state has changed.
		 * @param isChecked
		 *            The new checked state of button.
		 */
		void onCheckedChanged(CheckableImageButton button, boolean isChecked);
	}

	@Override
	public int[] onCreateDrawableState(int extraSpace) {
		final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
		if (isChecked()) {
			mergeDrawableStates(drawableState, CHECKED_STATE_SET);
		}
		return drawableState;
	}

	@Override
	protected void drawableStateChanged() {
		super.drawableStateChanged();
		invalidate();
	}

	static class SavedState extends BaseSavedState {
		boolean checked;

		SavedState(Parcelable superState) {
			super(superState);
		}

		private SavedState(Parcel in) {
			super(in);
			checked = (Boolean) in.readValue(null);
		}

		@Override
		public void writeToParcel(Parcel out, int flags) {
			super.writeToParcel(out, flags);
			out.writeValue(checked);
		}

		public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
			public SavedState createFromParcel(Parcel in) {
				return new SavedState(in);
			}

			public SavedState[] newArray(int size) {
				return new SavedState[size];
			}
		};
	}

	@Override
	public Parcelable onSaveInstanceState() {
		Parcelable superState = super.onSaveInstanceState();
		SavedState ss = new SavedState(superState);
		ss.checked = isChecked();
		return ss;
	}

	@Override
	public void onRestoreInstanceState(Parcelable state) {
		SavedState ss = (SavedState) state;

		super.onRestoreInstanceState(ss.getSuperState());
		setChecked(ss.checked);
		requestLayout();
	}
}

The button adds a new drawable state to the base class, which is exposed via onCreateDrawableState.

Switchable personality is supported by getting a value from XML in the constructor:

mPersonality = a.getInt(R.styleable.CheckableImageButton_personality, PERSONALITY_RADIO_BUTTON);

and doing different things in performClick depending on whether the personality is a checkbox or a radiobutton:

	@Override
	public boolean performClick() {
		if (mPersonality == PERSONALITY_RADIO_BUTTON) {
			setChecked(true);
		} else if (mPersonality == PERSONALITY_CHECK_BOX) {
			toggle();
		}
		return super.performClick();
	}

The background drawable selector with states

This goes into res/drawable/checkable_image_selector.xml, and can shared by all CheckableImageButton objects.

<?xml version="1.0" encoding="utf-8"?>
<selector
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:sample="http://schemas.android.com/apk/res/your.package.name">
	<item
		android:state_pressed="true"
		android:drawable="@drawable/checkable_image_button_state_pressed" />
	<item
		sample:is_checked="true"
		android:drawable="@drawable/checkable_image_button_state_checked" />
	<item
		android:drawable="@android:color/transparent" />
</selector>

Note the use of sample namespace for is_checked. This links the attribute to the styleable in styles.xml and then on to R.styleable.CheckableImageButton in the constructor.

Finally, background drawables for individual states

This is the drawable for pressed state. You can see in the second screenshot near the top of this page. The magic colors for the gradient were picked from nine-patch drawables supplied with the SDK. The stroke color is a nice shade of pale blue I happen to like.

<?xml version="1.0" encoding="utf-8"?>
<shape
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:shape="rectangle">
	<corners
		android:radius="4dp" />
	<padding
		android:left="4dp"
		android:right="4dp"
		android:top="4dp"
		android:bottom="4dp" />
	<gradient
		android:angle="90"
		android:startColor="#FFFFC700"
		android:centerColor="#FFFFA600"
		android:endColor="#FFFFC700" />
	<stroke
		android:width="3dp"
		android:color="#FF80B0E0" />
</shape>

The “non-pressed” checked state is simplier, as it doesn’t need a gradient fill:

<?xml version="1.0" encoding="utf-8"?>
<shape
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:shape="rectangle">
	<corners
		android:radius="4dp" />
	<padding
		android:left="4dp"
		android:right="4dp"
		android:top="4dp"
		android:bottom="4dp" />
	<solid
		android:color="#00000000" />
	<stroke
		android:width="3dp"
		android:color="#FF80B0E0" />
</shape>

And that’s about it. Hope this was useful.

Advertisements
Categories: android, ui
  1. opasam
    July 21, 2013 at 6:11 pm

    Kostya Vasilyev :
    Sounds like it’s not, somewhere.. Are you initializing button states? Say, calling setChecked(true) on the first button (be sure to do it after setting onCheckedChangeListener)

    Thanks. This works for me:

    cib1.setOnCheckedChangeListener(onCheckedChanged); cib2.setOnCheckedChangeListener(onCheckedChanged);

    mCurrentButton = cib1;
    mCurrentButton.setChecked(false);

    I hope, this is, what you mean.

  2. opasam
    July 21, 2013 at 12:17 pm

    Is my source code not correkt ?

    • opasam
      July 21, 2013 at 12:46 pm

      sorry, I mean not correct 😉

      • July 21, 2013 at 3:17 pm

        Sounds like it’s not, somewhere.. Are you initializing button states? Say, calling setChecked(true) on the first button (be sure to do it after setting onCheckedChangeListener)

  3. opasam
    July 20, 2013 at 6:29 pm

    Can you explain please, how to keep track currently checked button as a member variable ?

    • July 20, 2013 at 6:35 pm

      Add a variable somewhere in the activity (or fragment, or view group) that has the value of the currently checked button.

      Implement CompoundButton.OnCheckedChangeListener.

      In the listener, implement onCheckedChanged as shown in the code snippet below, to allow only one button to be checked at a time (so it works like a radio button group).

      • opasam
        July 20, 2013 at 8:17 pm

        Wow, you are amazing!!!!!

        So here is my code:

        public class MainActivity extends Activity {

        private CheckableImageButton cib1,cib2;

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

        cib1 = (CheckableImageButton) findViewById(R.id.checkable_image_1);
        cib2 = (CheckableImageButton) findViewById(R.id.checkable_image_2);

        cib1.setOnCheckedChangeListener(onCheckedChanged);
        cib2.setOnCheckedChangeListener(onCheckedChanged);

        }

        OnCheckedChangeListener onCheckedChanged = new OnCheckedChangeListener() {

        @Override
        public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
        // TODO Auto-generated method stub
        if (isChecked && mCurrentButton != button) {
        mCurrentButton.setChecked(false);
        mCurrentButton = button;

        }

        }
        };

        What to do now to retrieve the value of the currently checked button ?

      • opasam
        July 20, 2013 at 8:59 pm

        What to do now to retrieve the value of the currently checked button ?

      • July 20, 2013 at 10:18 pm

        In the code snippet below, the currently selected button is always in mCurrentButton.

      • opasam
        July 20, 2013 at 10:51 pm

        Kostya Vasilyev :
        In the code snippet below, the currently selected button is always in mCurrentButton.

        Thanx, but my code doesn’t work. Where is the error ? Can you post an example, that works for me ?

  4. Andreas
    March 28, 2011 at 6:55 pm

    Nice Tutorial, Thanks! But how to group the radio buttons. Could you post a more detailed example?

    • March 28, 2011 at 7:07 pm

      Basically, like this:

      1 – In the Activity that uses these buttons, find them all, and setOnCheckedChangeListener for all, let’s say to the activity itself.

      2 – Keep track of currently checked button as a member variable.

      3 – In the checked change listener’s onCheckedChanged, do something like this:


      @Override
      public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
      if (isChecked && mCurrentButton != button) {
      mCurrentButton.setChecked(false);
      mCurrentButton = button;
      }
      }

      This makes mCurrentButton always be the currently selected button, and unselects the other buttons, so it’s like a group of radio buttons.

      • Andreas
        March 28, 2011 at 7:41 pm

        Wow that’s a fast reply. Thanks. I’ll try it.

  5. simon.zhou
    January 29, 2011 at 7:54 am

    Thinks.

  1. November 9, 2013 at 8:08 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s