Ejemplo de cómo crear un grupo de elementos selecionalbes y personalizados para Android usando Kotlin.
El apunte sirve para armar una vista como la siguiente:
Prerequisitos
Primero de todo deberemos importar la depencia de GridLayout de AndroidX, en el gradle de nivel de modulo
implementation "androidx.gridlayout:gridlayout:1.0.0"
Componente RadioGridLayout
Componente que servirá para agrupar los radioButtons y separados por colunas, componente modificado del original de aquí
La modificación sirve para que cuando un radiobutton este deshabilitado le asigna un alpha de 0.33 para que se pueda diferenciar del otro
class RadioGridGroupLayout : GridLayout {
var checkedCheckableImageButtonId = -1
private set
private var mChildOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
private var mProtectFromCheckedChange = false
private var mOnCheckedChangeListener: OnCheckedChangeListener? = null
private var mPassThroughListener: PassThroughHierarchyChangeListener? = null
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
private fun init() {
mChildOnCheckedChangeListener = CheckedStateTracker()
mPassThroughListener = PassThroughHierarchyChangeListener()
super.setOnHierarchyChangeListener(mPassThroughListener)
}
override fun setOnHierarchyChangeListener(listener: OnHierarchyChangeListener?) {
mPassThroughListener!!.mOnHierarchyChangeListener = listener
}
override fun onFinishInflate() {
super.onFinishInflate()
if (checkedCheckableImageButtonId != -1) {
mProtectFromCheckedChange = true
setCheckedStateForView(checkedCheckableImageButtonId, true)
mProtectFromCheckedChange = false
setCheckedId(checkedCheckableImageButtonId)
}
}
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
if (child is AppCompatRadioButton) {
if (child.isChecked) {
mProtectFromCheckedChange = true
if (checkedCheckableImageButtonId != -1) {
setCheckedStateForView(checkedCheckableImageButtonId, false)
}
mProtectFromCheckedChange = false
setCheckedId(child.id)
}
}
super.addView(child, index, params)
}
private fun check(id: Int) {
if (id != -1 && id == checkedCheckableImageButtonId) {
return
}
if (checkedCheckableImageButtonId != -1) {
setCheckedStateForView(checkedCheckableImageButtonId, false)
}
if (id != -1) {
setCheckedStateForView(id, true)
}
setCheckedId(id)
}
fun setCheckedId(id: Int) {
checkedCheckableImageButtonId = id
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener?.onCheckedChanged(this, checkedCheckableImageButtonId)
}
}
private fun setCheckedStateForView(viewId: Int, checked: Boolean) {
val checkedView: View = findViewById(viewId)
if (checkedView is AppCompatRadioButton) {
checkedView.isChecked = checked
}
}
fun clearCheck() {
check(-1)
}
fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) {
mOnCheckedChangeListener = listener
}
override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
super.onInitializeAccessibilityEvent(event)
event.className = RadioGridGroupLayout::class.java.name
}
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(info)
info.className = RadioGridGroupLayout::class.java.name
}
interface OnCheckedChangeListener {
fun onCheckedChanged(group: RadioGridGroupLayout?, checkedId: Int)
}
private inner class CheckedStateTracker : CompoundButton.OnCheckedChangeListener {
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
if (mProtectFromCheckedChange) {
return
}
mProtectFromCheckedChange = true
if (checkedCheckableImageButtonId != -1) {
setCheckedStateForView(checkedCheckableImageButtonId, false)
}
mProtectFromCheckedChange = false
val id = buttonView.id
setCheckedId(id)
}
}
private inner class PassThroughHierarchyChangeListener :
OnHierarchyChangeListener {
var mOnHierarchyChangeListener: OnHierarchyChangeListener? = null
override fun onChildViewAdded(parent: View, child: View) {
if (parent === this@RadioGridGroupLayout && child is AppCompatRadioButton) {
var id: Int = child.getId()
// generates an id if it's missing
if (id == View.NO_ID) {
id = generateViewId()
child.setId(id)
}
if (!child.isEnabled) child.alpha = .5F
child.setOnCheckedChangeListener(
mChildOnCheckedChangeListener
)
}
mOnHierarchyChangeListener?.onChildViewAdded(parent, child)
}
override fun onChildViewRemoved(parent: View, child: View?) {
if (parent === this@RadioGridGroupLayout && child is AppCompatRadioButton) {
child.setOnCheckedChangeListener(null)
}
mOnHierarchyChangeListener?.onChildViewRemoved(parent, child)
}
}
companion object {
private val sNextGeneratedId: AtomicInteger = AtomicInteger(1)
fun generateViewId(): Int {
while (true) {
val result: Int = sNextGeneratedId.get()
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
var newValue = result + 1
if (newValue > 0x00FFFFFF) newValue = 1 // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result
}
}
}
}
}
Preparando graficos
Prepararemos las imagenes para mostrar, en mi caso tienen un tamaño cuadrado de 1024×1204 y luego crear un xml para poder modificar su tamaño para cada elemento, en este caso 128dpx128dp small_yoga1.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/yoga1"
android:width="128dp"
android:height="128dp">
</item>
</layer-list>
Implementar RadioGridLayout
A continuación añadir en el layout donde queremos mostrar el grupo de selección, para asignar la imágen en la parte superior se hace con la propiedad drawableTop se le asigna el elemento grafico con el tamaño 128×128 es decir del small_yoga1.xml
<com.webserveis.app.testradiogridlayout.RadioGridGroupLayout
android:id="@+id/gridRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:columnCount="2">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/option_1"
style="@style/GridRadioButton"
android:checked="true"
android:drawableTop="@drawable/small_yoga1"
android:text="Beginner essentials"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/option_2"
style="@style/GridRadioButton"
android:drawableTop="@drawable/small_yoga2"
android:text="Intermediate essentials"
app:layout_columnWeight="1"
app:layout_rowWeight="1"
tools:checked="true" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/option_3"
style="@style/GridRadioButton"
android:drawableTop="@drawable/small_yoga3"
android:text="Advanced essentials"
app:layout_columnWeight="1"
app:layout_rowWeight="1"
tools:checked="true" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/option_4"
style="@style/GridRadioButton"
android:enabled="false"
android:text="Sun salutations"
app:drawableTopCompat="@drawable/small_yoga4"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
</com.webserveis.app.testradiogridlayout.RadioGridGroupLayout>
Definición de estilo
Para personalizar el componente lo haremos mediante la declaración de un estilo en styles.xml
<style name="GridRadioButton">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">10dp</item>
<item name="android:background">@drawable/button_option_background_selector</item>
<item name="android:drawablePadding">16dp</item>
<item name="android:button">@null</item>
<item name="android:gravity">center_horizontal</item>
<item name="android:padding">20dp</item>
<item name="android:textColor">@drawable/button_option_text_color_selector</item>
<item name="android:textSize">14sp</item>
</style>
Selectores
Selectores para alternar los colores de la selección, de base obtiene los establecidos del tema
button_option_background_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/square_round_on" android:state_checked="true" />
<item android:drawable="@drawable/square_round_off" android:state_checked="false" />
</selector>
button_option_text_color_selector.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?android:colorControlActivated" android:state_checked="true" />
<item android:color="?android:colorControlNormal" android:state_checked="false" />
</selector>
Recursos
Dejo el código usado en un gist