Publicado en Android

Grupo de elementos selecionables personalizados para Android


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

Autor:

Desarrollador freelance programador apasionado por el arte de programar, amante del auto aprendizaje y interesado por la tecnología en general.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .