35

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.

Gabriele Mariotti
  • 192,671
  • 57
  • 469
  • 489
adriennoir
  • 970
  • 1
  • 10
  • 23

7 Answers7

70

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

Gabriele Mariotti
  • 192,671
  • 57
  • 469
  • 489
  • 2
    This needs to get upvotes. The functionality no longer needs to be hacked like with the other answers. – smdufb Dec 06 '19 at 21:01
17

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.

Joaquim Ley
  • 3,562
  • 2
  • 22
  • 41
  • 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 Jul 10 '19 at 05:18
  • Thanks for pointing out but downvoting seems a bit excessive, I've added the working solution now. – Joaquim Ley Jul 10 '19 at 10:16
  • 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 Jul 10 '19 at 11:15
  • 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 Jul 10 '19 at 11:19
  • @JoaquimLey could you explain what is the return type `return@setOnCheckedChangeListener`. – Agapito Gallart i Bernat Nov 02 '19 at 16:27
  • 1
    @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. – Joaquim Ley Nov 03 '19 at 13:42
13

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);
    }
});
adriennoir
  • 970
  • 1
  • 10
  • 23
  • There's a slight error in this code. chipGroup.getChildAt() takes an index, not the resource id of the chip view. – Todd DeLand Jan 18 '19 at 21:33
  • @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. – adriennoir Jan 19 '19 at 06:36
  • As weird as it sounds this is so far the only solution I found too (with correction of using id not indexes). – ror May 21 '19 at 17:13
  • 1
    Chip chip = chipGroup.findViewById(chipGroup.getCheckedChipId()); – Joseph Paddy Jun 26 '19 at 23:28
  • 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 Dec 13 '19 at 21:41
6

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

}
Todd DeLand
  • 2,807
  • 1
  • 25
  • 14
4

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.

toktorio
  • 61
  • 1
  • 5
1

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
mhashim6
  • 390
  • 4
  • 14
  • Doesn't this cause the previously clicked Chip to be checked each time, rather than the one the user actually clicked? – AlgoRyan Jul 10 '19 at 05:28
  • 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. – mhashim6 Jul 10 '19 at 07:20
  • 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()`. – AlgoRyan Jul 10 '19 at 11:31
  • Yeah I believe my answer was the simplest of all. But no one noticed it. – mhashim6 Jul 10 '19 at 11:31
  • This was the only solution that worked for me, specially because there was a default value when the screen was rendered. – FabioR Dec 16 '19 at 14:46
0

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()));
                }
            }
    });
zayn1991
  • 166
  • 10