android – ChipGroup single selection-ThrowExceptions

Exception or error:

How can I force a ChipGroup to act like a RadioGroup as in having at least one selected item always? Setting setSingleSelection(true) also adds the possibility to have nothing selected if you click twice on a Chip.

How to solve:

To prevent all chips from being deselected you can use the method setSelectionRequired:

chipGroup.setSelectionRequired(true)

You can also define it in the layout using the app:selectionRequired attribute:

<com.google.android.material.chip.ChipGroup
    app:singleSelection="true"
    app:selectionRequired="true"
    app:checkedChip="@id/..."
    ..>

Note: This requires a minimum of version 1.2.0-alpha02

###

A solution would be to preset a clicked chip and then toggling the clickable property of the chips:

chipGroup.setOnCheckedChangeListener((chipGroup, id) -> {
    Chip chip = ((Chip) chipGroup.getChildAt(chipGroup.getCheckedChipId()));
    if (chip != null) {
        for (int i = 0; i < chipGroup.getChildCount(); ++i) {
            chipGroup.getChildAt(i).setClickable(true);
        }
        chip.setClickable(false);
    }
});

###

EDIT

With version 1.2.0-alpha02 the old hacky solution is no longer required!

Either use the attribute app:selectionRequired="true"


<com.google.android.material.chip.ChipGroup
            android:id="@+id/group"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:selectionRequired="true"
            app:singleSelection="true">

  (...)
</com.google.android.material.chip.ChipGroup>

Or in code


// Kotlin
group.isSelectionRequired = true

// Java
group.setSelectionRequired(true);


For older versions 👇

There are two steps to achieve this

Step 1

We have this support built-in, just make sure to add app:singleSelection="true" to your ChipGroup, for example:

XML

<com.google.android.material.chip.ChipGroup
            android:id="@+id/group"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:singleSelection="true">

        <com.google.android.material.chip.Chip
                android:id="@+id/option_1"
                style="@style/Widget.MaterialComponents.Chip.Choice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Option 1" />

        <com.google.android.material.chip.Chip
                android:id="@+id/option_2"
                style="@style/Widget.MaterialComponents.Chip.Choice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Option 2" />
</com.google.android.material.chip.ChipGroup>

Code


// Kotlin
group.isSingleSelection = true

// Java
group.setSingleSelection(true);

Step 2

Now to support a radio group like functionality:


var lastCheckedId = View.NO_ID
chipGroup.setOnCheckedChangeListener { group, checkedId ->
    if(checkedId == View.NO_ID) {
        // User tried to uncheck, make sure to keep the chip checked          
        group.check(lastCheckedId)
        return@setOnCheckedChangeListener
    }
    lastCheckedId = checkedId

    // New selection happened, do your logic here.
    (...)

}

From the docs:

ChipGroup also supports a multiple-exclusion scope for a set of chips.
When you set the app:singleSelection attribute, checking one chip that
belongs to a chip group unchecks any previously checked chip within
the same group. The behavior mirrors that of RadioGroup.

###

Brief modification of @adriennoir ‘s answer (in Kotlin). Thanks for the help!
Note that getChildAt() takes an index.

for (i in 0 until group.childCount) {
    val chip = group.getChildAt(i)
    chip.isClickable = chip.id != group.checkedChipId
}

Here’s my larger `setOnCheckedChangeListener, for context:

intervalChipGroup.setOnCheckedChangeListener { group, checkedId ->

    for (i in 0 until group.childCount) {
        val chip = group.getChildAt(i)
        chip.isClickable = chip.id != group.checkedChipId
    }

    when (checkedId) {
        R.id.intervalWeek -> {
            view.findViewById<Chip>(R.id.intervalWeek).chipStrokeWidth = 1F
            view.findViewById<Chip>(R.id.intervalMonth).chipStrokeWidth = 0F
            view.findViewById<Chip>(R.id.intervalYear).chipStrokeWidth = 0F
            currentIntervalSelected = weekInterval
            populateGraph(weekInterval)
        }
        R.id.intervalMonth -> {
            view.findViewById<Chip>(R.id.intervalWeek).chipStrokeWidth = 0F
            view.findViewById<Chip>(R.id.intervalMonth).chipStrokeWidth = 1F
            view.findViewById<Chip>(R.id.intervalYear).chipStrokeWidth = 0F
            currentIntervalSelected = monthInterval
            populateGraph(monthInterval)

        }
        R.id.intervalYear -> {
            view.findViewById<Chip>(R.id.intervalWeek).chipStrokeWidth = 0F
            view.findViewById<Chip>(R.id.intervalMonth).chipStrokeWidth = 0F
            view.findViewById<Chip>(R.id.intervalYear).chipStrokeWidth = 1F
            currentIntervalSelected = yearInterval
            populateGraph(yearInterval)
        }
    }

}

###

Most of the answers are great and really helpful for me. Another slight modification to @adriennoir and @Todd DeLand, to prevent unchecking already checked chip in a setSingleSelection(true) ChipGroup, here’s my solution:

for (i in 0 until chipGroup.childCount) {
    val chip = chipGroup.getChildAt(i) as Chip
    chip.isCheckable = chip.id != chipGroup.checkedChipId
    chip.isChecked = chip.id == chipGroup.checkedChipId
}

For me, I just need to prevent the same checked Chip to be unchecked without making it non-clickable. This way, the user can still click the checked chip and see the fancy ripple effect and nothing will happen.

###

This is how I did it:

var previousSelection: Int = default_selection_id 
chipGroup.setOnCheckedChangeListener { chipGroup, id ->
    if (id == -1) //nothing is selected.
        chipGroup.check(previousSelection)
    else
        previousSelection = id

###

This is my working solution

mChipGroup.setOnCheckedChangeListener((group, checkedId) -> {
            for (int i = 0; i < mChipGroup.getChildCount(); i++) {
                Chip chip = (Chip) mChipGroup.getChildAt(i);
                if (chip != null) {
                    chip.setClickable(!(chip.getId() == mChipGroup.getCheckedChipId()));
                }
            }
    });

Leave a Reply

Your email address will not be published. Required fields are marked *