'ChipGroup single selection

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.



Solution 1:[1]

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 2:[2]

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 3:[3]

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 4:[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.

Solution 5:[5]

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

Solution 6:[6]

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

Solution 7:[7]

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

Solution 8:[8]

The Bootstrap css framework requires SASS and SASS is a Node module. When you specify that you want to use Bootstrap, your project is switched to a ESBuild one. It is not possible to have Importmap and Bootstrap in the same Rails 7.0.0 project.

Have a look on the following files:

  • bin/dev
  • Procfile.dev
  • package.json

At this time, the only CSS framework that does not require Node is Tailwindcss

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 adriennoir
Solution 3 Todd DeLand
Solution 4 toktorio
Solution 5
Solution 6 zayn1991
Solution 7 WhoisAbel
Solution 8 Yanik Crépeau