ChipGroup single selection

21,707

Solution 1

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

Solution 2

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.

Solution 3

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);
    }
});

Solution 4

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)
        }
    }

}

Solution 5

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.

Share:
21,707

Related videos on Youtube

adriennoir
Author by

adriennoir

Updated on April 22, 2021

Comments

  • adriennoir
    adriennoir about 3 years

    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.

  • Todd DeLand
    Todd DeLand over 5 years
    There's a slight error in this code. chipGroup.getChildAt() takes an index, not the resource id of the chip view.
  • adriennoir
    adriennoir over 5 years
    @ToddDeLand: That's right. I forgot to mention that in my use case I added Chips to a ChipGroup programmatically according to some other container so resource ids weren't helpful. I used chip.setId(i++) before adding them to the group.
  • ror
    ror about 5 years
    As weird as it sounds this is so far the only solution I found too (with correction of using id not indexes).
  • Joseph Paddy
    Joseph Paddy almost 5 years
    Chip chip = chipGroup.findViewById(chipGroup.getCheckedChipId());
  • AlgoRyan
    AlgoRyan almost 5 years
    The OP asked for a way to ensure that at least one Chip is always selected. This answer points to functionality they were already aware of in their question, and doesn't meet their needs.
  • AlgoRyan
    AlgoRyan almost 5 years
    Doesn't this cause the previously clicked Chip to be checked each time, rather than the one the user actually clicked?
  • mhashim6
    mhashim6 almost 5 years
    No, when The user clicks a chip, the chip group updates the selection automatically. All this code does is that it stores the most recent selection in case there was no item being currently selected, as chipgroup allows that.
  • Joaquim Ley
    Joaquim Ley almost 5 years
    Thanks for pointing out but downvoting seems a bit excessive, I've added the working solution now.
  • AlgoRyan
    AlgoRyan almost 5 years
    Fair enough, now that there's a working solution I'll remove the downvote. :) You still have a typo in group.check(lastCheckId) though.
  • AlgoRyan
    AlgoRyan almost 5 years
    Also, now that I think about it, doesn't calling group.check() cause this listener to be called again, redundantly? It's only one extra call, due to some guard code in ChipGroup, but still.
  • AlgoRyan
    AlgoRyan almost 5 years
    The only thing I would highlight then is that, as with Joaquim's answer, this involves a single duplicate call to onCheckedChange() by calling chipGroup.check().
  • mhashim6
    mhashim6 almost 5 years
    Yeah I believe my answer was the simplest of all. But no one noticed it. 😅
  • Agapito Gallart Bernat
    Agapito Gallart Bernat over 4 years
    @JoaquimLey could you explain what is the return type return@setOnCheckedChangeListener.
  • Joaquim Ley
    Joaquim Ley over 4 years
    @AgapitoGallartiBernat it should be void/Unit, the @ setOnCheckedChangeListener is a Kotlin syntax, which refers to what statement you want to return at, in this case, the logic running for the listener lambda.
  • smdufb
    smdufb over 4 years
    This needs to get upvotes. The functionality no longer needs to be hacked like with the other answers.
  • FabioR
    FabioR over 4 years
    The only problem that I found is when I start the screen with a chip initially selected through XML. If you click it, it will deselect. After that, this solution works
  • FabioR
    FabioR over 4 years
    This was the only solution that worked for me, specially because there was a default value when the screen was rendered.