Center CheckBox Drawable Of SegmentedBar In GreenDroid
Posted by blogmeister on
August 15, 2011
GreenDroid has a great widget called a SegmentedBar where an app can display information in different sections much like how tabs work but they all get loaded at one time.
From the developer of GreenDroid, he describes a SegmentedBar as a bunch of segments where each segment has an indicator that can be on or off depending on its current state (checked or not). Once you are tapping a segment, the associated view is displayed.
Each of these segments is represented by a CheckBox widget. There came a point wherein I wanted my app to show only images without any text. The problem was that even if I managed to center the default checkbox image (the one with a checkmark), it did not center itself when I set a custom drawable image like the ones below.
![]()
There was always an extra space on the right that made the drawable images not aligned to the center. So how did I manage to make the custom drawables aligned to the center (pictured above)? By creating my own custom CheckBox class.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public class CheckBoxCenter extends CheckBox { private Drawable buttonDrawable; public CheckBoxCenter(Context context, AttributeSet attrs) { super(context, attrs); setButtonDrawable(android.R.id.empty); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (buttonDrawable != null) { buttonDrawable.setState(getDrawableState()); final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; final int height = buttonDrawable.getIntrinsicHeight(); int y = 0; switch (verticalGravity) { case Gravity.BOTTOM: y = getHeight() - height; break; case Gravity.CENTER_VERTICAL: y = (getHeight() - height) / 2; break; } int buttonWidth = buttonDrawable.getIntrinsicWidth(); int buttonLeft = (getWidth() - buttonWidth) / 2; buttonDrawable.setBounds(buttonLeft, y, buttonLeft+buttonWidth, y + height); buttonDrawable.draw(canvas); } } @Override public void setButtonDrawable(int resid) { setButtonDrawable(getContext().getResources().getDrawable(resid)); } @Override public void setButtonDrawable(Drawable d) { buttonDrawable = d; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (buttonDrawable != null) { int[] myDrawableState = getDrawableState(); buttonDrawable.setState(myDrawableState); invalidate(); } } } |
I also extended GreenDroid classes related to the SegmentedBar (no sense modifying the source in GreenDroid in case of new updates, right?) to cater to the XML layout file of the CheckBox widget.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
public class IconOnlySegmentedBar extends LinearLayout implements OnFocusChangeListener { /** * Clients may use this listener to be notified of any changes that occurs * on the SegmentBar * * @author Cyril Mottier */ public static interface OnSegmentChangeListener { /** * Notification that the current segment has changed. * * @param index The index of the new selected segment. * @param clicked Whether the segment has been selected via a user * click. */ public void onSegmentChange(int index, boolean clicked); } private OnSegmentChangeListener mOnSegmentChangeListener; private int mCheckedSegment; private Drawable mDividerDrawable; private int mDividerWidth; public IconOnlySegmentedBar(Context context) { this(context, null); } public IconOnlySegmentedBar(Context context, AttributeSet attrs) { this(context, attrs, R.attr.gdSegmentedBarStyle); } public IconOnlySegmentedBar(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); initSegmentedBar(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SegmentedBar, defStyle, 0); mDividerDrawable = a.getDrawable(R.styleable.SegmentedBar_dividerDrawable); mDividerWidth = a.getDimensionPixelSize(R.styleable.SegmentedBar_dividerWidth, 0); a.recycle(); } private void initSegmentedBar() { mCheckedSegment = 0; setOrientation(LinearLayout.HORIZONTAL); // Register ourselves so that we can handle focus on internal segments setFocusable(true); setOnFocusChangeListener(this); } /** * Sets the drawable that is used as divider between each segment. * * @param dividerDrawable The drawable to used as a divider. Note : using a * ColorDrawable will not work properly as the intrinsic width of * a ColorDrawable is -1. */ public void setDividerDrawable(Drawable dividerDrawable) { mDividerDrawable = dividerDrawable; } /** * Sets the drawable that is used as divider between each segment. * * @param resId The identifier of the Drawable to use. */ public void setDividerDrawable(int resId) { mDividerDrawable = getContext().getResources().getDrawable(resId); } /** * Sets the width of the divider that will be used as segment divider. If * the dividerWidth has not been set, the intrinsic width of the divider * drawable is used. * * @param width Width of the divider */ public void setDividerWidth(int width) { mDividerWidth = width; } /** * Returns the current number of segment in this SegmentBar * * @return The number of segments in this SegmentBar */ public int getSegmentCount() { int segmentCount = getChildCount(); // If we have divider we'll have an odd number of child if (mDividerDrawable != null) { segmentCount = (segmentCount + 1) / 2; } return segmentCount; } /** * Use this method to register an OnSegmentChangeListener and listen to * changes that occur on this SegmentBar * * @param listener The listener to use */ public void setOnSegmentChangeListener(OnSegmentChangeListener listener) { mOnSegmentChangeListener = listener; } /** * Sets the current segment to the index <em>index</em> * * @param index The index of the segment to set. Client will be notified * using this method of the segment change. */ public void setCurrentSegment(int index) { if (index < 0 || index >= getSegmentCount()) { return; } mCheckedSegment = index; ((CheckBoxCenter) ((LinearLayout) getChildSegmentAt(mCheckedSegment)).getChildAt(0)).setChecked(true); } /** * Returns the view representing the segment at the index <em>index</em> * * @param index The index of the segment to retrieve * @return The view that represents the segment at index <em>index</em> */ public View getChildSegmentAt(int index) { /* * If we are using dividers, then instead of segments at 0, 1, 2, ... we * have segments at 0, 2, 4, ... */ if (mDividerDrawable != null) { index *= 2; } return getChildAt(index); } /** * Adds a segment to the SegmentBar. This method automatically adds a * divider if needed. * * @param title The title of the segment to add. */ public void addSegment(String title) { final Context context = getContext(); final LayoutInflater inflater = LayoutInflater.from(context); /* * First of all, we have to check whether or not we need to add a * divider. A divider is added when there is at least one segment */ if (mDividerDrawable != null && getSegmentCount() > 0) { ImageView divider = new ImageView(context); final int width = (mDividerWidth > 0) ? mDividerWidth : mDividerDrawable.getIntrinsicWidth(); final LinearLayout.LayoutParams lp = new LayoutParams(width, LayoutParams.FILL_PARENT); lp.setMargins(0, 0, 0, 0); divider.setLayoutParams(lp); divider.setBackgroundDrawable(mDividerDrawable); addView(divider); } LinearLayout ll = (LinearLayout) inflater.inflate(R.layout.segmented_segment_icononly, this, false); CheckBoxCenter segment = (CheckBoxCenter) ll.getChildAt(0); segment.setText(title); segment.setClickable(true); segment.setFocusable(true); segment.setOnFocusChangeListener(this); segment.setOnCheckedChangeListener(new SegmentCheckedListener(getSegmentCount())); segment.setOnClickListener(new SegmentClickedListener(getSegmentCount())); addView(ll); } public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { return; } if (v == this) { final View segment = getChildSegmentAt(mCheckedSegment); if (segment != null) { segment.requestFocus(); } } else { final int segmentCounts = getSegmentCount(); for (int i = 0; i < segmentCounts; i++) { if (getChildSegmentAt(i) == v) { setCurrentSegment(i); notifyListener(i, false); break; } } } } private class SegmentClickedListener implements OnClickListener { private final int mSegmentIndex; private SegmentClickedListener(int segmentIndex) { mSegmentIndex = segmentIndex; } public void onClick(View v) { final CheckBoxCenter segment = (CheckBoxCenter) ((LinearLayout) getChildSegmentAt(mCheckedSegment)).getChildAt(0); if (mSegmentIndex == mCheckedSegment && !segment.isChecked()) { segment.setChecked(true); } else { notifyListener(mSegmentIndex, true); } } } private class SegmentCheckedListener implements OnCheckedChangeListener { private final int mSegmentIndex; private SegmentCheckedListener(int segmentIndex) { mSegmentIndex = segmentIndex; } public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { setCurrentSegment(mSegmentIndex); } } } private void notifyListener(int index, boolean clicked) { if (mOnSegmentChangeListener != null) { mOnSegmentChangeListener.onSegmentChange(index, clicked); } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
public class IconOnlySegmentedHost extends LinearLayout { private final static String LOG_TAG = SegmentedHost.class.getSimpleName(); private int mSegmentedBarId; private IconOnlySegmentedBar mSegmentedBar; private int mSegmentedHostId; private FrameLayout mContentView; private int mSelectedSegment; private SegmentedAdapter mAdapter; private View[] mViews; private DataSetObserver mSegmentObserver = new DataSetObserver() { public void onChanged() { setupSegmentedHost(mSelectedSegment); } public void onInvalidated() { // Do nothing - method never called } }; public IconOnlySegmentedHost(Context context) { this(context, null); } public IconOnlySegmentedHost(Context context, AttributeSet attrs) { this(context, attrs, R.attr.gdSegmentedHostStyle); } public IconOnlySegmentedHost(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); initSegmentedView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SegmentedHost, defStyle, 0); mSegmentedBarId = a.getResourceId(R.styleable.SegmentedHost_segmentedBar, -1); if (mSegmentedBarId <= 0) { throw new IllegalArgumentException("The segmentedBar attribute is required and must refer " + "to a valid child."); } mSegmentedHostId = a.getResourceId(R.styleable.SegmentedHost_segmentedContentView, -1); if (mSegmentedHostId <= 0) { throw new IllegalArgumentException("The segmentedHost attribute is required and must refer " + "to a valid child."); } } private void initSegmentedView() { setOrientation(LinearLayout.VERTICAL); } @Override protected void onFinishInflate() { super.onFinishInflate(); mSegmentedBar = (IconOnlySegmentedBar) findViewById(mSegmentedBarId); if (mSegmentedBar == null) { throw new IllegalArgumentException("The segmentedBar attribute must refer to an existing child."); } mSegmentedBar.setOnSegmentChangeListener(new SegmentSwitcher()); mContentView = (FrameLayout) findViewById(mSegmentedHostId); if (mContentView == null) { throw new IllegalArgumentException("The segmentedHost attribute must refer to an existing child."); } else if (!(mContentView instanceof FrameLayout)) { throw new RuntimeException("The segmentedHost attribute must refer to a FrameLayout"); } } public IconOnlySegmentedBar getSegmentedBar() { return mSegmentedBar; } public FrameLayout getContentView() { return mContentView; } public void setAdapter(SegmentedAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mSegmentObserver); } mAdapter = adapter; if (adapter != null) { mAdapter.registerDataSetObserver(mSegmentObserver); } setupSegmentedHost(0); } private void setupSegmentedHost(int selectedSegment) { if (Config.GD_INFO_LOGS_ENABLED) { Log.i(LOG_TAG, "Preparing the SegmentedHost with the segment " + selectedSegment); } mSegmentedBar.removeAllViews(); mContentView.removeAllViews(); mViews = null; if (mAdapter != null) { // Add all segments to the Segmentedbar final int count = mAdapter.getCount(); for (int i = 0; i < count; i++) { mSegmentedBar.addSegment(mAdapter.getSegmentTitle(i)); } if (selectedSegment < 0) { selectedSegment = 0; } else if (selectedSegment > count) { selectedSegment = count; } if (count > 0) { // Prepare the SegmentBar mViews = new View[count]; mSegmentedBar.setCurrentSegment(selectedSegment); // And displays the first view setContentView(selectedSegment); } } } private class SegmentSwitcher implements OnSegmentChangeListener { public void onSegmentChange(int index, boolean clicked) { setContentView(index); } } private void setContentView(int index) { mSelectedSegment = index; mContentView.removeAllViews(); if (mViews[index] == null) { mViews[index] = mAdapter.getView(index, IconOnlySegmentedHost.this); } mContentView.addView(mViews[index]); } } |
Here is the segmented bar background 9-patch image that I trimmed. Save it as segment_bg.9.png.
![]()
Here is a sample drawable state list on for a checkbox. Make sure you place this in the drawables folder.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Enabled states --> <item android:state_checked="true" android:state_window_focused="false" android:state_enabled="true" android:drawable="@drawable/home_alert_on" /> <item android:state_checked="false" android:state_window_focused="false" android:state_enabled="true" android:drawable="@drawable/home_alert_off" /> <item android:state_checked="false" android:state_enabled="true" android:drawable="@drawable/home_alert_off" /> <item android:state_checked="true" android:state_enabled="true" android:drawable="@drawable/home_alert_on" /> </selector> |
Here is the segmented XML file that covers the CheckBox. I purposely placed another LinearLayout layer on top of the CheckBox widget to center it within the layout.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:gravity="center" android:layout_weight=".20" android:layout_width="0dip" android:layout_height="wrap_content" android:background="@drawable/segment_bg" > <mypackage.blabla.CheckBoxCenter android:layout_height="wrap_content" android:layout_width="wrap_content" /> </LinearLayout> |
To make this work, here is a sample code that assigns a drawable to each segment. For this sample, I have 6 segments in total.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
IconOnlySegmentedBar bar = segmentedHost.getSegmentedBar(); for (int i=0; i<bar.getChildCount(); i++) { if (bar.getChildAt(i) instanceof LinearLayout) { CheckBoxCenter cb = (CheckBoxCenter) ((LinearLayout) bar.getChildAt(i)).getChildAt(0); if (i == 0) cb.setButtonDrawable(R.drawable.home_alert); else if (i == 2) cb.setButtonDrawable(R.drawable.home_alert); else if (i == 4) cb.setButtonDrawable(R.drawable.home_alert); else if (i == 6) cb.setButtonDrawable(R.drawable.home_alert); else if (i == 8) cb.setButtonDrawable(R.drawable.home_alert); else if (i == 10) cb.setButtonDrawable(R.drawable.home_alert); } } |
If you wondered why the index is in increments of even numbers, it is because the odd numbers represent the divider drawable image that divides each segment.
So there you have it. It took me quite some time to figure this out. I had to dig into the CompoundButton class to see what methods I could override to make the icons of segments without text aligned to the center.
Donations appreciated. Every little $ helps. Or click Google +1.









