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