
The basics of radio buttons is to let the user have a bunch of options but only the possibility to select one of those options. So when a radio button is selected another radio button in the same group has to be deselected. This action always had me thinking if the little selection mark is just one which moves between the options? Or should they be considered physical buttons where the previously selected button pops out when a new option is pressed?
In this case I'm considering the first one, where there is only one selection which is moved between the options as I think it can be more fun graphically. Let's make Radio Buttons where the selection slides from its current position in the direction of the new position.
For this effect we will need to create a custom RadioGroup, a custom RadioButton, and the animations for the button transitions between selected states.
In the CustomRadioGroup a reference to which of its CustomRadioButtons is selected is needed. So just create a simple class which extends RadioGroup and includes a reference to the currently selected CustomRadioButton.
class CustomRadioGroup : RadioGroup {
lateinit var selectedRadioButton: CustomRadioButton
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(measuredWidth, measuredHeight)
}
}
Then for the CustomRadioButton we also want a reference to the CustomRadioGroup parent for easy access to the currently selected CustomRadioButton as well as an index to indicate where in the list of RadioButtons this button is placed. And we also want to override the toggle function to set the right animation for the newly and previous selected RadioButton.
class CustomRadioButton: AppCompatRadioButton {
var order = -1
lateinit var customRadioGroup: CustomRadioGroup
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(measuredWidth, measuredHeight)
}
/**
* Selects and sets the correct drawable if the previous selection was below or above the current one
*/
override fun toggle() {
if (slidingRadioGroup.selectedRadioButton.order < order) {
setButtonDrawable(R.drawable.ic_radio_up)
slidingRadioGroup.selectedRadioButton.setButtonDrawable(R.drawable.ic_radio_down)
} else {
setButtonDrawable(R.drawable.ic_radio_down)
slidingRadioGroup.selectedRadioButton.setButtonDrawable(R.drawable.ic_radio_up)
}
super.toggle()
}
}
In toggle we want to compare the order of the newly selected RadioButton (order) and the previously selected button (slidingRadioGroup.selectedRadioButton.order
). If the newly selected RadioButton is further down the list than the previous one, we want the selection to slide down in the previously select RadioButton and come sliding in to the center of the newly selected RadioButton from above. The opposite if the newly selected RadioButton comes before the previous one in the list.
Next up we have to create two animated-selectors for sliding up to selected, sliding from selected up, sliding down to selected and sliding from selected down. In ic_radio_down.xml
I keep the selection and deselection of a RadioButton when the animation come from the bottom or animates towards the bottom. And the opposite in ic_radio_up.xml
. Here is a zip-file of the drawable resources used in this animation. You can use them as a reference to create your own style.
ic_radio_down.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/unchecked_dis" android:drawable="@drawable/rb_deselected_disabled" android:state_checked="false" android:state_enabled="false"/>
<item android:id="@+id/checked_dis" android:drawable="@drawable/rb_selected_disabled" android:state_checked="true" android:state_enabled="false"/>
<item android:id="@+id/unchecked" android:drawable="@drawable/ic_radio_unchecked" android:state_checked="false" />
<item android:id="@+id/checked" android:drawable="@drawable/ic_radio_checked" android:state_checked="true"/>
<transition android:fromId="@+id/unchecked" android:toId="@+id/checked">
<animation-list>
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_6" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_5" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_4" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_3" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_2" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_1" />
<item android:duration="18" android:drawable="@drawable/ic_radio_checked" />
</animation-list>
</transition>
<transition android:fromId="@+id/checked" android:toId="@+id/unchecked">
<animation-list>
<item android:duration="18" android:drawable="@drawable/ic_radio_checked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_1" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_2" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_3" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_4" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_5" />
<item android:duration="18" android:drawable="@drawable/ic_radio_down_6" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
</animation-list>
</transition>
</animated-selector>
ic_radio_up.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/unchecked_dis" android:drawable="@drawable/rb_deselected_disabled" android:state_checked="false" android:state_enabled="false"/>
<item android:id="@+id/checked_dis" android:drawable="@drawable/rb_selected_disabled" android:state_checked="true" android:state_enabled="false"/>
<item android:id="@+id/unchecked" android:drawable="@drawable/ic_radio_unchecked" android:state_checked="false" />
<item android:id="@+id/checked" android:drawable="@drawable/ic_radio_checked" android:state_checked="true"/>
<transition android:fromId="@+id/unchecked" android:toId="@+id/checked">
<animation-list>
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_6" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_5" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_4" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_3" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_2" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_1" />
<item android:duration="18" android:drawable="@drawable/ic_radio_checked" />
</animation-list>
</transition>
<transition android:fromId="@+id/checked" android:toId="@+id/unchecked">
<animation-list>
<item android:duration="18" android:drawable="@drawable/ic_radio_checked" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_1" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_2" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_3" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_4" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_5" />
<item android:duration="18" android:drawable="@drawable/ic_radio_up_6" />
<item android:duration="18" android:drawable="@drawable/ic_radio_unchecked" />
</animation-list>
</transition>
</animated-selector>
As can be seen in both files, the animation from unchecked to checked has six images where there is no animation, this is to delay the entry of the selection to make it look like the selection mark had to travel a little bit before entering the newly selected radio button.
That's pretty much all you need to have sliding radio buttons. To use them just add the newly created CustomRadioGroup to your layout file, something similar to this.
<?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:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
...
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:fadeScrollbars="false">
<com.app.views.CustomRadioGroup
android:id="@+id/customRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="16dp"/>
</ScrollView>
...
</RelativeLayout>
Then all you have to do is to populate the CustomRadioGroup with some CustomRadioButtons.
customRadioGroup.setOnCheckedChangeListener(null)
customRadioGroup.removeAllViews()
customRadioGroup.selectedRadioButton = CustomRadioButton(this)
for ((index, option) in listOfOptions.withIndex()) {
val customRadioButton = CustomRadioButton(this)
customRadioButton.id = View.generateViewId()
customRadioButton.order = index
customRadioButton.slidingRadioGroup = customRadioGroup
customRadioButton.text = option.name
customRadioGroup.addView(customRadioButton)
}
customRadioGroup.setOnCheckedChangeListener { radioGroup, clickedButton ->
val customRadioButton: CustomRadioButton = radioGroup.findViewById(clickedButton)
if (customRadioButton.isChecked) {
customRadioGroup.selectedRadioButton = customRadioButton
}
}
The first two lines are only needed if you need to refresh the CustomRadioGroup with new buttons. The third line sets the default selection, which in this case is just an empty selection, but you could bake it in to the for loop. In the for loop it's pretty straight forward. Set an ID, order, the CustomRadioGroup, name and then add it to the customRadioGroup
parent.
Last we have to add a setOnCheckedChangeListener
to the radio group, get the RadioButton which just changed, and if its checked, set it as the currently selected CustomRadioButton in the CustomRadioGroup.