Search code examples
androidkotlinview

Dynamically access child views in custom ViewGroup Instance


I would like to create a Custom CardView where I can add additional child components when I create an instance as follows :

<com.example.app.CardComponent 
tools:context=".MainActivity"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title_text="title"
app:description_text="description"
app:clickable_button="true"
xmlns:android="http://schemas.android.com/apk/res/android">

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="testButton"/>

</com.example.app.CardComponent>

where CardComponent is the custom CardView I created and testButton an example of an additional view I'd like to insert in my CardComponent. I would like to insert it in this layer and not directly in the xml file of the CardComponent for customization purposes and reusability.

The problem is I don't know how to dynamically access child views of a custom viewgroup instance like this testButton in the CardComponent class in order to then dynamically add them to the layout, etc.

Here are the CardComponent class and xml file as well as the attr file for the CardComponent.

class CardComponent @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    MaterialCardView(context, attrs, defStyleAttr) {

    private val bindings: CardComponentBinding = CardComponentBinding.inflate(LayoutInflater.from(context), this, true)

    init {
        attrs?.let {

            val styledAttributes = context.obtainStyledAttributes(it, R.styleable.CardComponent)

            val titleText = styledAttributes.getString(R.styleable.CardComponent_title_text)
            bindings.cardTitle.text = titleText

            val descriptionText = styledAttributes.getString(R.styleable.CardComponent_description_text)
            bindings.cardDescription.text = descriptionText

            val clickableButton = styledAttributes.getBoolean(R.styleable.CardComponent_clickable_button, false)
            if (clickableButton){
                bindings.cardDescriptionButton.setOnClickListener{
                    val v = bindings.retractableLayout.visibility
                    bindings.retractableLayout.visibility = if(v == GONE) VISIBLE else GONE
                }
            }
            // Here I would have something like this but can't find a way to do it:
            // val button = styledAttributes.getChild()
            // bindings.relative_layout.addChild(button)

        }
    }
}

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:animateLayoutChanges="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        tools:layout_editor_absoluteX="1dp"
        tools:layout_editor_absoluteY="1dp">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:id="@+id/card_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentStart="true"
                android:layout_alignParentTop="true"
                android:padding="15dp"/>

            <Button
                android:id="@+id/card_description_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:padding="15dp"
                android:text="test"/>
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/retractable_layout"
            android:visibility="gone"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/card_description"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"/>
        </RelativeLayout>
    </LinearLayout>

</com.google.android.material.card.MaterialCardView>

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CardComponent">
        <attr name="title_text" format="string"/>
        <attr name="description_text" format="string"/>
        <attr name="clickable_button" format="boolean"/>
    </declare-styleable>
</resources>

Thank you for your help !


Solution

  • My solution was to use the OnFinishInflate method where I get the children of my CardComponent (1st picture), I delete their parent view and add them to the correct layout I'd set up in the xml file of my custom view (3rd picture). I eventually skip the first child because it's an instance of MaterialCardView (this child represents the custom Card I made, 3rd picture) and then take all the remaining children and add them to the good spot.

    private val bindings: CardComponentBinding = CardComponentBinding.inflate(layoutInflater, this, true)
    private var customComponentLayout: RelativeLayout = bindings.customComponentLayout
        
    override fun onFinishInflate() {
        val children = this.children.toList()
    
        for (child in children) {
            if (child is MaterialCardView) continue
            else {
                this.removeView(child)
                this.customComponentLayout.addView(child)
            }
        }
        super.onFinishInflate()
    }