Radio Group and Radio Button are two great components provided by the Android Framework, but unfortunately they often cannot satisfy all requirements. In this case there are several possible ways to get missing functionality, for example, use third-party libraries or create your own custom view components. In this tutorial I am going to explain the latter option. It is always a great idea to learn internals and understand what is going on under the hood.
Introduction
This tutorial is intended to explain how to create custom Radio Group and Radio Button components. There are lot of related topics addressed in this guide like Custom Views, Compound Views, View Groups. However, I won’t cover this topics in great details as far as there are lot of great tutorials out there. This tutorial supposes some basic knowledge of the Android Framework and the Java language. Furthermore you can find the link to the source code at the end of the article.
Step 1 — Creating a Custom Radio Button
First of all we need to find a design prototype to implement it in our custom Radio Button.
Design inspiration
I have found this great design prototype of the settings screen. Here you can see three radio buttons that are used as presets. This is a great choice from the UX perspective that lets a user easily choose from standard, in this case time, presets.
We are going to create a similar custom radio button, but in addition there will be an Edit Text view in order to enter custom values, however this is not related to the main purpose of the guide.
Creating a compound view
A compound view is a group of views or even view groups created for reusability and consistency. Furthermore such views could be enhanced programmatically as well.
Let’s start by creating the hierarchy of our compound radio button.
As you can see it is quite simple and contains only two Text View components.
Custom Radio Button XML File
To get started, create the /res/layout/custom_preset_button.xml XML file that will be used as the layout file for the compound view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/text_view_value"
style="@style/PresetLayoutButton_ValueText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_time_preset_30" />
<TextView
android:id="@+id/text_view_unit"
style="@style/PresetLayoutButton_UnitText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/unit_seconds"
android:layout_below="@+id/text_view_value" />
</merge>
|
Please make sure you are using the <merge> tag. This tag helps to eliminate redundant view groups when a file with the <merge> tag is included into another layout or used as the layout file for a compound view.
Implementing a custom Radio Button
Next, we need to create a compound view class that will inflate the created layout file. Let’s call it PresetValueButton. We will extend the Relative Layout view group, because it is the most appropriate view group for this purpose. Here is the final implementation, let’s discuss the most interesting parts.
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 | public class PresetValueButton extends RelativeLayout implements RadioCheckable {
// Views
private TextView mValueTextView, mUnitTextView;
// Constants
public static final int DEFAULT_TEXT_COLOR = Color.TRANSPARENT;
// Attribute Variables
private String mValue;
private String mUnit;
private int mValueTextColor;
private int mUnitTextColor;
private int mPressedTextColor;
// Variables
private Drawable mInitialBackgroundDrawable;
private OnClickListener mOnClickListener;
private OnTouchListener mOnTouchListener;
private boolean mChecked;
private ArrayList<OnCheckedChangeListener> mOnCheckedChangeListeners = new ArrayList<>();
//================================================================================
// Constructors
//================================================================================
public PresetValueButton(Context context) {
super(context);
setupView();
}
public PresetValueButton(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttributes(attrs);
setupView();
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public PresetValueButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
parseAttributes(attrs);
setupView();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PresetValueButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
parseAttributes(attrs);
setupView();
}
//================================================================================
// Init & inflate methods
//================================================================================
private void parseAttributes(AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.PresetValueButton, 0, 0);
Resources resources = getContext().getResources();
try {
mValue = a.getString(R.styleable.PresetValueButton_presetButtonValueText);
mUnit = a.getString(R.styleable.PresetValueButton_presetButtonUnitText);
mValueTextColor = a.getColor(R.styleable.PresetValueButton_presetButtonValueTextColor, resources.getColor(R.color.black));
mPressedTextColor = a.getColor(R.styleable.PresetValueButton_presetButtonPressedTextColor, Color.WHITE);
mUnitTextColor = a.getColor(R.styleable.PresetValueButton_presetButtonUnitTextColor, resources.getColor(R.color.gray));
} finally {
a.recycle();
}
}
// Template method
private void setupView() {
inflateView();
bindView();
setCustomTouchListener();
}
protected void inflateView() {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.custom_preset_button, this, true);
mValueTextView = (TextView) findViewById(R.id.text_view_value);
mUnitTextView = (TextView) findViewById(R.id.text_view_unit);
mInitialBackgroundDrawable = getBackground();
}
protected void bindView() {
if (mUnitTextColor != DEFAULT_TEXT_COLOR) {
mUnitTextView.setTextColor(mUnitTextColor);
}
if (mValueTextColor != DEFAULT_TEXT_COLOR) {
mValueTextView.setTextColor(mValueTextColor);
}
mUnitTextView.setText(mUnit);
mValueTextView.setText(mValue);
}
//================================================================================
// Overriding default behavior
//================================================================================
@Override
public void setOnClickListener(@Nullable OnClickListener l) {
mOnClickListener = l;
}
private void setCustomTouchListener() {
super.setOnTouchListener(new TouchListener());
}
@Override
public void setOnTouchListener(OnTouchListener onTouchListener) {
mOnTouchListener = onTouchListener;
}
public OnTouchListener getOnTouchListener() {
return mOnTouchListener;
}
private void onTouchDown(MotionEvent motionEvent) {
setChecked(true);
}
private void onTouchUp(MotionEvent motionEvent) {
// Handle user defined click listeners
if (mOnClickListener != null) {
mOnClickListener.onClick(this);
}
}
//================================================================================
// Public methods
//================================================================================
public void setCheckedState() {
setBackgroundResource(R.drawable.background_shape_preset_button__pressed);
mValueTextView.setTextColor(mPressedTextColor);
mUnitTextView.setTextColor(mPressedTextColor);
}
public void setNormalState() {
setBackgroundDrawable(mInitialBackgroundDrawable);
mValueTextView.setTextColor(mValueTextColor);
mUnitTextView.setTextColor(mUnitTextColor);
}
public String getValue() {
return mValue;
}
public void setValue(String value) {
mValue = value;
}
public String getUnit() {
return mUnit;
}
public void setUnit(String unit) {
mUnit = unit;
}
//================================================================================
// Checkable implementation
//================================================================================
@Override
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
if (!mOnCheckedChangeListeners.isEmpty()) {
for (int i = 0; i < mOnCheckedChangeListeners.size(); i++) {
mOnCheckedChangeListeners.get(i).onCheckedChanged(this, mChecked);
}
}
if (mChecked) {
setCheckedState();
} else {
setNormalState();
}
}
}
@Override
public boolean isChecked() {
return mChecked;
}
@Override
public void toggle() {
setChecked(!mChecked);
}
@Override
public void addOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
mOnCheckedChangeListeners.add(onCheckedChangeListener);
}
@Override
public void removeOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
mOnCheckedChangeListeners.remove(onCheckedChangeListener);
}
//================================================================================
// Inner classes
//================================================================================
private final class TouchListener implements OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onTouchDown(event);
break;
case MotionEvent.ACTION_UP:
onTouchUp(event);
break;
}
if (mOnTouchListener != null) {
mOnTouchListener.onTouch(v, event);
}
return true;
}
}
}
|
We have defined the following custom attributes:
- presetButtonValueText – Value text
- presetButtonUnitText – Unit text
- presetButtonValueTextColor – Value text color
- presetButtonUnitTextColor – Unit text color
- presetButtonPressedTextColor – Text color for both labels for the pressed state
The RadioCheckable interface
In order to define common properties and methods that a radio button must have to be a part of a radio group create the following RadioCheckable interface.
1 2 3 4 5 6 7 8 | public interface RadioCheckable extends Checkable {
void addOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener);
void removeOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener);
public static interface OnCheckedChangeListener {
void onCheckedChanged(View radioGroup, boolean isChecked);
}
}
|
As you can see from the code above, our interface extends the Checkable interface defined in the Android Framework.
1 2 3 4 5 | public interface Checkable {
void setChecked(boolean checked);
boolean isChecked();
void toggle();
}
|
You may wonder what is the purpose of these two methods addOnCheckChangeListener and removeOnCheckChangeListener. Let’s see the implementation in the PresetValueButton 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 | @Override
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
if (!mOnCheckedChangeListeners.isEmpty()) {
for (int i = 0; i < mOnCheckedChangeListeners.size(); i++) {
mOnCheckedChangeListeners.get(i).onCheckedChanged(this, mChecked);
}
}
if (mChecked) {
setCheckedState();
} else {
setNormalState();
}
}
}
@Override
public boolean isChecked() {
return mChecked;
}
@Override
public void toggle() {
setChecked(!mChecked);
}
@Override
public void addOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
mOnCheckedChangeListeners.add(onCheckedChangeListener);
}
@Override
public void removeOnCheckChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
mOnCheckedChangeListeners.remove(onCheckedChangeListener);
}
|
I’ve decided to use multiple listeners (add instead of set method), since the default implementation of the RadioButton class contains two listeners, one is available to a user and the second one is used just by the parent RadiouGroup class. Have a look at this snippet from the CompoundButton class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
// Avoid infinite recursions if setChecked() is called from a listener
if (mBroadcasting) {
return;
}
mBroadcasting = true;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
}
if (mOnCheckedChangeWidgetListener != null) {
mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
}
mBroadcasting = false;
}
}
|
Handling click and touch events
As you may have already noticed click and touch events are handled in the specific way.
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 | @Override
public void setOnClickListener(@Nullable OnClickListener l) {
mOnClickListener = l;
}
protected void setCustomTouchListener() {
super.setOnTouchListener(new TouchListener());
}
@Override
public void setOnTouchListener(OnTouchListener onTouchListener) {
mOnTouchListener = onTouchListener;
}
public OnTouchListener getOnTouchListener() {
return mOnTouchListener;
}
private void onTouchDown(MotionEvent motionEvent) {
setChecked(true);
}
private void onTouchUp(MotionEvent motionEvent) {
// Handle user defined click listeners
if (mOnClickListener != null) {
mOnClickListener.onClick(this);
}
}
private final class TouchListener implements OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onTouchDown(event);
break;
case MotionEvent.ACTION_UP:
onTouchUp(event);
break;
}
if (mOnTouchListener != null) {
mOnTouchListener.onTouch(v, event);
}
return true;
}
}
|
This decision is the result of the peculiarity of handling click events and appearance of a button. When a user clicks on a radio button the text color should be set to presetButtonPressedTextColor and then reset to the normal state on the up action. Therefore this is handled manually, in addition if user has set its own touch handler it will be called after the switch statement.
Step 2 — Creating a Custom Radio Group
Now it is time to create a custom radio group. A lot of ideas and implementation details are taken from the default RadioGroup class. Our custom radio group class extends the LinearLayout view group as it is the most suitable container for this purpose.
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 | public class PresetRadioGroup extends LinearLayout {
// Attribute Variables
private int mCheckedId = View.NO_ID;
private boolean mProtectFromCheckedChange = false;
// Variables
private OnCheckedChangeListener mOnCheckedChangeListener;
private HashMap<Integer, View> mChildViewsMap = new HashMap<>();
private PassThroughHierarchyChangeListener mPassThroughListener;
private RadioCheckable.OnCheckedChangeListener mChildOnCheckedChangeListener;
//================================================================================
// Constructors
//================================================================================
public PresetRadioGroup(Context context) {
super(context);
setupView();
}
public PresetRadioGroup(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttributes(attrs);
setupView();
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public PresetRadioGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
parseAttributes(attrs);
setupView();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PresetRadioGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
parseAttributes(attrs);
setupView();
}
//================================================================================
// Init & inflate methods
//================================================================================
private void parseAttributes(AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.PresetRadioGroup, 0, 0);
try {
mCheckedId = a.getResourceId(R.styleable.PresetRadioGroup_presetRadioCheckedId, View.NO_ID);
} finally {
a.recycle();
}
}
// Template method
private void setupView() {
mChildOnCheckedChangeListener = new CheckedStateTracker();
mPassThroughListener = new PassThroughHierarchyChangeListener();
super.setOnHierarchyChangeListener(mPassThroughListener);
}
//================================================================================
// Overriding default behavior
//================================================================================
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (child instanceof RadioCheckable) {
final RadioCheckable button = (RadioCheckable) child;
if (button.isChecked()) {
mProtectFromCheckedChange = true;
if (mCheckedId != View.NO_ID) {
setCheckedStateForView(mCheckedId, false);
}
mProtectFromCheckedChange = false;
setCheckedId(child.getId(), true);
}
}
super.addView(child, index, params);
}
@Override
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
// the user listener is delegated to our pass-through listener
mPassThroughListener.mOnHierarchyChangeListener = listener;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// checks the appropriate radio button as requested in the XML file
if (mCheckedId != View.NO_ID) {
mProtectFromCheckedChange = true;
setCheckedStateForView(mCheckedId, true);
mProtectFromCheckedChange = false;
setCheckedId(mCheckedId, true);
}
}
private void setCheckedId(@IdRes int id, boolean isChecked) {
mCheckedId = id;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mChildViewsMap.get(id), isChecked, mCheckedId);
}
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public void clearCheck() {
check(View.NO_ID);
}
public void check(@IdRes int id) {
// don't even bother
if (id != View.NO_ID && (id == mCheckedId)) {
return;
}
if (mCheckedId != View.NO_ID) {
setCheckedStateForView(mCheckedId, false);
}
if (id != View.NO_ID) {
setCheckedStateForView(id, true);
}
setCheckedId(id, true);
}
private void setCheckedStateForView(int viewId, boolean checked) {
View checkedView;
checkedView = mChildViewsMap.get(viewId);
if (checkedView == null) {
checkedView = findViewById(viewId);
if (checkedView != null) {
mChildViewsMap.put(viewId, checkedView);
}
}
if (checkedView != null && checkedView instanceof RadioCheckable) {
((RadioCheckable) checkedView).setChecked(checked);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
//================================================================================
// Public methods
//================================================================================
public void setOnCheckedChangeListener(OnCheckedChangeListener onCheckedChangeListener) {
mOnCheckedChangeListener = onCheckedChangeListener;
}
public OnCheckedChangeListener getOnCheckedChangeListener() {
return mOnCheckedChangeListener;
}
//================================================================================
// Nested classes
//================================================================================
public static interface OnCheckedChangeListener {
void onCheckedChanged(View radioGroup, View radioButton, boolean isChecked, int checkedId);
}
//================================================================================
// Inner classes
//================================================================================
private class CheckedStateTracker implements RadioCheckable.OnCheckedChangeListener {
@Override
public void onCheckedChanged(View buttonView, boolean isChecked) {
// prevents from infinite recursion
if (mProtectFromCheckedChange) {
return;
}
mProtectFromCheckedChange = true;
if (mCheckedId != View.NO_ID) {
setCheckedStateForView(mCheckedId, false);
}
mProtectFromCheckedChange = false;
int id = buttonView.getId();
setCheckedId(id, true);
}
}
private class PassThroughHierarchyChangeListener implements
ViewGroup.OnHierarchyChangeListener {
private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
/**
* {@inheritDoc}
*/
public void onChildViewAdded(View parent, View child) {
if (parent == PresetRadioGroup.this && child instanceof RadioCheckable) {
int id = child.getId();
// generates an id if it's missing
if (id == View.NO_ID) {
id = ViewUtils.generateViewId();
child.setId(id);
}
((RadioCheckable) child).addOnCheckChangeListener(
mChildOnCheckedChangeListener);
mChildViewsMap.put(id, child);
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
/**
* {@inheritDoc}
*/
public void onChildViewRemoved(View parent, View child) {
if (parent == PresetRadioGroup.this && child instanceof RadioCheckable) {
((RadioCheckable) child).removeOnCheckChangeListener(mChildOnCheckedChangeListener);
}
mChildViewsMap.remove(child.getId());
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
}
|
Listeners
There are three listeners defined in the class:
- OnCheckedChangeListener: Listener that is set by a user in order to listen for check change events.
- PassThroughHierarchyChangeListener: This listener is used the internal purpose only. The radio group is notified when a child view is added or removed.
- RadioCheckable.OnCheckedChangeListener: This listener is assigned to a child radio button when it is added.
Let’s look closer at the PassThroughHierarchyChangeListener implementation.
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 | private class PassThroughHierarchyChangeListener implements
ViewGroup.OnHierarchyChangeListener {
private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
public void onChildViewAdded(View parent, View child) {
if (parent == PresetRadioGroup.this && child instanceof RadioCheckable) {
int id = child.getId();
// generates an id if it's missing
if (id == View.NO_ID) {
id = ViewUtils.generateViewId();
child.setId(id);
}
((RadioCheckable) child).addOnCheckChangeListener(
mChildOnCheckedChangeListener);
mChildViewsMap.put(id, child);
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
public void onChildViewRemoved(View parent, View child) {
if (parent == PresetRadioGroup.this && child instanceof RadioCheckable) {
((RadioCheckable) child).removeOnCheckChangeListener(mChildOnCheckedChangeListener);
}
mChildViewsMap.remove(child.getId());
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
|
As it has been just mentioned, on line 12 mChildOnCheckedChangeListener is added to a child view, hence the radio group will be notified about child views click/change events. On line 14 we are adding a child view to the hash map in order to cache child views, as a result we do not need to call the findViewById method whenever we need a child view.
1 2 3 4 5 6 7 8 9 10 11 12 13 | private void setCheckedStateForView(int viewId, boolean checked) {
View checkedView;
checkedView = mChildViewsMap.get(viewId);
if (checkedView == null) {
checkedView = findViewById(viewId);
if (checkedView != null) {
mChildViewsMap.put(viewId, checkedView);
}
}
if (checkedView != null && checkedView instanceof RadioCheckable) {
((RadioCheckable) checkedView).setChecked(checked);
}
}
|
It is worthwhile to mention that we defined the custom attribute for presetting id in the XML file, similarly to the default RadioGroup implementation. Therefore you can set a view id in the XML layout file to make this view checked by default.
1 2 3 4 5 6 7 8 9 10 11 | @Override
protected void onFinishInflate() {
super.onFinishInflate();
// checks the appropriate radio button as requested in the XML file
if (mCheckedId != View.NO_ID) {
mProtectFromCheckedChange = true;
setCheckedStateForView(mCheckedId, true);
mProtectFromCheckedChange = false;
setCheckedId(mCheckedId, true);
}
}
|
Step 3 — Using custom components
Layout file example
Finally, let’s test our custom components. Create the following XML layout file.
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 | <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context="net.crosp.customradiobtton.MainActivity">
<net.crosp.customradiobtton.PresetRadioGroup
android:id="@+id/preset_time_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_margin="3dp"
android:layout_marginBottom="13dp"
android:orientation="horizontal"
android:weightSum="3"
app:presetRadioCheckedId="@+id/preset_time_value_button_60">
<net.crosp.customradiobtton.PresetValueButton
android:id="@+id/preset_time_value_button_30"
style="@style/PresetLayoutButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:presetButtonUnitText="@string/unit_seconds"
app:presetButtonValueText="@string/title_time_preset_30" />
<net.crosp.customradiobtton.PresetValueButton
android:id="@+id/preset_time_value_button_60"
style="@style/PresetLayoutButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:presetButtonUnitText="@string/unit_seconds"
app:presetButtonValueText="@string/title_time_preset_60" />
<net.crosp.customradiobtton.PresetValueButton
android:id="@+id/preset_time_value_button_120"
style="@style/PresetLayoutButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
app:presetButtonUnitText="@string/unit_seconds"
app:presetButtonValueText="@string/title_time_preset_120" />
</net.crosp.customradiobtton.PresetRadioGroup>
<android.support.v7.widget.AppCompatEditText
android:id="@+id/edit_text_set_duration"
style="@style/SettingsEditText"
android:layout_below="@+id/preset_time_radio_group"
android:hint="@string/title_settings_set_duration"
android:imeOptions="actionNext"
android:inputType="number"
android:text="30" />
</RelativeLayout>
|
Values and styles
As you may noticed, there a lot of resources (strings, dimensions, styles) defined in external files (in the res/values folder). You can find all these values in the complete project source code at the end of the article.
Handling check changes
In order to get notified about check change events register the OnCheckedChangeListener on the radio group.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class MainActivity extends AppCompatActivity {
PresetRadioGroup mSetDurationPresetRadioGroup;
AppCompatEditText mSetDurationEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSetDurationPresetRadioGroup = (PresetRadioGroup) findViewById(R.id.preset_time_radio_group);
mSetDurationEditText = (AppCompatEditText) findViewById(R.id.edit_text_set_duration);
mSetDurationPresetRadioGroup.setOnCheckedChangeListener(new PresetRadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(View radioGroup, View radioButton, boolean isChecked, int checkedId) {
mSetDurationEditText.setText(((PresetValueButton) radioButton).getValue());
mSetDurationEditText.setSelection(mSetDurationEditText.getText().length());
}
});
}
}
|
Running the application
If you run the application you should get the similar UI on a device.
Verify whether it works by clicking on preset buttons.
Conclusion
The purpose of this tutorial is to show how easily you can create custom components and compound views. Furthermore, there is nothing complicated about default view components, therefore don’t be afraid to look into implementation. Also I encourage you to have a look at RadioGroup and RadioButton classes and compare them with the implemented in this tutorial.
Recent posts
- Mar 1, 2020 Implementing Laravel custom Auth Guard and Provider
- Feb 16, 2019 Hacking Java Applications with Byte Buddy and Decompilers
- Jan 5, 2019 Page Specific Dynamic Angular Components using Child Routes
- Oct 13, 2018 Understanding Dagger 2 Scopes Under The Hood
- Jul 21, 2018 Understanding and using Xdebug with PHPStorm and Magento remotely
Popular posts
- 139702 Views How to Install The Latest Apache Server (httpd) on Centos 7
- 101054 Views Routing network traffic through a transparent SOCKS5 proxy using DD-WRT
- 72335 Views How to Unbrick TP-Link WiFi Router WR841ND using TFTP and Wireshark
- 71667 Views Android Reverse Engineering: Debugging Smali in Smalidea
- 59138 Views Clean Architecture : Part 2 – The Clean Architecture