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
.
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
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.
group.check(lastCheckId)
though. β
Potshot 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 return@setOnCheckedChangeListener
. β
Illuminating @ 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 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);
}
});
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.
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:
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
onCheckedChange()
by calling chipGroup.check()
. β
Potshot 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()));
}
}
});
© 2022 - 2024 β McMap. All rights reserved.