/* * 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 android.view.accessibility.AccessibilityNodeInfo; 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. * *

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 {@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: * *

* *

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)}. * *

For proper rendering of the ancestor TextView in RTL mode, call {@link * #setLayoutDirection(int)} with {@link 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. * * @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); if (VERSION.SDK_INT >= VERSION_CODES.N) { ViewCompat.setAccessibilityDelegate(this, touchHelper); } else { updateAccessibilityDelegate(); } 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); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(Chip.class.getName()); info.setCheckable(isCheckable()); info.setClickable(isClickable()); } // TODO: Due to a11y bug, avoid setting custom ExploreByTouchHelper as delegate // unless there's a close/trailing icon. Revert this once bug is fixed. private void updateAccessibilityDelegate() { if (VERSION.SDK_INT >= VERSION_CODES.N) { return; } if ((hasCloseIcon() && isCloseIconVisible())) { ViewCompat.setAccessibilityDelegate(this, touchHelper); } else { ViewCompat.setAccessibilityDelegate(this, null); } } 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) { updateFrameworkRippleBackground(); } else { chipDrawable.setUseCompatRipple(true); ViewCompat.setBackground(this, chipDrawable); } } } private void updateFrameworkRippleBackground() { //noinspection NewApi ripple = new RippleDrawable( RippleUtils.convertToRippleDrawableColor(chipDrawable.getRippleColor()), chipDrawable, null); chipDrawable.setUseCompatRipple(false); //noinspection NewApi ViewCompat.setBackground(this, ripple); } 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 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(isCheckable()); node.setClickable(isClickable()); 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); if (!chipDrawable.getUseCompatRipple()) { updateFrameworkRippleBackground(); } } } public void setRippleColor(@Nullable ColorStateList rippleColor) { if (chipDrawable != null) { chipDrawable.setRippleColor(rippleColor); } if (!chipDrawable.getUseCompatRipple()) { updateFrameworkRippleBackground(); } } /** @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.KITKAT) { super.setLayoutDirection(layoutDirection); } else { ViewCompat.setLayoutDirection(this, layoutDirection); } chipDrawable.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); } 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) { setCloseIconVisible(getResources().getBoolean(id)); } 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); } @Nullable public Drawable getCloseIcon() { return chipDrawable != null ? chipDrawable.getCloseIcon() : null; } public void setCloseIconResource(@DrawableRes int id) { if (chipDrawable != null) { chipDrawable.setCloseIconResource(id); } updateAccessibilityDelegate(); } public void setCloseIcon(@Nullable Drawable closeIcon) { if (chipDrawable != null) { chipDrawable.setCloseIcon(closeIcon); } updateAccessibilityDelegate(); } @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}. * *

This also adds an OnAttachStateChangeListener to the view to remove the TouchDelegate from * its ancestor when it is detached from its parent. * *

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