diff --git a/lib/java/com/google/android/material/chip/Chip.java b/lib/java/com/google/android/material/chip/Chip.java
index 6259e2d386b..1f63508cd32 100644
--- a/lib/java/com/google/android/material/chip/Chip.java
+++ b/lib/java/com/google/android/material/chip/Chip.java
@@ -18,6 +18,7 @@
import com.google.android.material.R;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
import android.annotation.SuppressLint;
@@ -64,6 +65,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.view.ViewCompat;
@@ -73,6 +75,7 @@
import androidx.customview.widget.ExploreByTouchHelper;
import com.google.android.material.animation.MotionSpec;
import com.google.android.material.chip.ChipDrawable.Delegate;
+import com.google.android.material.internal.MaterialCheckable;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.resources.MaterialResources;
@@ -112,8 +115,8 @@
*
*
*
You can register a listener on the main chip with {@link #setOnClickListener(OnClickListener)}
- * or {@link #setOnCheckedChangeListener(OnCheckedChangeListener)}. You can register a listener on
- * the close icon with {@link #setOnCloseIconClickListener(OnClickListener)}.
+ * or {@link #setOnCheckedChangeListener(AppCompatCheckBox.OnCheckedChangeListener)}. You can
+ * register a listener on the close icon with {@link #setOnCloseIconClickListener(OnClickListener)}.
*
*
For proper rendering of the ancestor TextView in RTL mode, call {@link
* #setLayoutDirection(int)} with View.LAYOUT_DIRECTION_LOCALE
. By default, TextView's
@@ -123,7 +126,8 @@
*
* @see ChipDrawable
*/
-public class Chip extends AppCompatCheckBox implements Delegate, Shapeable {
+public class Chip extends AppCompatCheckBox
+ implements Delegate, Shapeable, MaterialCheckable {
private static final String TAG = "Chip";
@@ -147,7 +151,7 @@ public class Chip extends AppCompatCheckBox implements Delegate, Shapeable {
@Nullable private RippleDrawable ripple;
@Nullable private OnClickListener onCloseIconClickListener;
- @Nullable private OnCheckedChangeListener onCheckedChangeListenerInternal;
+ @Nullable private MaterialCheckable.OnCheckedChangeListener onCheckedChangeListenerInternal;
private boolean deferredCheckedValue;
private boolean closeIconPressed;
private boolean closeIconHovered;
@@ -713,14 +717,6 @@ public void setChecked(boolean checked) {
}
}
- /**
- * Register a callback to be invoked when the checked state of this chip changes. This callback is
- * used for internal purpose only.
- */
- void setOnCheckedChangeListenerInternal(OnCheckedChangeListener listener) {
- onCheckedChangeListenerInternal = listener;
- }
-
/** Register a callback to be invoked when the close icon is clicked. */
public void setOnCloseIconClickListener(OnClickListener listener) {
this.onCloseIconClickListener = listener;
@@ -962,6 +958,14 @@ public PointerIcon onResolvePointerIcon(@NonNull MotionEvent event, int pointerI
return null;
}
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setInternalOnCheckedChangeListener(
+ @Nullable MaterialCheckable.OnCheckedChangeListener listener) {
+ onCheckedChangeListenerInternal = listener;
+ }
+
/** Provides a virtual view hierarchy for the close icon. */
private class ChipTouchHelper extends ExploreByTouchHelper {
diff --git a/lib/java/com/google/android/material/chip/ChipGroup.java b/lib/java/com/google/android/material/chip/ChipGroup.java
index c648d47a3d7..fbe54578270 100644
--- a/lib/java/com/google/android/material/chip/ChipGroup.java
+++ b/lib/java/com/google/android/material/chip/ChipGroup.java
@@ -27,7 +27,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.CompoundButton;
import androidx.annotation.BoolRes;
import androidx.annotation.DimenRes;
import androidx.annotation.Dimension;
@@ -37,10 +36,11 @@
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat;
+import com.google.android.material.internal.CheckableGroup;
import com.google.android.material.internal.FlowLayout;
import com.google.android.material.internal.ThemeEnforcement;
-import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* A ChipGroup is used to hold multiple {@link Chip}s. By default, the chips are reflowed across
@@ -52,12 +52,19 @@
* 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 {@link
* android.widget.RadioGroup}.
+ *
+ * When a chip is added to a chip group, its checked state will be preserved. If the chip group
+ * is in the single selection mode and there is an existing checked chip when another checked chip
+ * is added, the existing checked chip will be unchecked to maintain the single selection rule.
*/
public class ChipGroup extends FlowLayout {
/**
* Interface definition for a callback to be invoked when the checked chip changed in this group.
+ *
+ * @deprecated Use {@link OnCheckedStateChangeListener} instead.
*/
+ @Deprecated
public interface OnCheckedChangeListener {
/**
* Called when the checked chip has changed. When the selection is cleared, checkedId is {@link
@@ -66,7 +73,22 @@ public interface OnCheckedChangeListener {
* @param group the group in which the checked chip has changed
* @param checkedId the unique identifier of the newly checked chip
*/
- public void onCheckedChanged(ChipGroup group, @IdRes int checkedId);
+ void onCheckedChanged(@NonNull ChipGroup group, @IdRes int checkedId);
+ }
+
+ /**
+ * Interface definition for a callback which supports multiple checked IDs to be invoked when the
+ * checked chips changed in this group.
+ */
+ public interface OnCheckedStateChangeListener {
+ /**
+ * Called when the checked chips are changed. When the selection is cleared, {@code checkedIds}
+ * will be an empty list.
+ *
+ * @param group the group in which the checked chip has changed
+ * @param checkedIds the unique identifier list of the newly checked chips
+ */
+ void onCheckedChanged(@NonNull ChipGroup group, @NonNull List checkedIds);
}
/** A {@link ChipGroup.LayoutParams} implementation for {@link ChipGroup}. */
@@ -92,20 +114,16 @@ public LayoutParams(MarginLayoutParams source) {
@Dimension private int chipSpacingHorizontal;
@Dimension private int chipSpacingVertical;
- private boolean singleSelection;
- private boolean selectionRequired;
- @Nullable private OnCheckedChangeListener onCheckedChangeListener;
+ @Nullable private OnCheckedStateChangeListener onCheckedStateChangeListener;
- private final CheckedStateTracker checkedStateTracker = new CheckedStateTracker();
+ private final CheckableGroup checkableGroup = new CheckableGroup<>();
+ private final int defaultCheckedId;
@NonNull
- private PassThroughHierarchyChangeListener passThroughListener =
+ private final PassThroughHierarchyChangeListener passThroughListener =
new PassThroughHierarchyChangeListener();
- @IdRes private int checkedId = View.NO_ID;
- private boolean protectFromCheckedChange = false;
-
public ChipGroup(Context context) {
this(context, null);
}
@@ -131,12 +149,21 @@ public ChipGroup(Context context, AttributeSet attrs, int defStyleAttr) {
setSingleLine(a.getBoolean(R.styleable.ChipGroup_singleLine, false));
setSingleSelection(a.getBoolean(R.styleable.ChipGroup_singleSelection, false));
setSelectionRequired(a.getBoolean(R.styleable.ChipGroup_selectionRequired, false));
- int checkedChip = a.getResourceId(R.styleable.ChipGroup_checkedChip, View.NO_ID);
- if (checkedChip != View.NO_ID) {
- checkedId = checkedChip;
- }
+ defaultCheckedId = a.getResourceId(R.styleable.ChipGroup_checkedChip, View.NO_ID);
a.recycle();
+
+ checkableGroup.setOnCheckedStateChangeListener(
+ new CheckableGroup.OnCheckedStateChangeListener() {
+ @Override
+ public void onCheckedStateChanged(Set checkedIds) {
+ if (onCheckedStateChangeListener != null) {
+ onCheckedStateChangeListener.onCheckedChanged(
+ ChipGroup.this,
+ checkableGroup.getCheckedIdsSortedByChildOrder(ChipGroup.this));
+ }
+ }
+ });
super.setOnHierarchyChangeListener(passThroughListener);
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -192,25 +219,9 @@ protected void onFinishInflate() {
super.onFinishInflate();
// checks the appropriate chip as requested in the XML file
- if (checkedId != View.NO_ID) {
- setCheckedStateForView(checkedId, true);
- setCheckedId(checkedId);
- }
- }
-
- @Override
- public void addView(View child, int index, ViewGroup.LayoutParams params) {
- if (child instanceof Chip) {
- final Chip chip = (Chip) child;
- if (chip.isChecked()) {
- if (checkedId != View.NO_ID && singleSelection) {
- setCheckedStateForView(checkedId, false);
- }
- setCheckedId(chip.getId());
- }
+ if (defaultCheckedId != View.NO_ID) {
+ checkableGroup.check(defaultCheckedId);
}
-
- super.addView(child, index, params);
}
/** @deprecated Use {@link ChipGroup#setChipSpacingHorizontal(int)} instead. */
@@ -261,19 +272,7 @@ public void setFlexWrap(int flexWrap) {
* @see #clearCheck()
*/
public void check(@IdRes int id) {
- if (id == checkedId) {
- return;
- }
-
- if (checkedId != View.NO_ID && singleSelection) {
- setCheckedStateForView(checkedId, false);
- }
-
- if (id != View.NO_ID) {
- setCheckedStateForView(id, true);
- }
-
- setCheckedId(id);
+ checkableGroup.check(id);
}
/**
* When in {@link #isSingleSelection() single selection mode}, returns the identifier of the
@@ -288,7 +287,7 @@ public void check(@IdRes int id) {
*/
@IdRes
public int getCheckedChipId() {
- return singleSelection ? checkedId : View.NO_ID;
+ return checkableGroup.getSingleCheckedId();
}
/**
@@ -304,20 +303,7 @@ public int getCheckedChipId() {
*/
@NonNull
public List getCheckedChipIds() {
- ArrayList checkedIds = new ArrayList<>();
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- if (child instanceof Chip) {
- if (((Chip) child).isChecked()) {
- checkedIds.add(child.getId());
- if (singleSelection) {
- return checkedIds;
- }
- }
- }
- }
-
- return checkedIds;
+ return checkableGroup.getCheckedIdsSortedByChildOrder(this);
}
/**
@@ -329,16 +315,7 @@ public List getCheckedChipIds() {
* @see #getCheckedChipIds()
*/
public void clearCheck() {
- protectFromCheckedChange = true;
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- if (child instanceof Chip) {
- ((Chip) child).setChecked(false);
- }
- }
- protectFromCheckedChange = false;
-
- setCheckedId(View.NO_ID);
+ checkableGroup.clearCheck();
}
/**
@@ -346,30 +323,35 @@ public void clearCheck() {
* only invoked in {@link #isSingleSelection() single selection mode}.
*
* @param listener the callback to call on checked state change
+ * @deprecated use {@link #setOnCheckedStateChangeListener(OnCheckedStateChangeListener)} instead.
*/
- public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
- onCheckedChangeListener = listener;
- }
-
- private void setCheckedId(int checkedId) {
- setCheckedId(checkedId, true);
- }
-
- private void setCheckedId(int checkedId, boolean fromUser) {
- this.checkedId = checkedId;
-
- if (onCheckedChangeListener != null && singleSelection && fromUser) {
- onCheckedChangeListener.onCheckedChanged(this, checkedId);
+ @Deprecated
+ public void setOnCheckedChangeListener(@Nullable final OnCheckedChangeListener listener) {
+ if (listener == null) {
+ setOnCheckedStateChangeListener(null);
+ return;
}
+ setOnCheckedStateChangeListener(
+ new OnCheckedStateChangeListener() {
+ @Override
+ public void onCheckedChanged(
+ @NonNull ChipGroup group, @NonNull List checkedIds) {
+ if (!checkableGroup.isSingleSelection()) {
+ return;
+ }
+ listener.onCheckedChanged(group, getCheckedChipId());
+ }
+ });
}
- private void setCheckedStateForView(@IdRes int viewId, boolean checked) {
- View checkedView = findViewById(viewId);
- if (checkedView instanceof Chip) {
- protectFromCheckedChange = true;
- ((Chip) checkedView).setChecked(checked);
- protectFromCheckedChange = false;
- }
+ /**
+ * Register a callback to be invoked when the checked chip changes in this group. This callback is
+ * only invoked in {@link #isSingleSelection() single selection mode}.
+ *
+ * @param listener the callback to call on checked state change
+ */
+ public void setOnCheckedStateChangeListener(@Nullable OnCheckedStateChangeListener listener) {
+ onCheckedStateChangeListener = listener;
}
private int getChipCount() {
@@ -476,7 +458,7 @@ public void setSingleLine(@BoolRes int id) {
/** Returns whether this chip group only allows a single chip to be checked. */
public boolean isSingleSelection() {
- return singleSelection;
+ return checkableGroup.isSingleSelection();
}
/**
@@ -485,11 +467,7 @@ public boolean isSingleSelection() {
* Calling this method results in all the chips in this group to become unchecked.
*/
public void setSingleSelection(boolean singleSelection) {
- if (this.singleSelection != singleSelection) {
- this.singleSelection = singleSelection;
-
- clearCheck();
- }
+ checkableGroup.setSingleSelection(singleSelection);
}
/**
@@ -508,7 +486,7 @@ public void setSingleSelection(@BoolRes int id) {
* @see #setSingleSelection(boolean)
*/
public void setSelectionRequired(boolean selectionRequired) {
- this.selectionRequired = selectionRequired;
+ checkableGroup.setSelectionRequired(selectionRequired);
}
/**
@@ -519,35 +497,7 @@ public void setSelectionRequired(boolean selectionRequired) {
* @see #setSelectionRequired(boolean)
*/
public boolean isSelectionRequired() {
- return selectionRequired;
- }
-
- private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
- @Override
- public void onCheckedChanged(@NonNull CompoundButton buttonView, boolean isChecked) {
- // prevents from infinite recursion
- if (protectFromCheckedChange) {
- return;
- }
-
- List checkedChipIds = getCheckedChipIds();
- if (checkedChipIds.isEmpty() && selectionRequired) {
- setCheckedStateForView(buttonView.getId(), true);
- setCheckedId(buttonView.getId(), false);
- return;
- }
-
- int id = buttonView.getId();
-
- if (isChecked) {
- if (checkedId != View.NO_ID && checkedId != id && singleSelection) {
- setCheckedStateForView(checkedId, false);
- }
- setCheckedId(id);
- } else if (checkedId == id) {
- setCheckedId(View.NO_ID);
- }
- }
+ return checkableGroup.isSelectionRequired();
}
/**
@@ -567,11 +517,7 @@ public void onChildViewAdded(View parent, View child) {
id = ViewCompat.generateViewId();
child.setId(id);
}
- Chip chip = ((Chip) child);
- if (chip.isChecked()) {
- ((ChipGroup) parent).check(chip.getId());
- }
- chip.setOnCheckedChangeListenerInternal(checkedStateTracker);
+ checkableGroup.addCheckable((Chip) child);
}
if (onHierarchyChangeListener != null) {
@@ -582,7 +528,7 @@ public void onChildViewAdded(View parent, View child) {
@Override
public void onChildViewRemoved(View parent, View child) {
if (parent == ChipGroup.this && child instanceof Chip) {
- ((Chip) child).setOnCheckedChangeListenerInternal(null);
+ checkableGroup.removeCheckable((Chip) child);
}
if (onHierarchyChangeListener != null) {
diff --git a/lib/java/com/google/android/material/internal/CheckableGroup.java b/lib/java/com/google/android/material/internal/CheckableGroup.java
new file mode 100644
index 00000000000..32633a3e05b
--- /dev/null
+++ b/lib/java/com/google/android/material/internal/CheckableGroup.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.material.internal;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import com.google.android.material.internal.MaterialCheckable.OnCheckedChangeListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A helper class to support check group logic.
+ *
+ * @hide
+ */
+@UiThread
+@RestrictTo(LIBRARY_GROUP)
+public class CheckableGroup> {
+ private final Map checkables = new HashMap<>();
+ private final Set checkedIds = new HashSet<>();
+
+ private OnCheckedStateChangeListener onCheckedStateChangeListener;
+ private boolean singleSelection;
+ private boolean selectionRequired;
+
+ public void setSingleSelection(boolean singleSelection) {
+ if (this.singleSelection != singleSelection) {
+ this.singleSelection = singleSelection;
+ clearCheck();
+ }
+ }
+
+ public boolean isSingleSelection() {
+ return singleSelection;
+ }
+
+ public void setSelectionRequired(boolean selectionRequired) {
+ this.selectionRequired = selectionRequired;
+ }
+
+ public boolean isSelectionRequired() {
+ return selectionRequired;
+ }
+
+ public void setOnCheckedStateChangeListener(@Nullable OnCheckedStateChangeListener listener) {
+ this.onCheckedStateChangeListener = listener;
+ }
+
+ public void addCheckable(T checkable) {
+ checkables.put(checkable.getId(), checkable);
+ if (checkable.isChecked()) {
+ checkInternal(checkable);
+ }
+ checkable.setInternalOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(T checkable, boolean isChecked) {
+ if (isChecked ? checkInternal(checkable) : uncheckInternal(checkable, selectionRequired)) {
+ onCheckedStateChanged();
+ }
+ }
+ });
+ }
+
+ public void removeCheckable(T checkable) {
+ checkable.setInternalOnCheckedChangeListener(null);
+ checkables.remove(checkable.getId());
+ checkedIds.remove(checkable.getId());
+ }
+
+ public void check(@IdRes int id) {
+ MaterialCheckable checkable = checkables.get(id);
+ if (checkable == null) {
+ return;
+ }
+ if (checkInternal(checkable)) {
+ onCheckedStateChanged();
+ }
+ }
+
+ public void uncheck(@IdRes int id) {
+ MaterialCheckable checkable = checkables.get(id);
+ if (checkable == null) {
+ return;
+ }
+ if (uncheckInternal(checkable, selectionRequired)) {
+ onCheckedStateChanged();
+ }
+ }
+
+ public void clearCheck() {
+ boolean checkedStateChanged = !checkedIds.isEmpty();
+ for (MaterialCheckable checkable : checkables.values()) {
+ uncheckInternal(checkable, false);
+ }
+ if (checkedStateChanged) {
+ onCheckedStateChanged();
+ }
+ }
+
+ @IdRes
+ public int getSingleCheckedId() {
+ return singleSelection && !checkedIds.isEmpty() ? checkedIds.iterator().next() : View.NO_ID;
+ }
+
+ @NonNull
+ public Set getCheckedIds() {
+ return new HashSet<>(checkedIds);
+ }
+
+ @NonNull
+ public List getCheckedIdsSortedByChildOrder(@NonNull ViewGroup parent) {
+ Set checkedIds = getCheckedIds();
+ List sortedCheckedIds = new ArrayList<>();
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+ if (child instanceof MaterialCheckable> && checkedIds.contains(child.getId())) {
+ sortedCheckedIds.add(child.getId());
+ }
+ }
+ return sortedCheckedIds;
+ }
+
+ private boolean checkInternal(@NonNull MaterialCheckable checkable) {
+ int id = checkable.getId();
+ if (checkedIds.contains(id)) {
+ return false;
+ }
+ MaterialCheckable singleCheckedItem = checkables.get(getSingleCheckedId());
+ if (singleCheckedItem != null) {
+ uncheckInternal(singleCheckedItem, false);
+ }
+ boolean checkedStateChanged = checkedIds.add(id);
+ if (!checkable.isChecked()) {
+ checkable.setChecked(true);
+ }
+ return checkedStateChanged;
+ }
+
+ private boolean uncheckInternal(
+ @NonNull MaterialCheckable checkable, boolean selectionRequired) {
+ int id = checkable.getId();
+ if (!checkedIds.contains(id)) {
+ return false;
+ }
+ if (selectionRequired && checkedIds.size() == 1 && checkedIds.contains(id)) {
+ // It's the only checked item, cannot be unchecked if selection is required
+ checkable.setChecked(true);
+ return false;
+ }
+ boolean checkedStateChanged = checkedIds.remove(id);
+ if (checkable.isChecked()) {
+ checkable.setChecked(false);
+ }
+ return checkedStateChanged;
+ }
+
+ private void onCheckedStateChanged() {
+ if (onCheckedStateChangeListener != null) {
+ onCheckedStateChangeListener.onCheckedStateChanged(getCheckedIds());
+ }
+ }
+
+ /**
+ * A listener interface for checked state changes.
+ */
+ public interface OnCheckedStateChangeListener {
+ void onCheckedStateChanged(@NonNull Set checkedIds);
+ }
+}
diff --git a/lib/java/com/google/android/material/internal/MaterialCheckable.java b/lib/java/com/google/android/material/internal/MaterialCheckable.java
new file mode 100644
index 00000000000..04d3cb730ef
--- /dev/null
+++ b/lib/java/com/google/android/material/internal/MaterialCheckable.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.material.internal;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.widget.Checkable;
+import androidx.annotation.IdRes;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+/**
+ * An custom checkable interface extending {@link Checkable} to support check group logic.
+ *
+ * @see CheckableGroup
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface MaterialCheckable> extends Checkable {
+ @IdRes int getId();
+
+ void setInternalOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener);
+
+ /**
+ * Interface definition for a callback to be invoked when a {@link MaterialCheckable} is checked
+ * or unchecked.
+ */
+ interface OnCheckedChangeListener {
+ /**
+ * Called when the checked state of a {@link MaterialCheckable} has changed.
+ *
+ * @param checkable The compound button view whose state has changed.
+ * @param isChecked The new checked state of buttonView.
+ */
+ void onCheckedChanged(C checkable, boolean isChecked);
+ }
+}
diff --git a/lib/javatests/com/google/android/material/chip/ChipGroupTest.java b/lib/javatests/com/google/android/material/chip/ChipGroupTest.java
index ea1916e7f1e..a0b43b679d9 100644
--- a/lib/javatests/com/google/android/material/chip/ChipGroupTest.java
+++ b/lib/javatests/com/google/android/material/chip/ChipGroupTest.java
@@ -29,7 +29,8 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import androidx.test.core.app.ApplicationProvider;
-import com.google.android.material.chip.ChipGroup.OnCheckedChangeListener;
+import com.google.android.material.chip.ChipGroup.OnCheckedStateChangeListener;
+import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,6 +45,7 @@ public class ChipGroupTest {
private static final int CHIP_GROUP_SPACING = 4;
private ChipGroup chipgroup;
private int checkedChangeCallCount;
+ private List checkedIds;
private final Context context = ApplicationProvider.getApplicationContext();
@Before
@@ -65,13 +67,13 @@ public void testSetChipSpacing() {
public void testSelection() {
chipgroup.setSingleSelection(true);
assertThat(chipgroup.isSingleSelection()).isTrue();
- assertThat(chipgroup.getCheckedChipId()).isEqualTo(View.NO_ID);
+ assertThat(chipgroup.getCheckedChipIds()).isEmpty();
int chipId = chipgroup.getChildAt(0).getId();
assertThat(chipId).isNotEqualTo(View.NO_ID);
chipgroup.check(chipId);
- assertThat(chipId).isEqualTo(chipgroup.getCheckedChipId());
+ assertThat(chipId).isEqualTo(chipgroup.getCheckedChipIds().get(0));
chipgroup.clearCheck();
- assertThat(chipgroup.getCheckedChipId()).isEqualTo(View.NO_ID);
+ assertThat(chipgroup.getCheckedChipIds()).isEmpty();
}
@Test
@@ -104,7 +106,7 @@ public void testSingleSelection_addingCheckedChipWithoutId() {
Chip chipNotChecked = new Chip(context);
chipgroup.addView(chipNotChecked);
assertThat(chipgroup.getCheckedChipIds()).hasSize(1);
- int checkedId = chipgroup.getCheckedChipId();
+ int checkedId = chipgroup.getCheckedChipIds().get(0);
assertThat(checkedId).isEqualTo(chipId);
// Add a checked Chip
@@ -115,7 +117,7 @@ public void testSingleSelection_addingCheckedChipWithoutId() {
int newChipId = chipChecked.getId();
assertThat(chipgroup.getCheckedChipIds()).hasSize(1);
- int checkedId2 = chipgroup.getCheckedChipId();
+ int checkedId2 = chipgroup.getCheckedChipIds().get(0);
assertThat(checkedId2).isEqualTo(newChipId);
}
@@ -137,12 +139,13 @@ public void singleSelection_withSelectionRequired_callsListenerOnce() {
chipgroup.setSingleSelection(true);
checkedChangeCallCount = 0;
- chipgroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(ChipGroup group, int checkedId) {
- checkedChangeCallCount++;
- }
- });
+ chipgroup.setOnCheckedStateChangeListener(
+ new OnCheckedStateChangeListener() {
+ @Override
+ public void onCheckedChanged(ChipGroup group, List checkedIds) {
+ checkedChangeCallCount++;
+ }
+ });
View chip = chipgroup.getChildAt(0);
chip.performClick();
@@ -163,6 +166,36 @@ public void singleSelection_withoutSelectionRequired_unSelects() {
assertThat(((Chip) chip).isChecked()).isFalse();
}
+ @Test
+ public void multipleSelection_callsListener() {
+ chipgroup.setSingleSelection(false);
+
+ chipgroup.setOnCheckedStateChangeListener(
+ new OnCheckedStateChangeListener() {
+ @Override
+ public void onCheckedChanged(ChipGroup group, List checkedIds) {
+ checkedChangeCallCount++;
+ ChipGroupTest.this.checkedIds = checkedIds;
+ }
+ });
+
+ View first = chipgroup.getChildAt(0);
+ View second = chipgroup.getChildAt(1);
+
+ first.performClick();
+
+ assertThat(checkedChangeCallCount).isEqualTo(1);
+ assertThat(checkedIds).hasSize(1);
+ assertThat(checkedIds).contains(first.getId());
+
+ second.performClick();
+
+ assertThat(checkedChangeCallCount).isEqualTo(2);
+ assertThat(checkedIds).hasSize(2);
+ assertThat(checkedIds).contains(first.getId());
+ assertThat(checkedIds).contains(second.getId());
+ }
+
@Test
public void multiSelection_withSelectionRequired_unSelectsIfTwo() {
chipgroup.setSingleSelection(false);