/* * Copyright 2017 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.chip; import com.google.android.material.R; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.BoolRes; import androidx.annotation.DimenRes; import androidx.annotation.Dimension; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.List; import java.util.Set; /** * A ChipGroup is used to hold multiple {@link Chip}s. By default, the chips are reflowed across * multiple lines. Set the {@code app:singleLine} attribute to constrain the chips to a single * horizontal line. If you do so, you'll usually want to wrap this ChipGroup in a {@link * android.widget.HorizontalScrollView}. * *
ChipGroup also supports a multiple-exclusion scope for a set of chips. When you set the {@code * 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. * *
For more information, see the component
* developer guidance and design
* guidelines.
*/
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
* View#NO_ID}.
*
* @param group the group in which the checked chip has changed
* @param checkedId the unique identifier of the newly checked chip
*/
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 In {@link #isSingleSelection() single selection mode}, checking a chip also unchecks all
* others.
*
* @param id the unique id of the chip to select in this group
* @see #getCheckedChipId()
* @see #clearCheck()
*/
public void check(@IdRes int id) {
checkableGroup.check(id);
}
/**
* When in {@link #isSingleSelection() single selection mode}, returns the identifier of the
* selected chip in this group. Upon empty selection, the returned value is {@link View#NO_ID}. If
* not in single selection mode, the return value is {@link View#NO_ID}.
*
* @return the unique id of the selected chip in this group in single selection mode
* @see #check(int)
* @see #clearCheck()
* @see #getCheckedChipIds()
* @attr ref R.styleable#ChipGroup_checkedChip
*/
@IdRes
public int getCheckedChipId() {
return checkableGroup.getSingleCheckedId();
}
/**
* Returns the identifiers of the selected {@link Chip}s in this group. Upon empty selection, the
* returned value is an empty list.
*
* @return The unique IDs of the selected {@link Chip}s in this group. When in {@link
* #isSingleSelection() single selection mode}, returns a list with a single ID. When no
* {@link Chip}s are selected, returns an empty list.
* @see #check(int)
* @see #clearCheck()
* @see #getCheckedChipId()
*/
@NonNull
public List Non-Chip and non-visible children are ignored when computing the index.
*/
int getIndexOfChip(@Nullable View child) {
if (!(child instanceof Chip)) {
return -1;
}
int index = 0;
for (int i = 0; i < getChildCount(); i++) {
View current = getChildAt(i);
if (current instanceof Chip && isChildVisible(i)) {
Chip chip = (Chip) current;
if (chip == child) {
return index;
}
index++;
}
}
return -1;
}
private boolean isChildVisible(int i) {
return getChildAt(i).getVisibility() == VISIBLE;
}
/** Sets the horizontal and vertical spacing between visible chips in this group. */
public void setChipSpacing(@Dimension int chipSpacing) {
setChipSpacingHorizontal(chipSpacing);
setChipSpacingVertical(chipSpacing);
}
/** Sets the horizontal and vertical spacing between visible chips in this group. */
public void setChipSpacingResource(@DimenRes int id) {
setChipSpacing(getResources().getDimensionPixelOffset(id));
}
/** Returns the horizontal spacing between visible chips in this group. */
@Dimension
public int getChipSpacingHorizontal() {
return chipSpacingHorizontal;
}
/** Sets the horizontal spacing between visible chips in this group. */
public void setChipSpacingHorizontal(@Dimension int chipSpacingHorizontal) {
if (this.chipSpacingHorizontal != chipSpacingHorizontal) {
this.chipSpacingHorizontal = chipSpacingHorizontal;
setItemSpacing(chipSpacingHorizontal);
requestLayout();
}
}
/** Sets the horizontal spacing between visible chips in this group. */
public void setChipSpacingHorizontalResource(@DimenRes int id) {
setChipSpacingHorizontal(getResources().getDimensionPixelOffset(id));
}
/** Returns the vertical spacing between visible chips in this group. */
@Dimension
public int getChipSpacingVertical() {
return chipSpacingVertical;
}
/** Sets the vertical spacing between visible chips in this group. */
public void setChipSpacingVertical(@Dimension int chipSpacingVertical) {
if (this.chipSpacingVertical != chipSpacingVertical) {
this.chipSpacingVertical = chipSpacingVertical;
setLineSpacing(chipSpacingVertical);
requestLayout();
}
}
/** Sets the vertical spacing between visible chips in this group. */
public void setChipSpacingVerticalResource(@DimenRes int id) {
setChipSpacingVertical(getResources().getDimensionPixelOffset(id));
}
// Need to override here in order to un-restrict access to this method outside of the library.
@SuppressWarnings("RedundantOverride")
@Override
public boolean isSingleLine() {
return super.isSingleLine();
}
// Need to override here in order to un-restrict access to this method outside of the library.
@SuppressWarnings("RedundantOverride")
@Override
public void setSingleLine(boolean singleLine) {
super.setSingleLine(singleLine);
}
/** Sets whether this chip group is single line, or reflowed multiline. */
public void setSingleLine(@BoolRes int id) {
setSingleLine(getResources().getBoolean(id));
}
/** Returns whether this chip group only allows a single chip to be checked. */
public boolean isSingleSelection() {
return checkableGroup.isSingleSelection();
}
/**
* Sets whether this chip group only allows a single chip to be checked.
*
* Calling this method results in all the chips in this group to become unchecked.
*/
public void setSingleSelection(boolean singleSelection) {
checkableGroup.setSingleSelection(singleSelection);
}
/**
* Sets whether this chip group only allows a single chip to be checked.
*
* Calling this method results in all the chips in this group to become unchecked.
*/
public void setSingleSelection(@BoolRes int id) {
setSingleSelection(getResources().getBoolean(id));
}
/**
* Sets whether we prevent all child chips from being deselected.
*
* @attr ref R.styleable#ChipGroup_selectionRequired
* @see #setSingleSelection(boolean)
*/
public void setSelectionRequired(boolean selectionRequired) {
checkableGroup.setSelectionRequired(selectionRequired);
}
/**
* Returns whether we prevent all child chips from being deselected.
*
* @attr ref R.styleable#ChipGroup_selectionRequired
* @see #setSingleSelection(boolean)
* @see #setSelectionRequired(boolean)
*/
public boolean isSelectionRequired() {
return checkableGroup.isSelectionRequired();
}
/**
* A pass-through listener acts upon the events and dispatches them to another listener. This
* allows the layout to set its own internal hierarchy change listener without preventing the user
* to setup theirs.
*/
private class PassThroughHierarchyChangeListener implements OnHierarchyChangeListener {
private OnHierarchyChangeListener onHierarchyChangeListener;
@Override
public void onChildViewAdded(View parent, View child) {
if (parent == ChipGroup.this && child instanceof Chip) {
int id = child.getId();
// generates an id if it's missing
if (id == View.NO_ID) {
id = View.generateViewId();
child.setId(id);
}
checkableGroup.addCheckable((Chip) child);
}
if (onHierarchyChangeListener != null) {
onHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
@Override
public void onChildViewRemoved(View parent, View child) {
if (parent == ChipGroup.this && child instanceof Chip) {
checkableGroup.removeCheckable((Chip) child);
}
if (onHierarchyChangeListener != null) {
onHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
}