Material Design Team a5c1ea2709 Fixes Chip text padding & drawing.
The previous code was setting total padding (both start & end) as just end padding of the parent TextView and relying on the invalidation phase drawing the text in the correct place by shifting canvas with computed offset of start padding. This is risky as Chip may be just re-laid without actually invalidating it and render wrong paddings. Also RTL layout direction is incorrectly identified as LTR during layout sometimes, which makes the text being rendered with incorrect offset.

This change sets the paddings correctly removing the need of manual offsetting during onDraw() and relies purely on TextView for its text rendering.

PiperOrigin-RevId: 210042249
2018-08-28 16:40:05 -04:00

1683 lines
53 KiB
Java

/*
* 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 android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
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.RippleDrawable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.AnimatorRes;
import android.support.annotation.BoolRes;
import android.support.annotation.CallSuper;
import android.support.annotation.ColorRes;
import android.support.annotation.DimenRes;
import android.support.annotation.Dimension;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.annotation.StringRes;
import android.support.annotation.StyleRes;
import com.google.android.material.animation.MotionSpec;
import com.google.android.material.chip.ChipDrawable.Delegate;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.TouchTargetUtils;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.resources.TextAppearance;
import com.google.android.material.ripple.RippleUtils;
import android.support.v4.content.res.ResourcesCompat.FontCallback;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.support.v7.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.TouchDelegate;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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.
*
* <p>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.
*
* <p>All attributes from {@link 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:
*
* <ul>
* <li>{@link android.R.attr#checkable android:checkable} - If true, the chip can be toggled. If
* false, the chip acts like a button.
* <li>{@link android.R.attr#text android:text} - Sets the text of the chip.
* <li>{@link R.attr#chipIcon app:chipIcon} and {@link R.attr#chipIconEnabled app:chipIconEnabled}
* - Sets the icon of the chip. Usually on the left.
* <li>{@link R.attr#checkedIcon app:checkedIcon} and {@link R.attr#checkedIconEnabled
* app:checkedIconEnabled} - Sets a custom icon to use when checked. Usually on the left.
* <li>{@link R.attr#closeIcon app:closeIcon} and {@link R.attr#closeIconEnabled
* app:closeIconEnabled} - Sets a custom icon that the user can click to close. Usually on the
* right.
* </ul>
*
* <p>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)}.
*
* @see ChipDrawable
*/
public class Chip extends AppCompatCheckBox implements Delegate {
private static final String TAG = "Chip";
private static final int CLOSE_ICON_VIRTUAL_ID = 0;
private static final Rect EMPTY_BOUNDS = new Rect();
private static final int[] SELECTED_STATE = new int[] {android.R.attr.state_selected};
private static final String NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android";
private static final String NAMESPACE_APP = "http://schemas.android.com/apk/res-auto";
/** Value taken from Android Accessibility Guide */
private static final int MIN_TOUCH_TARGET_DP = 48;
@Retention(RetentionPolicy.SOURCE)
@IntDef({ExploreByTouchHelper.INVALID_ID, ExploreByTouchHelper.HOST_ID, CLOSE_ICON_VIRTUAL_ID})
private @interface VirtualId {}
@Nullable private ChipDrawable chipDrawable;
//noinspection NewApi
@Nullable private RippleDrawable ripple;
@Nullable private OnClickListener onCloseIconClickListener;
@Nullable private OnCheckedChangeListener onCheckedChangeListenerInternal;
private boolean deferredCheckedValue;
@VirtualId private int focusedVirtualView = ExploreByTouchHelper.INVALID_ID;
private boolean closeIconPressed;
private boolean closeIconHovered;
private boolean closeIconFocused;
private int touchTargetDelegateResId;
@Dimension(unit = Dimension.PX)
private int minTouchTargetSize;
private final ChipTouchHelper touchHelper;
private final Rect rect = new Rect();
private final RectF rectF = new RectF();
private final FontCallback fontCallback =
new FontCallback() {
@Override
public void onFontRetrieved(@NonNull Typeface typeface) {
// Set text to re-trigger internal ellipsize width calculation.
setText(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(context, attrs, defStyleAttr);
validateAttributes(attrs);
ChipDrawable drawable =
ChipDrawable.createFromAttributes(
context, attrs, defStyleAttr, R.style.Widget_MaterialComponents_Chip_Action);
setChipDrawable(drawable);
touchHelper = new ChipTouchHelper(this);
ViewCompat.setAccessibilityDelegate(this, touchHelper);
initOutlineProvider();
// Set deferred values
setChecked(deferredCheckedValue);
// Defers to TextView to draw the text and ChipDrawable to render the
// rest (e.g. chip / check / close icons).
drawable.setShouldDrawText(false);
setText(drawable.getText());
setEllipsize(drawable.getEllipsize());
setIncludeFontPadding(false);
updateTextPaintDrawState();
// Chip text should not extend to more than 1 line.
setSingleLine();
// 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();
setupTouchTargetDelegate(context, attrs, defStyleAttr);
}
private void setupTouchTargetDelegate(Context context, AttributeSet attrs, int defStyleAttr) {
if (attrs == null) {
return;
}
// Checks if the Chip should meet Android's minimum touch target size.
TypedArray a =
ThemeEnforcement.obtainStyledAttributes(
context,
attrs,
R.styleable.Chip,
defStyleAttr,
R.style.Widget_MaterialComponents_Chip_Action);
touchTargetDelegateResId =
attrs.getAttributeIntValue(NAMESPACE_APP, "chipTouchTargetDelegate", -1);
touchTargetDelegateResId =
touchTargetDelegateResId > 0
? touchTargetDelegateResId
: a.getResourceId(R.styleable.Chip_chipTouchTargetDelegate, -1);
if (touchTargetDelegateResId <= 0) {
return;
}
if (attrs.getAttributeValue(NAMESPACE_APP, "chipMinTouchTargetSize") != null) {
minTouchTargetSize =
attrs.getAttributeIntValue(NAMESPACE_APP, "chipMinTouchTargetSize", MIN_TOUCH_TARGET_DP);
minTouchTargetSize = (int) Math.ceil(dpToPx(minTouchTargetSize, getContext()));
} else {
minTouchTargetSize =
(int)
Math.ceil(
a.getDimension(R.styleable.Chip_chipMinTouchTargetSize, MIN_TOUCH_TARGET_DP));
}
ViewTreeObserver viewTreeObserver = getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ensureAccessibleTouchTarget(
TouchTargetUtils.findViewAncestor(Chip.this, touchTargetDelegateResId),
minTouchTargetSize);
touchTargetDelegateResId = -1;
if (Build.VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
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 (ViewCompat.getPaddingEnd(this) != paddingEnd
|| ViewCompat.getPaddingStart(this) != paddingStart) {
ViewCompat.setPaddingRelative(
this, paddingStart, getPaddingTop(), paddingEnd, getPaddingBottom());
}
}
private void validateAttributes(@Nullable AttributeSet attributeSet) {
if (attributeSet == null) {
return;
}
if (attributeSet.getAttributeValue(NAMESPACE_ANDROID, "background") != null) {
throw new UnsupportedOperationException(
"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, 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;
applyChipDrawable(chipDrawable);
if (RippleUtils.USE_FRAMEWORK_RIPPLE) {
//noinspection NewApi
ripple =
new RippleDrawable(
RippleUtils.convertToRippleDrawableColor(chipDrawable.getRippleColor()),
chipDrawable,
null);
chipDrawable.setUseCompatRipple(false);
//noinspection NewApi
ViewCompat.setBackground(this, ripple);
} else {
chipDrawable.setUseCompatRipple(true);
ViewCompat.setBackground(this, chipDrawable);
}
}
}
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 + 1);
if (isChecked()) {
mergeDrawableStates(state, SELECTED_STATE);
}
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) {
throw new UnsupportedOperationException(
"Do not set the background tint list; Chip manages its own background drawable.");
}
@Override
public void setBackgroundTintMode(@Nullable Mode tintMode) {
throw new UnsupportedOperationException(
"Do not set the background tint mode; Chip manages its own background drawable.");
}
@Override
public void setBackgroundColor(int color) {
throw new UnsupportedOperationException(
"Do not set the background color; Chip manages its own background drawable.");
}
@Override
public void setBackgroundResource(int resid) {
throw new UnsupportedOperationException(
"Do not set the background resource; Chip manages its own background drawable.");
}
@Override
public void setBackground(Drawable background) {
if (background != chipDrawable && background != ripple) {
throw new UnsupportedOperationException(
"Do not set the background; Chip manages its own background drawable.");
} else {
super.setBackground(background);
}
}
@Override
public void setBackgroundDrawable(Drawable background) {
if (background != chipDrawable && background != ripple) {
throw new UnsupportedOperationException(
"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);
}
@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);
}
@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() {
updatePaddingInternal();
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()) {
boolean wasChecked = isChecked();
super.setChecked(checked);
if (wasChecked != checked) {
if (onCheckedChangeListenerInternal != null) {
onCheckedChangeListenerInternal.onCheckedChanged(this, 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;
}
/**
* Call this chip's {@link #onCloseIconClickListener}, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing a sound, etc.
*
* @return True there was an assigned {@link #onCloseIconClickListener} that was called, false
* otherwise is returned.
*/
@CallSuper
public boolean performCloseIconClick() {
playSoundEffect(SoundEffectConstants.CLICK);
boolean result;
if (onCloseIconClickListener != null) {
onCloseIconClickListener.onClick(this);
result = true;
} else {
result = false;
}
touchHelper.sendEventForVirtualView(
CLOSE_ICON_VIRTUAL_ID, AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
@Override
public boolean onTouchEvent(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(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);
}
// There is a bug which causes the AccessibilityEvent.TYPE_VIEW_HOVER_ENTER and
// AccessibilityEvent.TYPE_VIEW_HOVER_EXIT events to only fire the first time a chip gets focused.
// Until the accessibility focus bug is fixed in ExploreByTouchHelper, we simulate the correct
// behavior here. Once that bug is fixed we can remove this.
@SuppressLint("PrivateApi")
private boolean handleAccessibilityExit(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
try {
Field f = ExploreByTouchHelper.class.getDeclaredField("mHoveredVirtualViewId");
f.setAccessible(true);
int mHoveredVirtualViewId = (int) f.get(touchHelper);
if (mHoveredVirtualViewId != ExploreByTouchHelper.INVALID_ID) {
Method m =
ExploreByTouchHelper.class.getDeclaredMethod("updateHoveredVirtualView", int.class);
m.setAccessible(true);
m.invoke(touchHelper, ExploreByTouchHelper.INVALID_ID);
return true;
}
} catch (NoSuchMethodException e) {
// Multi-catch for reflection requires API level 19
Log.e(TAG, "Unable to send Accessibility Exit event", e);
} catch (IllegalAccessException e) {
// Multi-catch for reflection requires API level 19
Log.e(TAG, "Unable to send Accessibility Exit event", e);
} catch (InvocationTargetException e) {
// Multi-catch for reflection requires API level 19
Log.e(TAG, "Unable to send Accessibility Exit event", e);
} catch (NoSuchFieldException e) {
// Multi-catch for reflection requires API level 19
Log.e(TAG, "Unable to send Accessibility Exit event", e);
}
}
return false;
}
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
return handleAccessibilityExit(event)
|| touchHelper.dispatchHoverEvent(event)
|| super.dispatchHoverEvent(event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return touchHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (focused) {
// If we've gained focus from another view, always focus the chip first.
setFocusedVirtualView(ExploreByTouchHelper.HOST_ID);
} else {
setFocusedVirtualView(ExploreByTouchHelper.INVALID_ID);
}
invalidate();
super.onFocusChanged(focused, direction, previouslyFocusedRect);
touchHelper.onFocusChanged(focused, direction, previouslyFocusedRect);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// We need to handle focus change within the Chip because we are simulating multiple Views. The
// left/right arrow keys will move between the chip and the close icon. Focus
// up/down/forward/back jumps out of the Chip to the next focusable View in the hierarchy.
boolean focusChanged = false;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
focusChanged = moveFocus(ViewUtils.isLayoutRtl(this));
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
focusChanged = moveFocus(!ViewUtils.isLayoutRtl(this));
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
switch (focusedVirtualView) {
case ExploreByTouchHelper.HOST_ID:
performClick();
return true;
case CLOSE_ICON_VIRTUAL_ID:
performCloseIconClick();
return true;
case ExploreByTouchHelper.INVALID_ID:
default:
break;
}
break;
case KeyEvent.KEYCODE_TAB:
int focusChangeDirection = 0;
if (event.hasNoModifiers()) {
focusChangeDirection = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
focusChangeDirection = View.FOCUS_BACKWARD;
}
if (focusChangeDirection != 0) {
final ViewParent parent = getParent();
// Move focus out of this view.
View nextFocus = this;
do {
nextFocus = nextFocus.focusSearch(focusChangeDirection);
} while (nextFocus != null && nextFocus != this && nextFocus.getParent() == parent);
if (nextFocus != null) {
nextFocus.requestFocus();
return true;
}
}
break;
default:
break;
}
if (focusChanged) {
invalidate();
return true;
} else {
return super.onKeyDown(keyCode, event);
}
}
private boolean moveFocus(boolean positive) {
ensureFocus();
boolean focusChanged = false;
if (positive) {
if (focusedVirtualView == ExploreByTouchHelper.HOST_ID) {
setFocusedVirtualView(CLOSE_ICON_VIRTUAL_ID);
focusChanged = true;
}
} else {
if (focusedVirtualView == CLOSE_ICON_VIRTUAL_ID) {
setFocusedVirtualView(ExploreByTouchHelper.HOST_ID);
focusChanged = true;
}
}
return focusChanged;
}
private void ensureFocus() {
if (focusedVirtualView == ExploreByTouchHelper.INVALID_ID) {
setFocusedVirtualView(ExploreByTouchHelper.HOST_ID);
}
}
@Override
public void getFocusedRect(Rect r) {
if (focusedVirtualView == CLOSE_ICON_VIRTUAL_ID) {
r.set(getCloseIconTouchBoundsInt());
} else {
super.getFocusedRect(r);
}
}
private void setFocusedVirtualView(@VirtualId int virtualView) {
if (focusedVirtualView != virtualView) {
if (focusedVirtualView == CLOSE_ICON_VIRTUAL_ID) {
setCloseIconFocused(false);
}
focusedVirtualView = virtualView;
if (virtualView == CLOSE_ICON_VIRTUAL_ID) {
setCloseIconFocused(true);
}
}
}
private void setCloseIconPressed(boolean pressed) {
if (closeIconPressed != pressed) {
closeIconPressed = pressed;
refreshDrawableState();
}
}
private void setCloseIconHovered(boolean hovered) {
if (closeIconHovered != hovered) {
closeIconHovered = hovered;
refreshDrawableState();
}
}
private void setCloseIconFocused(boolean focused) {
if (closeIconFocused != focused) {
closeIconFocused = focused;
refreshDrawableState();
}
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
boolean changed = false;
if (chipDrawable != null && chipDrawable.isCloseIconStateful()) {
changed = chipDrawable.setCloseIconState(createCloseIconDrawableState());
}
if (changed) {
invalidate();
}
}
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;
}
private RectF getCloseIconTouchBounds() {
rectF.setEmpty();
if (hasCloseIcon()) {
// noinspection ConstantConditions
chipDrawable.getCloseIconTouchBounds(rectF);
}
return rectF;
}
private Rect getCloseIconTouchBoundsInt() {
RectF bounds = getCloseIconTouchBounds();
rect.set((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom);
return rect;
}
@Override
@TargetApi(VERSION_CODES.N)
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
if (getCloseIconTouchBounds().contains(event.getX(), event.getY()) && isEnabled()) {
return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
}
return null;
}
/** 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
: HOST_ID;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
if (hasCloseIcon()) {
virtualViewIds.add(CLOSE_ICON_VIRTUAL_ID);
}
}
@Override
protected void onPopulateNodeForVirtualView(
int virtualViewId, AccessibilityNodeInfoCompat node) {
if (hasCloseIcon()) {
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(AccessibilityNodeInfoCompat node) {
node.setCheckable(chipDrawable != null && chipDrawable.isCheckable());
node.setClassName(Chip.class.getName());
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
&& virtualViewId == CLOSE_ICON_VIRTUAL_ID) {
return performCloseIconClick();
}
return false;
}
}
// Getters and setters for attributes.
@Nullable
public ColorStateList getChipBackgroundColor() {
return chipDrawable != null ? chipDrawable.getChipBackgroundColor() : null;
}
public void setChipBackgroundColorResource(@ColorRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipBackgroundColorResource(id);
}
}
public void setChipBackgroundColor(@Nullable ColorStateList chipBackgroundColor) {
if (chipDrawable != null) {
chipDrawable.setChipBackgroundColor(chipBackgroundColor);
}
}
public float getChipMinHeight() {
return chipDrawable != null ? chipDrawable.getChipMinHeight() : 0;
}
public void setChipMinHeightResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipMinHeightResource(id);
}
}
public void setChipMinHeight(float minHeight) {
if (chipDrawable != null) {
chipDrawable.setChipMinHeight(minHeight);
}
}
public float getChipCornerRadius() {
return chipDrawable != null ? chipDrawable.getChipCornerRadius() : 0;
}
public void setChipCornerRadiusResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipCornerRadiusResource(id);
}
}
public void setChipCornerRadius(float chipCornerRadius) {
if (chipDrawable != null) {
chipDrawable.setChipCornerRadius(chipCornerRadius);
}
}
@Nullable
public ColorStateList getChipStrokeColor() {
return chipDrawable != null ? chipDrawable.getChipStrokeColor() : null;
}
public void setChipStrokeColorResource(@ColorRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipStrokeColorResource(id);
}
}
public void setChipStrokeColor(@Nullable ColorStateList chipStrokeColor) {
if (chipDrawable != null) {
chipDrawable.setChipStrokeColor(chipStrokeColor);
}
}
public float getChipStrokeWidth() {
return chipDrawable != null ? chipDrawable.getChipStrokeWidth() : 0;
}
public void setChipStrokeWidthResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipStrokeWidthResource(id);
}
}
public void setChipStrokeWidth(float chipStrokeWidth) {
if (chipDrawable != null) {
chipDrawable.setChipStrokeWidth(chipStrokeWidth);
}
}
@Nullable
public ColorStateList getRippleColor() {
return chipDrawable != null ? chipDrawable.getRippleColor() : null;
}
public void setRippleColorResource(@ColorRes int id) {
if (chipDrawable != null) {
chipDrawable.setRippleColorResource(id);
}
}
public void setRippleColor(@Nullable ColorStateList rippleColor) {
if (chipDrawable != null) {
chipDrawable.setRippleColor(rippleColor);
}
}
/** @deprecated Use {@link Chip#getText()} instead. */
@Deprecated
public CharSequence getChipText() {
return getText();
}
@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);
}
public void setTextAppearanceResource(@StyleRes int id) {
this.setTextAppearance(getContext(), id);
}
public void setTextAppearance(@Nullable TextAppearance textAppearance) {
// TODO: Make sure this also updates parent TextView styles.
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();
}
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;
}
public boolean isChipIconVisible() {
return chipDrawable != null && chipDrawable.isChipIconVisible();
}
/** @deprecated Use {@link Chip#isChipIconVisible()} instead. */
@Deprecated
public boolean isChipIconEnabled() {
return isChipIconVisible();
}
public void setChipIconVisible(@BoolRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipIconVisible(id);
}
}
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);
}
@Nullable
public Drawable getChipIcon() {
return chipDrawable != null ? chipDrawable.getChipIcon() : null;
}
public void setChipIconResource(@DrawableRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipIconResource(id);
}
}
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. */
@Nullable
public ColorStateList getChipIconTint() {
return chipDrawable != null ? chipDrawable.getChipIconTint() : null;
}
/**
* Sets the chip icon's color tint using a resource ID.
*
* @param id Resource id of a {@link android.content.res.ColorStateList} to tint 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 the chip icon's color tint using the specified {@link android.content.res.ColorStateList}.
*
* @param chipIconTint ColorStateList to tint the chip icon.
* @attr ref com.google.android.material.R.styleable#Chip_chipIconTint
*/
public void setChipIconTint(@Nullable ColorStateList chipIconTint) {
if (chipDrawable != null) {
chipDrawable.setChipIconTint(chipIconTint);
}
}
public float getChipIconSize() {
return chipDrawable != null ? chipDrawable.getChipIconSize() : 0;
}
public void setChipIconSizeResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipIconSizeResource(id);
}
}
public void setChipIconSize(float chipIconSize) {
if (chipDrawable != null) {
chipDrawable.setChipIconSize(chipIconSize);
}
}
public boolean isCloseIconVisible() {
return chipDrawable != null && chipDrawable.isCloseIconVisible();
}
/** @deprecated Use {@link Chip#isCloseIconVisible()} instead. */
@Deprecated
public boolean isCloseIconEnabled() {
return isCloseIconVisible();
}
public void setCloseIconVisible(@BoolRes int id) {
if (chipDrawable != null) {
chipDrawable.setCloseIconVisible(id);
}
}
public void setCloseIconVisible(boolean closeIconVisible) {
if (chipDrawable != null) {
chipDrawable.setCloseIconVisible(closeIconVisible);
}
}
/** @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);
}
@Nullable
public Drawable getCloseIcon() {
return chipDrawable != null ? chipDrawable.getCloseIcon() : null;
}
public void setCloseIconResource(@DrawableRes int id) {
if (chipDrawable != null) {
chipDrawable.setCloseIconResource(id);
}
}
public void setCloseIcon(@Nullable Drawable closeIcon) {
if (chipDrawable != null) {
chipDrawable.setCloseIcon(closeIcon);
}
}
@Nullable
public ColorStateList getCloseIconTint() {
return chipDrawable != null ? chipDrawable.getCloseIconTint() : null;
}
public void setCloseIconTintResource(@ColorRes int id) {
if (chipDrawable != null) {
chipDrawable.setCloseIconTintResource(id);
}
}
public void setCloseIconTint(@Nullable ColorStateList closeIconTint) {
if (chipDrawable != null) {
chipDrawable.setCloseIconTint(closeIconTint);
}
}
public float getCloseIconSize() {
return chipDrawable != null ? chipDrawable.getCloseIconSize() : 0;
}
public void setCloseIconSizeResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setCloseIconSizeResource(id);
}
}
public void setCloseIconSize(float closeIconSize) {
if (chipDrawable != null) {
chipDrawable.setCloseIconSize(closeIconSize);
}
}
public void setCloseIconContentDescription(@Nullable CharSequence closeIconContentDescription) {
if (chipDrawable != null) {
chipDrawable.setCloseIconContentDescription(closeIconContentDescription);
}
}
@Nullable
public CharSequence getCloseIconContentDescription() {
return chipDrawable != null ? chipDrawable.getCloseIconContentDescription() : null;
}
public boolean isCheckable() {
return chipDrawable != null && chipDrawable.isCheckable();
}
public void setCheckableResource(@BoolRes int id) {
if (chipDrawable != null) {
chipDrawable.setCheckableResource(id);
}
}
public void setCheckable(boolean checkable) {
if (chipDrawable != null) {
chipDrawable.setCheckable(checkable);
}
}
public boolean isCheckedIconVisible() {
return chipDrawable != null && chipDrawable.isCheckedIconVisible();
}
/** @deprecated Use {@link Chip#isCheckedIconVisible()} instead. */
@Deprecated
public boolean isCheckedIconEnabled() {
return isCheckedIconVisible();
}
public void setCheckedIconVisible(@BoolRes int id) {
if (chipDrawable != null) {
chipDrawable.setCheckedIconVisible(id);
}
}
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);
}
@Nullable
public Drawable getCheckedIcon() {
return chipDrawable != null ? chipDrawable.getCheckedIcon() : null;
}
public void setCheckedIconResource(@DrawableRes int id) {
if (chipDrawable != null) {
chipDrawable.setCheckedIconResource(id);
}
}
public void setCheckedIcon(@Nullable Drawable checkedIcon) {
if (chipDrawable != null) {
chipDrawable.setCheckedIcon(checkedIcon);
}
}
@Nullable
public MotionSpec getShowMotionSpec() {
return chipDrawable != null ? chipDrawable.getShowMotionSpec() : null;
}
public void setShowMotionSpecResource(@AnimatorRes int id) {
if (chipDrawable != null) {
chipDrawable.setShowMotionSpecResource(id);
}
}
public void setShowMotionSpec(@Nullable MotionSpec showMotionSpec) {
if (chipDrawable != null) {
chipDrawable.setShowMotionSpec(showMotionSpec);
}
}
@Nullable
public MotionSpec getHideMotionSpec() {
return chipDrawable != null ? chipDrawable.getHideMotionSpec() : null;
}
public void setHideMotionSpecResource(@AnimatorRes int id) {
if (chipDrawable != null) {
chipDrawable.setHideMotionSpecResource(id);
}
}
public void setHideMotionSpec(@Nullable MotionSpec hideMotionSpec) {
if (chipDrawable != null) {
chipDrawable.setHideMotionSpec(hideMotionSpec);
}
}
public float getChipStartPadding() {
return chipDrawable != null ? chipDrawable.getChipStartPadding() : 0;
}
public void setChipStartPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipStartPaddingResource(id);
}
}
public void setChipStartPadding(float chipStartPadding) {
if (chipDrawable != null) {
chipDrawable.setChipStartPadding(chipStartPadding);
}
}
public float getIconStartPadding() {
return chipDrawable != null ? chipDrawable.getIconStartPadding() : 0;
}
public void setIconStartPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setIconStartPaddingResource(id);
}
}
public void setIconStartPadding(float iconStartPadding) {
if (chipDrawable != null) {
chipDrawable.setIconStartPadding(iconStartPadding);
}
}
public float getIconEndPadding() {
return chipDrawable != null ? chipDrawable.getIconEndPadding() : 0;
}
public void setIconEndPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setIconEndPaddingResource(id);
}
}
public void setIconEndPadding(float iconEndPadding) {
if (chipDrawable != null) {
chipDrawable.setIconEndPadding(iconEndPadding);
}
}
public float getTextStartPadding() {
return chipDrawable != null ? chipDrawable.getTextStartPadding() : 0;
}
public void setTextStartPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setTextStartPaddingResource(id);
}
}
public void setTextStartPadding(float textStartPadding) {
if (chipDrawable != null) {
chipDrawable.setTextStartPadding(textStartPadding);
}
}
public float getTextEndPadding() {
return chipDrawable != null ? chipDrawable.getTextEndPadding() : 0;
}
public void setTextEndPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setTextEndPaddingResource(id);
}
}
public void setTextEndPadding(float textEndPadding) {
if (chipDrawable != null) {
chipDrawable.setTextEndPadding(textEndPadding);
}
}
public float getCloseIconStartPadding() {
return chipDrawable != null ? chipDrawable.getCloseIconStartPadding() : 0;
}
public void setCloseIconStartPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setCloseIconStartPaddingResource(id);
}
}
public void setCloseIconStartPadding(float closeIconStartPadding) {
if (chipDrawable != null) {
chipDrawable.setCloseIconStartPadding(closeIconStartPadding);
}
}
public float getCloseIconEndPadding() {
return chipDrawable != null ? chipDrawable.getCloseIconEndPadding() : 0;
}
public void setCloseIconEndPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setCloseIconEndPaddingResource(id);
}
}
public void setCloseIconEndPadding(float closeIconEndPadding) {
if (chipDrawable != null) {
chipDrawable.setCloseIconEndPadding(closeIconEndPadding);
}
}
public float getChipEndPadding() {
return chipDrawable != null ? chipDrawable.getChipEndPadding() : 0;
}
public void setChipEndPaddingResource(@DimenRes int id) {
if (chipDrawable != null) {
chipDrawable.setChipEndPaddingResource(id);
}
}
public void setChipEndPadding(float chipEndPadding) {
if (chipDrawable != null) {
chipDrawable.setChipEndPadding(chipEndPadding);
}
}
/**
* Extends the tap target of this chip using a {@link TouchDelegate}.
*
* <p>This also adds an OnAttachStateChangeListener to the view to remove the TouchDelegate from
* its ancestor when it is detached from its parent.
*
* <p>What this means for views which are part of a reusable layout is that you should call this
* method to extend its tap target every time it is attached to a new ancestor.
*
* @param ancestor an ancestor of the given view. This ancestor must have bounds which include the
* extended tap target.
* @param minTargetPx minimum toucch target size in pixel.
*/
public void ensureAccessibleTouchTarget(View ancestor, @Dimension int minTargetPx) {
int deltaHeight = Math.max(0, minTargetPx - getHeight());
int deltaWidth = Math.max(0, minTargetPx - getWidth());
int deltaX = deltaWidth > 0 ? deltaWidth / 2 : 0;
int deltaY = deltaHeight > 0 ? deltaHeight / 2 : 0;
TouchTargetUtils.extendViewTouchTarget(this, ancestor, deltaX, deltaY, deltaX, deltaY);
}
@Dimension(unit = Dimension.PX)
private static float dpToPx(@Dimension(unit = Dimension.DP) int dp, Context context) {
Resources r = context.getResources();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
}
}