/* * 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 androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Outline; import android.graphics.PorterDuff.Mode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.RippleDrawable; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; import androidx.appcompat.widget.AppCompatCheckBox; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewOutlineProvider; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.CompoundButton; import androidx.annotation.AnimatorRes; import androidx.annotation.BoolRes; import androidx.annotation.CallSuper; import androidx.annotation.ColorRes; import androidx.annotation.DimenRes; import androidx.annotation.Dimension; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; 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; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; 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; import com.google.android.material.resources.TextAppearance; import com.google.android.material.resources.TextAppearanceFontCallback; import com.google.android.material.ripple.RippleUtils; import com.google.android.material.shape.MaterialShapeUtils; import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.Shapeable; import java.util.List; /** * Chips are compact elements that represent an attribute, text, entity, or action. They allow users * to enter information, select a choice, filter content, or trigger an action. * *

The Chip widget is a thin view wrapper around the {@link ChipDrawable}, which contains all of * the layout and draw logic. The extra logic exists to support touch, mouse, keyboard, and * accessibility navigation. The main chip and close icon are considered to be separate logical * sub-views, and contain their own navigation behavior and state. * *

All attributes from {@code R.styleable.Chip} are supported. Do not use the {@code * android:background} attribute. It will be ignored because Chip manages its own background * Drawable. Also do not use the {@code android:drawableStart} and {@code android:drawableEnd} * attributes. They will be ignored because Chip manages its own start ({@code app:chipIcon}) and * end ({@code app:closeIcon}) drawables. The basic attributes you can set are: * *

* *

You can register a listener on the main chip with {@link #setOnClickListener(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 * layout rendering sets the text padding in LTR on initial rendering and it only renders correctly * after the layout has been invalidated so you need to ensure that initial rendering has the * correct layout. * *

For more information, see the component * developer guidance and design * guidelines. * * @see ChipDrawable */ public class Chip extends AppCompatCheckBox implements Delegate, Shapeable, MaterialCheckable { private static final String TAG = "Chip"; private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_Chip_Action; private static final int CHIP_BODY_VIRTUAL_ID = 0; private static final int CLOSE_ICON_VIRTUAL_ID = 1; private static final Rect EMPTY_BOUNDS = new Rect(); private static final int[] SELECTED_STATE = new int[] {android.R.attr.state_selected}; private static final int[] CHECKABLE_STATE_SET = {android.R.attr.state_checkable}; private static final String NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"; /** Value taken from Android Accessibility Guide */ private static final int MIN_TOUCH_TARGET_DP = 48; @Nullable private ChipDrawable chipDrawable; @Nullable private InsetDrawable insetBackgroundDrawable; //noinspection NewApi @Nullable private RippleDrawable ripple; @Nullable private OnClickListener onCloseIconClickListener; @Nullable private CompoundButton.OnCheckedChangeListener onCheckedChangeListener; @Nullable private MaterialCheckable.OnCheckedChangeListener onCheckedChangeListenerInternal; private boolean deferredCheckedValue; private boolean closeIconPressed; private boolean closeIconHovered; private boolean closeIconFocused; private boolean ensureMinTouchTargetSize; private int lastLayoutDirection; @Dimension(unit = Dimension.PX) private int minTouchTargetSize; @Nullable private CharSequence accessibilityClassName; private static final String BUTTON_ACCESSIBILITY_CLASS_NAME = "android.widget.Button"; private static final String RADIO_BUTTON_ACCESSIBILITY_CLASS_NAME = "android.widget.RadioButton"; private static final String GENERIC_VIEW_ACCESSIBILITY_CLASS_NAME = "android.view.View"; @NonNull private final ChipTouchHelper touchHelper; private boolean touchHelperEnabled; private final Rect rect = new Rect(); private final RectF rectF = new RectF(); private final TextAppearanceFontCallback fontCallback = new TextAppearanceFontCallback() { @Override public void onFontRetrieved(@NonNull Typeface typeface, boolean fontResolvedSynchronously) { // Set text to re-trigger internal ellipsize width calculation. setText(chipDrawable.shouldDrawText() ? chipDrawable.getText() : getText()); requestLayout(); invalidate(); } @Override public void onFontRetrievalFailed(int reason) {} }; public Chip(Context context) { this(context, null); } public Chip(Context context, AttributeSet attrs) { this(context, attrs, R.attr.chipStyle); } public Chip(Context context, AttributeSet attrs, int defStyleAttr) { super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr); // Ensure we are using the correctly themed context rather than the context that was passed in. context = getContext(); validateAttributes(attrs); ChipDrawable drawable = ChipDrawable.createFromAttributes( context, attrs, defStyleAttr, DEF_STYLE_RES); initMinTouchTarget(context, attrs, defStyleAttr); setChipDrawable(drawable); drawable.setElevation(ViewCompat.getElevation(this)); TypedArray a = ThemeEnforcement.obtainStyledAttributes( context, attrs, R.styleable.Chip, defStyleAttr, DEF_STYLE_RES); if (VERSION.SDK_INT < VERSION_CODES.M) { // This is necessary to work around a bug that doesn't support themed color referenced in // ColorStateList for API level < 23. setTextColor( MaterialResources.getColorStateList(context, a, R.styleable.Chip_android_textColor)); } boolean hasShapeAppearanceAttribute = a.hasValue(R.styleable.Chip_shapeAppearance); a.recycle(); touchHelper = new ChipTouchHelper(this); updateAccessibilityDelegate(); if (!hasShapeAppearanceAttribute) { initOutlineProvider(); } // Set deferred values setChecked(deferredCheckedValue); setText(drawable.getText()); setEllipsize(drawable.getEllipsize()); updateTextPaintDrawState(); // Chip text should not extend to more than 1 line. if (!chipDrawable.shouldDrawText()) { setLines(1); setHorizontallyScrolling(true); } // Chip text should be vertically center aligned and start aligned. // Final horizontal text origin is set during the onDraw call via canvas translation. setGravity(Gravity.CENTER_VERTICAL | Gravity.START); // Helps TextView calculate the available text width. updatePaddingInternal(); if (shouldEnsureMinTouchTargetSize()) { setMinHeight(minTouchTargetSize); } lastLayoutDirection = ViewCompat.getLayoutDirection(this); super.setOnCheckedChangeListener( (buttonView, isChecked) -> { if (onCheckedChangeListenerInternal != null) { onCheckedChangeListenerInternal.onCheckedChanged(Chip.this, isChecked); } if (onCheckedChangeListener != null) { onCheckedChangeListener.onCheckedChanged(buttonView, isChecked); } }); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); MaterialShapeUtils.setParentAbsoluteElevation(this, chipDrawable); } @RequiresApi(VERSION_CODES.LOLLIPOP) @Override public void setElevation(float elevation) { super.setElevation(elevation); if (chipDrawable != null) { chipDrawable.setElevation(elevation); } } @Override public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(getAccessibilityClassName()); info.setCheckable(isCheckable()); info.setClickable(isClickable()); if (getParent() instanceof ChipGroup) { ChipGroup chipGroup = ((ChipGroup) getParent()); AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info); // -1 for unknown column indices in a reflowing layout int columnIndex = chipGroup.isSingleLine() ? chipGroup.getIndexOfChip(this) : -1; infoCompat.setCollectionItemInfo( CollectionItemInfoCompat.obtain( /* rowIndex= */ chipGroup.getRowIndex(this), /* rowSpan= */ 1, /* columnIndex= */ columnIndex, /* columnSpan= */ 1, /* heading= */ false, /* selected= */ isChecked())); } } // TODO(b/80452017): Due to a11y bug, avoid setting custom ExploreByTouchHelper as delegate // unless there's a close/trailing icon. Re-evaluate this once bug is fixed. private void updateAccessibilityDelegate() { if (hasCloseIcon() && isCloseIconVisible() && onCloseIconClickListener != null) { ViewCompat.setAccessibilityDelegate(this, touchHelper); touchHelperEnabled = true; } else { // Avoid setting custom ExploreByTouchHelper if the trailing icon is only decorative. ViewCompat.setAccessibilityDelegate(this, null); touchHelperEnabled = false; } } private void initMinTouchTarget(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { // Checks if the Chip should meet Android's minimum touch target size. TypedArray a = ThemeEnforcement.obtainStyledAttributes( context, attrs, R.styleable.Chip, defStyleAttr, DEF_STYLE_RES); ensureMinTouchTargetSize = a.getBoolean(R.styleable.Chip_ensureMinTouchTargetSize, false); float defaultMinTouchTargetSize = (float) Math.ceil(ViewUtils.dpToPx(getContext(), MIN_TOUCH_TARGET_DP)); minTouchTargetSize = (int) Math.ceil( a.getDimension(R.styleable.Chip_chipMinTouchTargetSize, defaultMinTouchTargetSize)); a.recycle(); } /** * Updates the paddings to inform {@link android.widget.TextView} how much width the text can * occupy. */ private void updatePaddingInternal() { if (TextUtils.isEmpty(getText()) || chipDrawable == null) { return; } int paddingEnd = (int) (chipDrawable.getChipEndPadding() + chipDrawable.getTextEndPadding() + chipDrawable.calculateCloseIconWidth()); int paddingStart = (int) (chipDrawable.getChipStartPadding() + chipDrawable.getTextStartPadding() + chipDrawable.calculateChipIconWidth()); if (insetBackgroundDrawable != null) { Rect padding = new Rect(); insetBackgroundDrawable.getPadding(padding); paddingStart += padding.left; paddingEnd += padding.right; } ViewCompat.setPaddingRelative( this, paddingStart, getPaddingTop(), paddingEnd, getPaddingBottom()); } @Override @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); // Layout direction can be updated via a parent (or ancestor) View, causing this property change // method to be called. Update text padding whenever a layout direction change is detected. if (lastLayoutDirection != layoutDirection) { lastLayoutDirection = layoutDirection; updatePaddingInternal(); } } private void validateAttributes(@Nullable AttributeSet attributeSet) { if (attributeSet == null) { return; } if (attributeSet.getAttributeValue(NAMESPACE_ANDROID, "background") != null) { Log.w(TAG, "Do not set the background; Chip manages its own background drawable."); } if (attributeSet.getAttributeValue(NAMESPACE_ANDROID, "drawableLeft") != null) { throw new UnsupportedOperationException("Please set left drawable using R.attr#chipIcon."); } if (attributeSet.getAttributeValue(NAMESPACE_ANDROID, "drawableStart") != null) { throw new UnsupportedOperationException("Please set start drawable using R.attr#chipIcon."); } if (attributeSet.getAttributeValue(NAMESPACE_ANDROID, "drawableEnd") != null) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } if (attributeSet.getAttributeValue(NAMESPACE_ANDROID, "drawableRight") != null) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } if (!attributeSet.getAttributeBooleanValue(NAMESPACE_ANDROID, "singleLine", true) || (attributeSet.getAttributeIntValue(NAMESPACE_ANDROID, "lines", 1) != 1) || (attributeSet.getAttributeIntValue(NAMESPACE_ANDROID, "minLines", 1) != 1) || (attributeSet.getAttributeIntValue(NAMESPACE_ANDROID, "maxLines", 1) != 1)) { throw new UnsupportedOperationException("Chip does not support multi-line text"); } if (attributeSet.getAttributeIntValue( NAMESPACE_ANDROID, "gravity", (Gravity.CENTER_VERTICAL | Gravity.START)) != (Gravity.CENTER_VERTICAL | Gravity.START)) { Log.w(TAG, "Chip text must be vertically center and start aligned"); } } private void initOutlineProvider() { if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { setOutlineProvider( new ViewOutlineProvider() { @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void getOutline(View view, @NonNull Outline outline) { if (chipDrawable != null) { chipDrawable.getOutline(outline); } else { outline.setAlpha(0.0f); } } }); } } /** Returns the ChipDrawable backing this chip. */ public Drawable getChipDrawable() { return chipDrawable; } /** Sets the ChipDrawable backing this chip. */ public void setChipDrawable(@NonNull ChipDrawable drawable) { if (chipDrawable != drawable) { unapplyChipDrawable(chipDrawable); chipDrawable = drawable; // Defers to TextView to draw the text and ChipDrawable to render the // rest (e.g. chip / check / close icons). chipDrawable.setShouldDrawText(false); applyChipDrawable(chipDrawable); ensureAccessibleTouchTarget(minTouchTargetSize); } } private void updateBackgroundDrawable() { if (RippleUtils.USE_FRAMEWORK_RIPPLE) { updateFrameworkRippleBackground(); } else { chipDrawable.setUseCompatRipple(true); ViewCompat.setBackground(this, getBackgroundDrawable()); updatePaddingInternal(); ensureChipDrawableHasCallback(); } } private void ensureChipDrawableHasCallback() { if (getBackgroundDrawable() == insetBackgroundDrawable && chipDrawable.getCallback() == null) { // View#setBackground nulls out the callback of the previous background drawable, so we need // to reset it. chipDrawable.setCallback(insetBackgroundDrawable); } } @Nullable public Drawable getBackgroundDrawable() { if (insetBackgroundDrawable == null) { return chipDrawable; } return insetBackgroundDrawable; } private void updateFrameworkRippleBackground() { //noinspection NewApi ripple = new RippleDrawable( RippleUtils.sanitizeRippleDrawableColor(chipDrawable.getRippleColor()), getBackgroundDrawable(), null); chipDrawable.setUseCompatRipple(false); //noinspection NewApi ViewCompat.setBackground(this, ripple); updatePaddingInternal(); } private void unapplyChipDrawable(@Nullable ChipDrawable chipDrawable) { if (chipDrawable != null) { chipDrawable.setDelegate(null); } } private void applyChipDrawable(@NonNull ChipDrawable chipDrawable) { chipDrawable.setDelegate(this); } @Override protected int[] onCreateDrawableState(int extraSpace) { final int[] state = super.onCreateDrawableState(extraSpace + 2); if (isChecked()) { mergeDrawableStates(state, SELECTED_STATE); } if (isCheckable()) { mergeDrawableStates(state, CHECKABLE_STATE_SET); } return state; } @Override public void setGravity(int gravity) { if (gravity != (Gravity.CENTER_VERTICAL | Gravity.START)) { Log.w(TAG, "Chip text must be vertically center and start aligned"); } else { super.setGravity(gravity); } } public void setBackgroundTintList(@Nullable ColorStateList tint) { Log.w(TAG, "Do not set the background tint list; Chip manages its own background drawable."); } @Override public void setBackgroundTintMode(@Nullable Mode tintMode) { Log.w(TAG, "Do not set the background tint mode; Chip manages its own background drawable."); } @Override public void setBackgroundColor(int color) { Log.w(TAG, "Do not set the background color; Chip manages its own background drawable."); } @Override public void setBackgroundResource(int resid) { Log.w(TAG, "Do not set the background resource; Chip manages its own background drawable."); } @Override public void setBackground(Drawable background) { if (background != getBackgroundDrawable() && background != ripple) { Log.w(TAG, "Do not set the background; Chip manages its own background drawable."); } else { super.setBackground(background); } } @Override public void setBackgroundDrawable(Drawable background) { if (background != getBackgroundDrawable() && background != ripple) { Log.w(TAG, "Do not set the background drawable; Chip manages its own background drawable."); } else { super.setBackgroundDrawable(background); } } @Override public void setCompoundDrawables( @Nullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) { if (left != null) { throw new UnsupportedOperationException("Please set start drawable using R.attr#chipIcon."); } if (right != null) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } super.setCompoundDrawables(left, top, right, bottom); } @Override public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) { if (left != 0) { throw new UnsupportedOperationException("Please set start drawable using R.attr#chipIcon."); } if (right != 0) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); } @Override public void setCompoundDrawablesWithIntrinsicBounds( @Nullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) { if (left != null) { throw new UnsupportedOperationException("Please set left drawable using R.attr#chipIcon."); } if (right != null) { throw new UnsupportedOperationException("Please set right drawable using R.attr#closeIcon."); } super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); } @RequiresApi(17) @Override public void setCompoundDrawablesRelative( @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom) { if (start != null) { throw new UnsupportedOperationException("Please set start drawable using R.attr#chipIcon."); } if (end != null) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } super.setCompoundDrawablesRelative(start, top, end, bottom); } @Override public void setCompoundDrawablesRelativeWithIntrinsicBounds( int start, int top, int end, int bottom) { if (start != 0) { throw new UnsupportedOperationException("Please set start drawable using R.attr#chipIcon."); } if (end != 0) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); } @Override public void setCompoundDrawablesRelativeWithIntrinsicBounds( @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom) { if (start != null) { throw new UnsupportedOperationException("Please set start drawable using R.attr#chipIcon."); } if (end != null) { throw new UnsupportedOperationException("Please set end drawable using R.attr#closeIcon."); } super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); } @Nullable @Override public TruncateAt getEllipsize() { return chipDrawable != null ? chipDrawable.getEllipsize() : null; } @Override public void setEllipsize(TruncateAt where) { if (chipDrawable == null) { return; } if (where == TruncateAt.MARQUEE) { throw new UnsupportedOperationException("Text within a chip are not allowed to scroll."); } super.setEllipsize(where); if (chipDrawable != null) { chipDrawable.setEllipsize(where); } } @Override public void setSingleLine(boolean singleLine) { if (!singleLine) { throw new UnsupportedOperationException("Chip does not support multi-line text"); } super.setSingleLine(singleLine); } @Override public void setLines(int lines) { if (lines > 1) { throw new UnsupportedOperationException("Chip does not support multi-line text"); } super.setLines(lines); } @Override public void setMinLines(int minLines) { if (minLines > 1) { throw new UnsupportedOperationException("Chip does not support multi-line text"); } super.setMinLines(minLines); } @Override public void setMaxLines(int maxLines) { if (maxLines > 1) { throw new UnsupportedOperationException("Chip does not support multi-line text"); } super.setMaxLines(maxLines); } @Override public void setMaxWidth(@Px int maxWidth) { super.setMaxWidth(maxWidth); if (chipDrawable != null) { chipDrawable.setMaxWidth(maxWidth); } } @Override public void onChipDrawableSizeChange() { ensureAccessibleTouchTarget(minTouchTargetSize); requestLayout(); if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { invalidateOutline(); } } @Override public void setChecked(boolean checked) { if (chipDrawable == null) { // Defer the setChecked() call until after initialization. deferredCheckedValue = checked; } else if (chipDrawable.isCheckable()) { super.setChecked(checked); } } @Override public void setOnCheckedChangeListener( @Nullable CompoundButton.OnCheckedChangeListener listener) { // Do not call super here - the wrapped listener set in the constructor will call the listener. onCheckedChangeListener = listener; } /** Register a callback to be invoked when the close icon is clicked. */ public void setOnCloseIconClickListener(OnClickListener listener) { this.onCloseIconClickListener = listener; updateAccessibilityDelegate(); } /** * Call this chip's close icon click listener, if it is defined. Performs all normal actions * associated with clicking: reporting accessibility event, playing a sound, etc. * * @return True there was an assigned close icon click listener that was called, false otherwise * is returned. * @see #setOnCloseIconClickListener(OnClickListener) */ @CallSuper public boolean performCloseIconClick() { playSoundEffect(SoundEffectConstants.CLICK); boolean result; if (onCloseIconClickListener != null) { onCloseIconClickListener.onClick(this); result = true; } else { result = false; } if (touchHelperEnabled) { touchHelper.sendEventForVirtualView( CLOSE_ICON_VIRTUAL_ID, AccessibilityEvent.TYPE_VIEW_CLICKED); } return result; } @SuppressLint("ClickableViewAccessibility") // There's an accessibility delegate that will handle // interactions with the trailing chip icon. @Override public boolean onTouchEvent(@NonNull MotionEvent event) { boolean handled = false; int action = event.getActionMasked(); boolean eventInCloseIcon = getCloseIconTouchBounds().contains(event.getX(), event.getY()); switch (action) { case MotionEvent.ACTION_DOWN: if (eventInCloseIcon) { setCloseIconPressed(true); handled = true; } break; case MotionEvent.ACTION_MOVE: if (closeIconPressed) { if (!eventInCloseIcon) { setCloseIconPressed(false); } handled = true; } break; case MotionEvent.ACTION_UP: if (closeIconPressed) { performCloseIconClick(); handled = true; } // Fall-through. case MotionEvent.ACTION_CANCEL: setCloseIconPressed(false); break; default: break; } return handled || super.onTouchEvent(event); } @Override public boolean onHoverEvent(@NonNull MotionEvent event) { int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_HOVER_MOVE: setCloseIconHovered(getCloseIconTouchBounds().contains(event.getX(), event.getY())); break; case MotionEvent.ACTION_HOVER_EXIT: setCloseIconHovered(false); break; default: break; } return super.onHoverEvent(event); } @Override protected boolean dispatchHoverEvent(@NonNull MotionEvent event) { if (!touchHelperEnabled) { return super.dispatchHoverEvent(event); } return touchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (!touchHelperEnabled) { return super.dispatchKeyEvent(event); } boolean handled = touchHelper.dispatchKeyEvent(event); // If the key event moves focus one beyond the end of the virtual view hierarchy in the // traversal direction (i.e. beyond the last virtual view while moving forward or before the // first virtual view while traversing backward), ExploreByTouchHelper will erroneously report // that it consumed the key event even though it does not move focus to the next or previous // real view. In order to account for this, call through to super to move focus to the correct // real view. if (handled && touchHelper.getKeyboardFocusedVirtualViewId() != ExploreByTouchHelper.INVALID_ID) { return true; } return super.dispatchKeyEvent(event); } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); if (touchHelperEnabled) { touchHelper.onFocusChanged(focused, direction, previouslyFocusedRect); } } @Override public void getFocusedRect(@NonNull Rect r) { if (touchHelperEnabled && (touchHelper.getKeyboardFocusedVirtualViewId() == CLOSE_ICON_VIRTUAL_ID || touchHelper.getAccessibilityFocusedVirtualViewId() == CLOSE_ICON_VIRTUAL_ID)) { r.set(getCloseIconTouchBoundsInt()); } else { super.getFocusedRect(r); } } private void setCloseIconPressed(boolean pressed) { if (closeIconPressed != pressed) { closeIconPressed = pressed; refreshDrawableState(); } } private void setCloseIconHovered(boolean hovered) { if (closeIconHovered != hovered) { closeIconHovered = hovered; refreshDrawableState(); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); boolean changed = false; if (chipDrawable != null && chipDrawable.isCloseIconStateful()) { changed = chipDrawable.setCloseIconState(createCloseIconDrawableState()); } if (changed) { invalidate(); } } @NonNull private int[] createCloseIconDrawableState() { int count = 0; if (isEnabled()) { count++; } if (closeIconFocused) { count++; } if (closeIconHovered) { count++; } if (closeIconPressed) { count++; } if (isChecked()) { count++; } int[] stateSet = new int[count]; int i = 0; if (isEnabled()) { stateSet[i] = android.R.attr.state_enabled; i++; } if (closeIconFocused) { stateSet[i] = android.R.attr.state_focused; i++; } if (closeIconHovered) { stateSet[i] = android.R.attr.state_hovered; i++; } if (closeIconPressed) { stateSet[i] = android.R.attr.state_pressed; i++; } if (isChecked()) { stateSet[i] = android.R.attr.state_selected; i++; } return stateSet; } private boolean hasCloseIcon() { return chipDrawable != null && chipDrawable.getCloseIcon() != null; } @NonNull private RectF getCloseIconTouchBounds() { rectF.setEmpty(); if (hasCloseIcon() && onCloseIconClickListener != null) { // noinspection ConstantConditions chipDrawable.getCloseIconTouchBounds(rectF); } return rectF; } @NonNull private Rect getCloseIconTouchBoundsInt() { RectF bounds = getCloseIconTouchBounds(); rect.set((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom); return rect; } @Nullable @Override @TargetApi(VERSION_CODES.N) public PointerIcon onResolvePointerIcon(@NonNull MotionEvent event, int pointerIndex) { if (getCloseIconTouchBounds().contains(event.getX(), event.getY()) && isEnabled()) { return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND); } return super.onResolvePointerIcon(event, pointerIndex); } /** @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 { ChipTouchHelper(Chip view) { super(view); } @Override protected int getVirtualViewAt(float x, float y) { return (hasCloseIcon() && getCloseIconTouchBounds().contains(x, y)) ? CLOSE_ICON_VIRTUAL_ID : CHIP_BODY_VIRTUAL_ID; } @Override protected void getVisibleVirtualViews(@NonNull List virtualViewIds) { virtualViewIds.add(CHIP_BODY_VIRTUAL_ID); if (hasCloseIcon() && isCloseIconVisible() && onCloseIconClickListener != null) { virtualViewIds.add(CLOSE_ICON_VIRTUAL_ID); } } @Override protected void onVirtualViewKeyboardFocusChanged(int virtualViewId, boolean hasFocus) { if (virtualViewId == CLOSE_ICON_VIRTUAL_ID) { closeIconFocused = hasFocus; refreshDrawableState(); } } @Override protected void onPopulateNodeForVirtualView( int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) { if (virtualViewId == CLOSE_ICON_VIRTUAL_ID) { CharSequence closeIconContentDescription = getCloseIconContentDescription(); if (closeIconContentDescription != null) { node.setContentDescription(closeIconContentDescription); } else { CharSequence chipText = getText(); node.setContentDescription( getContext() .getString( R.string.mtrl_chip_close_icon_content_description, !TextUtils.isEmpty(chipText) ? chipText : "") .trim()); } node.setBoundsInParent(getCloseIconTouchBoundsInt()); node.addAction(AccessibilityActionCompat.ACTION_CLICK); node.setEnabled(isEnabled()); } else { node.setContentDescription(""); node.setBoundsInParent(EMPTY_BOUNDS); } } @Override protected void onPopulateNodeForHost(@NonNull AccessibilityNodeInfoCompat node) { node.setCheckable(isCheckable()); node.setClickable(isClickable()); node.setClassName(getAccessibilityClassName()); CharSequence chipText = getText(); if (VERSION.SDK_INT >= VERSION_CODES.M) { node.setText(chipText); } else { // Before M, TalkBack doesn't get the text from setText, so we have to set the content // description instead. node.setContentDescription(chipText); } } @Override protected boolean onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments) { if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { if (virtualViewId == CHIP_BODY_VIRTUAL_ID) { return performClick(); } else if (virtualViewId == CLOSE_ICON_VIRTUAL_ID) { return performCloseIconClick(); } } return false; } } // Getters and setters for attributes. /** * Returns this chip's background color. * * @see #setChipBackgroundColor(ColorStateList) * @attr ref com.google.android.material.R.styleable#Chip_chipBackgroundColor */ @Nullable public ColorStateList getChipBackgroundColor() { return chipDrawable != null ? chipDrawable.getChipBackgroundColor() : null; } /** * Sets this chip's background color using a resource id. * * @param id The resource id of this chip's background color. * @attr ref com.google.android.material.R.styleable#Chip_chipBackgroundColor */ public void setChipBackgroundColorResource(@ColorRes int id) { if (chipDrawable != null) { chipDrawable.setChipBackgroundColorResource(id); } } /** * Sets this chip's background color. * * @param chipBackgroundColor This chip's background color. * @attr ref com.google.android.material.R.styleable#Chip_chipBackgroundColor */ public void setChipBackgroundColor(@Nullable ColorStateList chipBackgroundColor) { if (chipDrawable != null) { chipDrawable.setChipBackgroundColor(chipBackgroundColor); } } /** * Returns this chip's minimum height. * * @see #setChipMinHeight(float) * @attr ref com.google.android.material.R.styleable#Chip_chipMinHeight */ public float getChipMinHeight() { return chipDrawable != null ? chipDrawable.getChipMinHeight() : 0; } /** * Sets this chip's minimum height using a resource id. * * @param id The resource id of this chip's minimum height. * @attr ref com.google.android.material.R.styleable#Chip_chipMinHeight */ public void setChipMinHeightResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setChipMinHeightResource(id); } } /** * Sets this chip's minimum height. * * @param minHeight This chip's minimum height. * @attr ref com.google.android.material.R.styleable#Chip_chipMinHeight */ public void setChipMinHeight(float minHeight) { if (chipDrawable != null) { chipDrawable.setChipMinHeight(minHeight); } } /** * Returns this chip's corner radius. * * @see #setChipCornerRadius(float) * @attr ref com.google.android.material.R.styleable#Chip_chipCornerRadius */ public float getChipCornerRadius() { return chipDrawable != null ? Math.max(0, chipDrawable.getChipCornerRadius()) : 0; } /** * @deprecated call {@link ShapeAppearanceModel#withCornerSize(float)} or call {@link * ShapeAppearanceModel#toBuilder()} on the {@link #getShapeAppearanceModel()}, modify the * shape using the builder and then call {@link * #setShapeAppearanceModel(ShapeAppearanceModel)}. */ @Deprecated public void setChipCornerRadiusResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setChipCornerRadiusResource(id); } } @Override public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) { chipDrawable.setShapeAppearanceModel(shapeAppearanceModel); } @NonNull @Override public ShapeAppearanceModel getShapeAppearanceModel() { return chipDrawable.getShapeAppearanceModel(); } /** * @deprecated call {@link ShapeAppearanceModel#withCornerSize(float)} or call {@link * ShapeAppearanceModel#toBuilder()} on the {@link #getShapeAppearanceModel()}, modify the * shape using the builder and then call {@link * #setShapeAppearanceModel(ShapeAppearanceModel)}. */ @Deprecated public void setChipCornerRadius(float chipCornerRadius) { if (chipDrawable != null) { chipDrawable.setChipCornerRadius(chipCornerRadius); } } /** * Returns this chip's stroke color. * * @see #setChipStrokeColor(ColorStateList) * @attr ref com.google.android.material.R.styleable#Chip_chipStrokeColor */ @Nullable public ColorStateList getChipStrokeColor() { return chipDrawable != null ? chipDrawable.getChipStrokeColor() : null; } /** * Sets this chip's stroke color using a resource id. * * @param id The resource id of this chip's stroke color. * @attr ref com.google.android.material.R.styleable#Chip_chipStrokeColor */ public void setChipStrokeColorResource(@ColorRes int id) { if (chipDrawable != null) { chipDrawable.setChipStrokeColorResource(id); } } /** * Sets this chip's stroke color. * * @param chipStrokeColor This chip's stroke color. * @attr ref com.google.android.material.R.styleable#Chip_chipStrokeColor */ public void setChipStrokeColor(@Nullable ColorStateList chipStrokeColor) { if (chipDrawable != null) { chipDrawable.setChipStrokeColor(chipStrokeColor); } } /** * Returns this chip's stroke width. * * @see #setChipStrokeWidth(float) * @attr ref com.google.android.material.R.styleable#Chip_chipStrokeWidth */ public float getChipStrokeWidth() { return chipDrawable != null ? chipDrawable.getChipStrokeWidth() : 0; } /** * Sets this chip's stroke width using a resource id. * * @param id The resource id of this chip's stroke width. * @attr ref com.google.android.material.R.styleable#Chip_chipStrokeWidth */ public void setChipStrokeWidthResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setChipStrokeWidthResource(id); } } /** * Sets this chip's stroke width. * * @param chipStrokeWidth This chip's stroke width. * @attr ref com.google.android.material.R.styleable#Chip_chipStrokeWidth */ public void setChipStrokeWidth(float chipStrokeWidth) { if (chipDrawable != null) { chipDrawable.setChipStrokeWidth(chipStrokeWidth); } } /** * Returns this chip's ripple color. * * @see #setRippleColor(ColorStateList) * @attr ref com.google.android.material.R.styleable#Chip_rippleColor */ @Nullable public ColorStateList getRippleColor() { return chipDrawable != null ? chipDrawable.getRippleColor() : null; } /** * Sets this chip's ripple color using a resource id. * * @param id The resource id of this chip's ripple color. * @attr ref com.google.android.material.R.styleable#Chip_rippleColor */ public void setRippleColorResource(@ColorRes int id) { if (chipDrawable != null) { chipDrawable.setRippleColorResource(id); if (!chipDrawable.getUseCompatRipple()) { updateFrameworkRippleBackground(); } } } /** * Sets this chip's ripple color. * * @param rippleColor This chip's ripple color. * @attr ref com.google.android.material.R.styleable#Chip_rippleColor */ public void setRippleColor(@Nullable ColorStateList rippleColor) { if (chipDrawable != null) { chipDrawable.setRippleColor(rippleColor); } if (!chipDrawable.getUseCompatRipple()) { updateFrameworkRippleBackground(); } } /** * Returns this chip's text. * * @deprecated Use {@link Chip#getText()} instead. */ @Deprecated public CharSequence getChipText() { return getText(); } @Override public void setLayoutDirection(int layoutDirection) { if (chipDrawable == null) { return; } if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { super.setLayoutDirection(layoutDirection); } } @Override public void setText(CharSequence text, BufferType type) { if (chipDrawable == null) { return; } if (text == null) { text = ""; } super.setText(chipDrawable.shouldDrawText() ? null : text, type); if (chipDrawable != null) { chipDrawable.setText(text); } } /** @deprecated Use {@link Chip#setText(int)} instead. */ @Deprecated public void setChipTextResource(@StringRes int id) { setText(getResources().getString(id)); } /** @deprecated Use {@link Chip#setText(CharSequence)} instead. */ @Deprecated public void setChipText(@Nullable CharSequence chipText) { setText(chipText); } /** * Sets this chip's text appearance using a resource id. * * @param id The resource id of this chip's text appearance. * @attr ref com.google.android.material.R.styleable#Chip_android_textAppearance */ public void setTextAppearanceResource(@StyleRes int id) { this.setTextAppearance(getContext(), id); } /** * Sets this chip's text appearance. * * @param textAppearance This chip's text appearance. * @attr ref com.google.android.material.R.styleable#Chip_android_textAppearance */ public void setTextAppearance(@Nullable TextAppearance textAppearance) { if (chipDrawable != null) { chipDrawable.setTextAppearance(textAppearance); } updateTextPaintDrawState(); } @Override public void setTextAppearance(Context context, int resId) { super.setTextAppearance(context, resId); if (chipDrawable != null) { chipDrawable.setTextAppearanceResource(resId); } updateTextPaintDrawState(); } @Override public void setTextAppearance(int resId) { super.setTextAppearance(resId); if (chipDrawable != null) { chipDrawable.setTextAppearanceResource(resId); } updateTextPaintDrawState(); } @Override public void setTextSize(int unit, float size) { super.setTextSize(unit, size); if (chipDrawable != null) { chipDrawable.setTextSize( TypedValue.applyDimension(unit, size, getResources().getDisplayMetrics())); } updateTextPaintDrawState(); } private void updateTextPaintDrawState() { TextPaint textPaint = getPaint(); if (chipDrawable != null) { textPaint.drawableState = chipDrawable.getState(); } TextAppearance textAppearance = getTextAppearance(); if (textAppearance != null) { textAppearance.updateDrawState(getContext(), textPaint, fontCallback); } } @Nullable private TextAppearance getTextAppearance() { return chipDrawable != null ? chipDrawable.getTextAppearance() : null; } /** * Returns whether this chip's icon is visible. * * @see #setChipIconVisible(boolean) * @attr ref com.google.android.material.R.styleable#Chip_chipIconVisible */ public boolean isChipIconVisible() { return chipDrawable != null && chipDrawable.isChipIconVisible(); } /** @deprecated Use {@link Chip#isChipIconVisible()} instead. */ @Deprecated public boolean isChipIconEnabled() { return isChipIconVisible(); } /** * Sets the visibility of this chip's icon using a resource id. * * @param id The resource id for the visibility of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_chipIconVisible */ public void setChipIconVisible(@BoolRes int id) { if (chipDrawable != null) { chipDrawable.setChipIconVisible(id); } } /** * Sets whether this chip's icon is visible. * * @param chipIconVisible The visibility of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_chipIconIsVisible */ public void setChipIconVisible(boolean chipIconVisible) { if (chipDrawable != null) { chipDrawable.setChipIconVisible(chipIconVisible); } } /** @deprecated Use {@link Chip#setChipIconVisible(int)} instead. */ @Deprecated public void setChipIconEnabledResource(@BoolRes int id) { setChipIconVisible(id); } /** @deprecated Use {@link Chip#setChipIconVisible(boolean)} instead. */ @Deprecated public void setChipIconEnabled(boolean chipIconEnabled) { setChipIconVisible(chipIconEnabled); } /** * Returns this chip's icon. * * @see #setChipIcon(Drawable) * @attr ref com.google.android.material.R.styleable#Chip_chipIcon */ @Nullable public Drawable getChipIcon() { return chipDrawable != null ? chipDrawable.getChipIcon() : null; } /** * Sets this chip's icon using a resource id. * * @param id The resource id for this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_chipIcon */ public void setChipIconResource(@DrawableRes int id) { if (chipDrawable != null) { chipDrawable.setChipIconResource(id); } } /** * Sets this chip's icon. * * @param chipIcon drawable of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_chipIcon */ public void setChipIcon(@Nullable Drawable chipIcon) { if (chipDrawable != null) { chipDrawable.setChipIcon(chipIcon); } } /** * Returns the {@link android.content.res.ColorStateList} used to tint the chip icon. * * @see #setChipIconTint(ColorStateList) * @attr ref com.google.android.material.R.styleable#Chip_chipIconTint */ @Nullable public ColorStateList getChipIconTint() { return chipDrawable != null ? chipDrawable.getChipIconTint() : null; } /** * Sets this chip icon's color tint using a resource id. * * @param id The resource id for tinting the chip icon. * @attr ref com.google.android.material.R.styleable#Chip_chipIconTint */ public void setChipIconTintResource(@ColorRes int id) { if (chipDrawable != null) { chipDrawable.setChipIconTintResource(id); } } /** * Sets this chip icon's color tint using the specified {@link * android.content.res.ColorStateList}. * * @param chipIconTint The tint color of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_chipIconTint */ public void setChipIconTint(@Nullable ColorStateList chipIconTint) { if (chipDrawable != null) { chipDrawable.setChipIconTint(chipIconTint); } } /** * Returns this chip's icon size. * If a non-positive value is set, the icon drawable's width and height (up to 24dp) will be used * instead. * * @see #setChipIconSize(float) * @attr ref com.google.android.material.R.styleable#Chip_chipIconTint */ public float getChipIconSize() { return chipDrawable != null ? chipDrawable.getChipIconSize() : 0; } /** * Sets this chip icon's size using a resource id. * If the value is zero or negative, the icon drawable's width and height (up to 24dp) will be * used instead. * * @param id The resource id of this chip's icon size. * @attr ref com.google.android.material.R.styleable#Chip_chipIconSize */ public void setChipIconSizeResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setChipIconSizeResource(id); } } /** * Sets this chip icon's size. * If the value is zero or negative, the icon drawable's width and height (up to 24dp) will be * used instead. * * @param chipIconSize This chip's icon size. * @attr ref com.google.android.material.R.styleable#Chip_chipIconSize */ public void setChipIconSize(float chipIconSize) { if (chipDrawable != null) { chipDrawable.setChipIconSize(chipIconSize); } } /** * Returns whether this chip's close icon is visible. * * @see id #setCloseIconVisible(boolean) * @attr ref com.google.android.material.R.styleable#Chip_chipIconSize */ public boolean isCloseIconVisible() { return chipDrawable != null && chipDrawable.isCloseIconVisible(); } /** @deprecated Use {@link Chip#isCloseIconVisible()} instead. */ @Deprecated public boolean isCloseIconEnabled() { return isCloseIconVisible(); } /** * Sets whether this chip close icon is visible using a resource id. * * @param id The resource id of this chip's close icon visibility. * @attr ref com.google.android.material.R.styleable#Chip_closeIconVisible */ public void setCloseIconVisible(@BoolRes int id) { setCloseIconVisible(getResources().getBoolean(id)); } /** * Sets whether this chip close icon is visible. * * @param closeIconVisible This chip's close icon visibility. * @attr ref com.google.android.material.R.styleable#Chip_closeIconVisible */ public void setCloseIconVisible(boolean closeIconVisible) { if (chipDrawable != null) { chipDrawable.setCloseIconVisible(closeIconVisible); } updateAccessibilityDelegate(); } /** @deprecated Use {@link Chip#setCloseIconVisible(int)} instead. */ @Deprecated public void setCloseIconEnabledResource(@BoolRes int id) { setCloseIconVisible(id); } /** @deprecated Use {@link Chip#setCloseIconVisible(boolean)} instead. */ @Deprecated public void setCloseIconEnabled(boolean closeIconEnabled) { setCloseIconVisible(closeIconEnabled); } /** * Returns this chip's close icon. * * @see #setCloseIcon(Drawable). * @attr ref com.google.android.material.R.styleable#Chip_closeIcon */ @Nullable public Drawable getCloseIcon() { return chipDrawable != null ? chipDrawable.getCloseIcon() : null; } /** * Sets this chip's close icon using a resource id. * * @param id The resource id of this chip's close icon. * @attr ref com.google.android.material.R.styleable#Chip_closeIcon */ public void setCloseIconResource(@DrawableRes int id) { if (chipDrawable != null) { chipDrawable.setCloseIconResource(id); } updateAccessibilityDelegate(); } /** * Sets this chip's close icon. * * @param closeIcon This chip's close icon. * @attr ref com.google.android.material.R.styleable#Chip_closeIcon */ public void setCloseIcon(@Nullable Drawable closeIcon) { if (chipDrawable != null) { chipDrawable.setCloseIcon(closeIcon); } updateAccessibilityDelegate(); } /** * Returns the tint color for this chip's close icon. * * @see #setCloseIconTint(ColorStateList) * @attr ref com.google.android.material.R.styleable#Chip_closeIconTint */ @Nullable public ColorStateList getCloseIconTint() { return chipDrawable != null ? chipDrawable.getCloseIconTint() : null; } /** * Sets the tint color for this chip's close icon using a resource id. * * @param id The resource id of this chip's close icon tint. * @attr ref com.google.android.material.R.styleable#Chip_closeIconTint */ public void setCloseIconTintResource(@ColorRes int id) { if (chipDrawable != null) { chipDrawable.setCloseIconTintResource(id); } } /** * Sets the tint color for this chip's close icon. * * @param closeIconTint This chip's close icon tint. * @attr ref com.google.android.material.R.styleable#Chip_closeIconTint */ public void setCloseIconTint(@Nullable ColorStateList closeIconTint) { if (chipDrawable != null) { chipDrawable.setCloseIconTint(closeIconTint); } } /** * Returns this chip's close icon size. * * @see #setCloseIconSize(float) * @attr ref com.google.android.material.R.styleable#Chip_closeIconSize */ public float getCloseIconSize() { return chipDrawable != null ? chipDrawable.getCloseIconSize() : 0; } /** * Sets this chip's close icon size using a resource id. * * @param id The resource id of this chip's close icon size. * @attr ref com.google.android.material.R.styleable#Chip_closeIconSize */ public void setCloseIconSizeResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setCloseIconSizeResource(id); } } /** * Sets this chip's close icon size. * * @param closeIconSize This chip's close icon size. * @attr ref com.google.android.material.R.styleable#Chip_closeIconSize */ public void setCloseIconSize(float closeIconSize) { if (chipDrawable != null) { chipDrawable.setCloseIconSize(closeIconSize); } } /** * Sets the content description for this chip's close icon. * * @param closeIconContentDescription The content description for this chip's close icon. */ public void setCloseIconContentDescription(@Nullable CharSequence closeIconContentDescription) { if (chipDrawable != null) { chipDrawable.setCloseIconContentDescription(closeIconContentDescription); } } /** * Returns this chip's close icon content description. * * @see #setCloseIconContentDescription(CharSequence) */ @Nullable public CharSequence getCloseIconContentDescription() { return chipDrawable != null ? chipDrawable.getCloseIconContentDescription() : null; } /** * Returns whether this chip is checkable. * * @see #setCheckable(boolean) * @attr ref com.google.android.material.R.styleable#Chip_android_checkable */ public boolean isCheckable() { return chipDrawable != null && chipDrawable.isCheckable(); } /** * Sets whether this chip is checkable using a resource id. * * @param id The resource id of this chip is checkable. * @attr ref com.google.android.material.R.styleable#Chip_android_checkable */ public void setCheckableResource(@BoolRes int id) { if (chipDrawable != null) { chipDrawable.setCheckableResource(id); } } /** * Sets whether this chip is checkable. * * @param checkable Whether this chip is checkable. * @attr ref com.google.android.material.R.styleable#Chip_android_checkable */ public void setCheckable(boolean checkable) { if (chipDrawable != null) { chipDrawable.setCheckable(checkable); } } /** * Returns whether this chip's checked icon is visible. * * @see #setCheckedIconVisible(boolean) * @attr ref com.google.android.material.R.styleable#Chip_checkedIconVisible */ public boolean isCheckedIconVisible() { return chipDrawable != null && chipDrawable.isCheckedIconVisible(); } /** @deprecated Use {@link Chip#isCheckedIconVisible()} instead. */ @Deprecated public boolean isCheckedIconEnabled() { return isCheckedIconVisible(); } /** * Sets whether this chip's checked icon is visible using a resource id. * * @param id The resource id of this chip's check icon visibility. * @attr ref com.google.android.material.R.styleable#Chip_checkedIconVisible */ public void setCheckedIconVisible(@BoolRes int id) { if (chipDrawable != null) { chipDrawable.setCheckedIconVisible(id); } } /** * Sets whether this chip's checked icon is visible. * * @param checkedIconVisible This chip's checked icon visibility. * @attr ref com.google.android.material.R.styleable#Chip_checkedIconVisible */ public void setCheckedIconVisible(boolean checkedIconVisible) { if (chipDrawable != null) { chipDrawable.setCheckedIconVisible(checkedIconVisible); } } /** @deprecated Use {@link Chip#setCheckedIconVisible(int)} instead. */ @Deprecated public void setCheckedIconEnabledResource(@BoolRes int id) { setCheckedIconVisible(id); } /** @deprecated Use {@link Chip#setCheckedIconVisible(boolean)} instead. */ @Deprecated public void setCheckedIconEnabled(boolean checkedIconEnabled) { setCheckedIconVisible(checkedIconEnabled); } /** * Returns this chip's checked icon. * * @see #setCheckedIcon(Drawable) * @attr ref com.google.android.material.R.styleable#Chip_checkedIcon */ @Nullable public Drawable getCheckedIcon() { return chipDrawable != null ? chipDrawable.getCheckedIcon() : null; } /** * Sets this chip's checked icon using a resource id. * * @param id The resource id of this chip's checked icon. * @attr ref com.google.android.material.R.styleable#Chip_checkedIcon */ public void setCheckedIconResource(@DrawableRes int id) { if (chipDrawable != null) { chipDrawable.setCheckedIconResource(id); } } /** * Sets this chip's checked icon. * * @param checkedIcon This chip's checked icon. * @attr ref com.google.android.material.R.styleable#Chip_checkedIcon */ public void setCheckedIcon(@Nullable Drawable checkedIcon) { if (chipDrawable != null) { chipDrawable.setCheckedIcon(checkedIcon); } } /** * Returns the {@link android.content.res.ColorStateList} used to tint the checked icon. * * @see #setCheckedIconTint(ColorStateList) * @attr ref com.google.android.material.R.styleable#Chip_checkedIconTint */ @Nullable public ColorStateList getCheckedIconTint() { return chipDrawable != null ? chipDrawable.getCheckedIconTint() : null; } /** * Sets this chip's checked icon's color tint using a resource id. * * @param id The resource id for tinting the checked icon. * @attr ref com.google.android.material.R.styleable#Chip_checkedIconTint */ public void setCheckedIconTintResource(@ColorRes int id) { if (chipDrawable != null) { chipDrawable.setCheckedIconTintResource(id); } } /** * Sets this chip's checked icon's color tint using the specified {@link * android.content.res.ColorStateList}. * * @param checkedIconTint The tint color of this chip's checked icon. * @attr ref com.google.android.material.R.styleable#Chip_checkedIconTint */ public void setCheckedIconTint(@Nullable ColorStateList checkedIconTint) { if (chipDrawable != null) { chipDrawable.setCheckedIconTint(checkedIconTint); } } /** * Returns this chip's show motion spec. * * @see #setShowMotionSpec(MotionSpec) * @attr ref com.google.android.material.R.styleable#Chip_showMotionSpec */ @Nullable public MotionSpec getShowMotionSpec() { return chipDrawable != null ? chipDrawable.getShowMotionSpec() : null; } /** * Sets this chip's show motion spec using a resource id. * * @param id The resource id of this chip's show motion spec. * @attr ref com.google.android.material.R.styleable#Chip_showMotionSpec */ public void setShowMotionSpecResource(@AnimatorRes int id) { if (chipDrawable != null) { chipDrawable.setShowMotionSpecResource(id); } } /** * Sets this chip's show motion spec. * * @param showMotionSpec This chip's show motion spec. * @attr ref com.google.android.material.R.styleable#Chip_showMotionSpec */ public void setShowMotionSpec(@Nullable MotionSpec showMotionSpec) { if (chipDrawable != null) { chipDrawable.setShowMotionSpec(showMotionSpec); } } /** * Returns this chip's hide motion spec. * * @see #setHideMotionSpec(MotionSpec) * @attr ref com.google.android.material.R.styleable#Chip_hideMotionSpec */ @Nullable public MotionSpec getHideMotionSpec() { return chipDrawable != null ? chipDrawable.getHideMotionSpec() : null; } /** * Sets this chip's hide motion spec using a resource id. * * @param id The resource id of this chip's hide motion spec. * @attr ref com.google.android.material.R.styleable#Chip_hideMotionSpec */ public void setHideMotionSpecResource(@AnimatorRes int id) { if (chipDrawable != null) { chipDrawable.setHideMotionSpecResource(id); } } /** * Sets this chip's hide motion spec. * * @param hideMotionSpec This chip's hide motion spec. * @attr ref com.google.android.material.R.styleable#Chip_hideMotionSpec */ public void setHideMotionSpec(@Nullable MotionSpec hideMotionSpec) { if (chipDrawable != null) { chipDrawable.setHideMotionSpec(hideMotionSpec); } } /** * Returns this chip's start padding. * * @see #setChipStartPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_chipStartPadding */ public float getChipStartPadding() { return chipDrawable != null ? chipDrawable.getChipStartPadding() : 0; } /** * Sets this chip's start padding using a resource id. * * @param id The resource id of this chip's start padding. * @attr ref com.google.android.material.R.styleable#Chip_chipStartPadding */ public void setChipStartPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setChipStartPaddingResource(id); } } /** * Sets this chip's start padding. * * @param chipStartPadding This chip's start padding. * @attr ref com.google.android.material.R.styleable#Chip_chipStartPadding */ public void setChipStartPadding(float chipStartPadding) { if (chipDrawable != null) { chipDrawable.setChipStartPadding(chipStartPadding); } } /** * Returns the start padding for this chip's icon. * * @see #setIconStartPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_iconStartPadding */ public float getIconStartPadding() { return chipDrawable != null ? chipDrawable.getIconStartPadding() : 0; } /** * Sets the start padding for this chip's icon using a resource id. * * @param id The resource id for the start padding of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_iconStartPadding */ public void setIconStartPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setIconStartPaddingResource(id); } } /** * Sets this chip's icon start padding. * * @param iconStartPadding The start padding of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_iconStartPadding */ public void setIconStartPadding(float iconStartPadding) { if (chipDrawable != null) { chipDrawable.setIconStartPadding(iconStartPadding); } } /** * Returns the end padding for this chip's icon. * * @see #setIconEndPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_iconEndPadding */ public float getIconEndPadding() { return chipDrawable != null ? chipDrawable.getIconEndPadding() : 0; } /** * Sets the end padding for this chip's icon using a resource id. * * @param id The resource id for the end padding of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_iconEndPadding */ public void setIconEndPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setIconEndPaddingResource(id); } } /** * Sets the end padding for this chip's icon. * * @param iconEndPadding The end padding of this chip's icon. * @attr ref com.google.android.material.R.styleable#Chip_iconEndPadding */ public void setIconEndPadding(float iconEndPadding) { if (chipDrawable != null) { chipDrawable.setIconEndPadding(iconEndPadding); } } /** * Returns the start padding for this chip's text. * * @see #setTextStartPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_textStartPadding */ public float getTextStartPadding() { return chipDrawable != null ? chipDrawable.getTextStartPadding() : 0; } /** * Sets the start padding for this chip's text using a resource id. * * @param id The resource id for the start padding of this chip's text. * @attr ref com.google.android.material.R.styleable#Chip_textStartPadding */ public void setTextStartPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setTextStartPaddingResource(id); } } /** * Sets the start padding for this chip's text. * * @param textStartPadding The start padding of this chip's text. * @attr ref com.google.android.material.R.styleable#Chip_textStartPadding */ public void setTextStartPadding(float textStartPadding) { if (chipDrawable != null) { chipDrawable.setTextStartPadding(textStartPadding); } } /** * Returns the end padding for this chip's text. * * @see #setTextEndPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_textEndPadding */ public float getTextEndPadding() { return chipDrawable != null ? chipDrawable.getTextEndPadding() : 0; } /** * Sets the end padding for this chip's text using a resource id. * * @param id The resource id for the end padding of this chip's text. * @attr ref com.google.android.material.R.styleable#Chip_textEndPadding */ public void setTextEndPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setTextEndPaddingResource(id); } } /** * Sets the end padding for this chip's text. * * @param textEndPadding The end padding of this chip's text. * @attr ref com.google.android.material.R.styleable#Chip_textStartPadding */ public void setTextEndPadding(float textEndPadding) { if (chipDrawable != null) { chipDrawable.setTextEndPadding(textEndPadding); } } /** * Returns the start padding for this chip's close icon. * * @see #setCloseIconStartPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_closeIconStartPadding */ public float getCloseIconStartPadding() { return chipDrawable != null ? chipDrawable.getCloseIconStartPadding() : 0; } /** * Sets the start padding for this chip's close icon using a resource id. * * @param id The resource id for the start padding of this chip's close icon. * @attr ref com.google.android.material.R.styleable#Chip_closeIconStartPadding */ public void setCloseIconStartPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setCloseIconStartPaddingResource(id); } } /** * Sets the start padding for this chip's close icon. * * @param closeIconStartPadding The start padding of this chip's close icon. * @attr ref com.google.android.material.R.styleable#Chip_closeIconStartPadding */ public void setCloseIconStartPadding(float closeIconStartPadding) { if (chipDrawable != null) { chipDrawable.setCloseIconStartPadding(closeIconStartPadding); } } /** * Returns the end padding for this chip's close icon. * * @see #setCloseIconEndPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_closeIconEndPadding */ public float getCloseIconEndPadding() { return chipDrawable != null ? chipDrawable.getCloseIconEndPadding() : 0; } /** * Sets the end padding for this chip's close icon using a resource id. * * @param id The resource id for the end padding of this chip's close icon. * @attr ref com.google.android.material.R.styleable#Chip_closeIconEndPadding */ public void setCloseIconEndPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setCloseIconEndPaddingResource(id); } } /** * Sets the end padding for this chip's close icon. * * @param closeIconEndPadding The end padding of this chip's close icon. * @attr ref com.google.android.material.R.styleable#Chip_closeIconEndPadding */ public void setCloseIconEndPadding(float closeIconEndPadding) { if (chipDrawable != null) { chipDrawable.setCloseIconEndPadding(closeIconEndPadding); } } /** * Returns this chip's end padding. * * @see #setChipEndPadding(float) * @attr ref com.google.android.material.R.styleable#Chip_chipEndPadding */ public float getChipEndPadding() { return chipDrawable != null ? chipDrawable.getChipEndPadding() : 0; } /** * Sets this chip's end padding using a resource id. * * @param id The resource id for this chip's end padding. * @attr ref com.google.android.material.R.styleable#Chip_chipEndPadding */ public void setChipEndPaddingResource(@DimenRes int id) { if (chipDrawable != null) { chipDrawable.setChipEndPaddingResource(id); } } /** * Sets this chip's end padding. * * @param chipEndPadding This chip's end padding. * @attr ref com.google.android.material.R.styleable#Chip_chipEndPadding */ public void setChipEndPadding(float chipEndPadding) { if (chipDrawable != null) { chipDrawable.setChipEndPadding(chipEndPadding); } } /** * Returns whether this chip will expand its bounds (if needed) to meet the minimum touch target * size. * * @see #setEnsureMinTouchTargetSize(boolean) * @attr ref com.google.android.material.R.styleable#Chip_ensureMinTouchTargetSize */ public boolean shouldEnsureMinTouchTargetSize() { return ensureMinTouchTargetSize; } /** * Sets whether this chip should expand its bounds (if needed) to meet the minimum touch target * size. * * @param flag Whether this chip should meet the min touch target size. * @attr ref com.google.android.material.R.styleable#Chip_ensureMinTouchTargetSize */ public void setEnsureMinTouchTargetSize(boolean flag) { ensureMinTouchTargetSize = flag; ensureAccessibleTouchTarget(minTouchTargetSize); } /** * Extends the touch target of this chip using a {@link InsetDrawable} if chip's intrinsic width / * height is smaller than the {@code minTargetPx}. * * @param minTargetPx minimum touch target size in pixel * @return whether the background was changed */ public boolean ensureAccessibleTouchTarget(@Dimension int minTargetPx) { minTouchTargetSize = minTargetPx; if (!shouldEnsureMinTouchTargetSize()) { if (insetBackgroundDrawable != null) { removeBackgroundInset(); } else { updateBackgroundDrawable(); } return false; } int deltaHeight = Math.max(0, minTargetPx - chipDrawable.getIntrinsicHeight()); int deltaWidth = Math.max(0, minTargetPx - chipDrawable.getIntrinsicWidth()); if (deltaWidth <= 0 && deltaHeight <= 0) { if (insetBackgroundDrawable != null) { removeBackgroundInset(); } else { updateBackgroundDrawable(); } return false; } int deltaX = deltaWidth > 0 ? deltaWidth / 2 : 0; int deltaY = deltaHeight > 0 ? deltaHeight / 2 : 0; if (insetBackgroundDrawable != null) { Rect padding = new Rect(); insetBackgroundDrawable.getPadding(padding); if (padding.top == deltaY && padding.bottom == deltaY && padding.left == deltaX && padding.right == deltaX) { updateBackgroundDrawable(); return true; } } if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { if (getMinHeight() != minTargetPx) { setMinHeight(minTargetPx); } if (getMinWidth() != minTargetPx) { setMinWidth(minTargetPx); } } else { setMinHeight(minTargetPx); setMinWidth(minTargetPx); } insetChipBackgroundDrawable(deltaX, deltaY, deltaX, deltaY); updateBackgroundDrawable(); return true; } /** * Sets this chip's accessibility class name. * * @param className This chip's accessibility class name. */ public void setAccessibilityClassName(@Nullable CharSequence className) { accessibilityClassName = className; } @Override @NonNull public CharSequence getAccessibilityClassName() { if (!TextUtils.isEmpty(accessibilityClassName)) { return accessibilityClassName; } else if (isCheckable()) { ViewParent parent = getParent(); if (parent instanceof ChipGroup && ((ChipGroup) parent).isSingleSelection()) { return RADIO_BUTTON_ACCESSIBILITY_CLASS_NAME; } else { return BUTTON_ACCESSIBILITY_CLASS_NAME; } } else if (isClickable()) { return BUTTON_ACCESSIBILITY_CLASS_NAME; } else { return GENERIC_VIEW_ACCESSIBILITY_CLASS_NAME; } } private void removeBackgroundInset() { if (insetBackgroundDrawable != null) { insetBackgroundDrawable = null; setMinWidth(0); setMinHeight((int) getChipMinHeight()); updateBackgroundDrawable(); } } private void insetChipBackgroundDrawable( int insetLeft, int insetTop, int insetRight, int insetBottom) { insetBackgroundDrawable = new InsetDrawable(chipDrawable, insetLeft, insetTop, insetRight, insetBottom); } }