ChipGroup single selection
Asked Answered
L

8

53

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.

Lashandralashar answered 5/11, 2018 at 13:6 Comment(0)
W
115

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

Wrought answered 20/11, 2019 at 6:27 Comment(1)
This needs to get upvotes. The functionality no longer needs to be hacked like with the other answers. – Noel
M
24

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.

Macmullin answered 8/7, 2019 at 13:42 Comment(6)
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. – Potshot
Thanks for pointing out but downvoting seems a bit excessive, I've added the working solution now. – Macmullin
Fair enough, now that there's a working solution I'll remove the downvote. :) You still have a typo in group.check(lastCheckId) though. – Potshot
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. – Potshot
@JoaquimLey could you explain what is the return type return@setOnCheckedChangeListener. – Illuminating
@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. – Macmullin
L
14

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);
    }
});
Lashandralashar answered 5/11, 2018 at 15:53 Comment(6)
There's a slight error in this code. chipGroup.getChildAt() takes an index, not the resource id of the chip view. – Stowe
@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. – Lashandralashar
As weird as it sounds this is so far the only solution I found too (with correction of using id not indexes). – Annabelannabela
Chip chip = chipGroup.findViewById(chipGroup.getCheckedChipId()); – Singleton
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 – Delphinedelphinia
hipGroup.getChildAt() takes an index. you most use Joseph code Chip chip = chipGroup.findViewById(chipGroup.getCheckedChipId()); – Jejunum
S
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)
        }
    }

}
Stowe answered 18/1, 2019 at 21:39 Comment(0)
P
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.

Paladin answered 3/2, 2019 at 21:55 Comment(0)
T
2

If singleSelection doesn't work with added dynamically chips, you must generate id for each chip when create them and then add to ChipGroup.

val chip = inflater.inflate(
R.layout.item_crypto_currency_category_chip,
binding.chipGroupCryptoCurrencyCategory,
false) as Chip

chip.id = ViewCompat.generateViewId()

binding.chipGroupCryptoCurrencyCategory.addView(chip)

//Set default value with index 0 when ChipGroup created.
if (index == 0) binding.chipGroupCryptoCurrencyCategory.check(chip.id)

item_crypto_currency_category_chip.xml

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/chip_smart_contract"
style="@style/Widget.Signal.Chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

item_crypto_currency_tag_category.xml

<HorizontalScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/spacing_6x"
    android:scrollbars="none"
    app:layout_constraintTop_toTopOf="parent">

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

</HorizontalScrollView>

Result:

ChipGroup example

Toreutic answered 22/5, 2022 at 10:53 Comment(1)
I am doing the same but still, multiple chips are being selected. Can you please help me? – Finbur
P
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
Pear answered 24/1, 2019 at 14:9 Comment(5)
Doesn't this cause the previously clicked Chip to be checked each time, rather than the one the user actually clicked? – Potshot
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. – Pear
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(). – Potshot
Yeah I believe my answer was the simplest of all. But no one noticed it. πŸ˜… – Pear
This was the only solution that worked for me, specially because there was a default value when the screen was rendered. – Delphinedelphinia
P
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()));
                }
            }
    });
Pinnatiped answered 11/9, 2019 at 12:57 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.