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