leticiars 3f73804b57 Implementing error icon for text fields.
They are set by default when the text field is on error state, but can be disabled by setting the error icon drawable to null via the errorIconDrawable attribute or the setErrorIconDrawable method.

PiperOrigin-RevId: 260495196
2019-07-29 16:43:16 -04:00

3247 lines
119 KiB
Java

/*
* Copyright (C) 2015 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
*
* http://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.textfield;
import com.google.android.material.R;
import static com.google.android.material.internal.ThemeEnforcement.createThemedContext;
import static com.google.android.material.textfield.IndicatorViewController.COUNTER_INDEX;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.customview.view.AbsSavedState;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.GravityCompat;
import androidx.core.view.MarginLayoutParamsCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.widget.TextViewCompat;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatDrawableManager;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.appcompat.widget.TintTypedArray;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
import android.view.accessibility.AccessibilityEvent;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.google.android.material.animation.AnimationUtils;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.internal.CheckableImageButton;
import com.google.android.material.internal.CollapsingTextHelper;
import com.google.android.material.internal.DescendantOffsetUtils;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedHashSet;
/**
* Layout which wraps a {@link TextInputEditText}, {@link android.widget.EditText}, or descendant to
* show a floating label when the hint is hidden while the user inputs text.
*
* <p>Also supports:
*
* <ul>
* <li>Showing an error via {@link #setErrorEnabled(boolean)} and {@link #setError(CharSequence)}
* <li>Showing helper text via {@link #setHelperTextEnabled(boolean)} and {@link
* #setHelperText(CharSequence)}
* <li>Showing a character counter via {@link #setCounterEnabled(boolean)} and {@link
* #setCounterMaxLength(int)}
* <li>Password visibility toggling via {@link #setEndIconMode(int)} API and related attribute. If
* set, a button is displayed to toggle between the password being displayed as plain-text or
* disguised, when your EditText is set to display a password.
* <li>Clearing text functionality via {@link #setEndIconMode(int)} API and related attribute. If
* set, a button is displayed when text is present and clicking it clears the EditText field.
* <li>Showing a custom icon specified via {@link #setEndIconMode(int)} API and related attribute.
* You should specify a drawable and content description for the icon. Optionally, you can
* also specify an {@link android.view.View.OnClickListener}, an {@link
* OnEditTextAttachedListener} and an {@link OnEndIconChangedListener}.
* <p><strong>Note:</strong> When using an end icon, the 'end' compound drawable of the
* EditText will be overridden while the end icon view is visible. To ensure that any existing
* drawables are restored correctly, you should set those compound drawables relatively
* (start/end), as opposed to absolutely (left/right).
* <li>Showing a start icon via {@link #setStartIconDrawable(Drawable)} API and related attribute.
* You should specify a content description for the icon. Optionally, you can also specify an
* {@link android.view.View.OnClickListener} for it.
* <p><strong>Note:</strong> Use the {@link #setStartIconDrawable(Drawable)} API in place of
* setting a start/left compound drawable on the EditText. When using a start icon, the
* 'start/left' compound drawable of the EditText will be overridden.
* <li>Showing a button that when clicked displays a dropdown menu. The selected option is
* displayed above the dropdown. You need to use an {@link AutoCompleteTextView} instead of a
* {@link TextInputEditText} as the input text child, and a
* Widget.MaterialComponents.TextInputLayout.(...).ExposedDropdownMenu style.
* <p>To disable user input you should set
* <pre>android:editable=&quot;false&quot;</pre>
* on the {@link AutoCompleteTextView}.
* </ul>
*
* <p>The {@link TextInputEditText} class is provided to be used as the input text child of this
* layout. Using TextInputEditText instead of an EditText provides accessibility support for the
* text field and allows TextInputLayout greater control over the visual aspects of the text field.
* An example usage is as so:
*
* <pre>
* &lt;com.google.android.material.textfield.TextInputLayout
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;wrap_content&quot;
* android:hint=&quot;@string/form_username&quot;&gt;
*
* &lt;com.google.android.material.textfield.TextInputEditText
* android:layout_width=&quot;match_parent&quot;
* android:layout_height=&quot;wrap_content&quot;/&gt;
*
* &lt;/com.google.android.material.textfield.TextInputLayout&gt;
* </pre>
*
* The hint should be set on the TextInputLayout, rather than the EditText. If a hint is specified
* on the child EditText in XML, the TextInputLayout might still work correctly; TextInputLayout
* will use the EditText's hint as its floating label. However, future calls to modify the hint will
* not update TextInputLayout's hint. To avoid unintended behavior, call {@link
* TextInputLayout#setHint(CharSequence)} and {@link TextInputLayout#getHint()} on TextInputLayout,
* instead of on EditText.
*
* <p>If the {@link EditText} child is not a {@link TextInputEditText}, make sure to set the {@link
* EditText}'s {@code android:background} to {@code null} when using an outlined or filled text
* field. This allows {@link TextInputLayout} to set the {@link EditText}'s background to an
* outlined or filled box, respectively.
*
* <p><strong>Note:</strong> The actual view hierarchy present under TextInputLayout is
* <strong>NOT</strong> guaranteed to match the view hierarchy as written in XML. As a result, calls
* to getParent() on children of the TextInputLayout -- such as a TextInputEditText -- may not
* return the TextInputLayout itself, but rather an intermediate View. If you need to access a View
* directly, set an {@code android:id} and use {@link View#findViewById(int)}.
*/
public class TextInputLayout extends LinearLayout {
private static final int DEF_STYLE_RES = R.style.Widget_Design_TextInputLayout;
/** Duration for the label's scale up and down animations. */
private static final int LABEL_SCALE_ANIMATION_DURATION = 167;
private static final int INVALID_MAX_LENGTH = -1;
private static final String LOG_TAG = "TextInputLayout";
private final FrameLayout inputFrame;
EditText editText;
private CharSequence originalHint;
private final IndicatorViewController indicatorViewController = new IndicatorViewController(this);
boolean counterEnabled;
private int counterMaxLength;
private boolean counterOverflowed;
private TextView counterView;
private int counterOverflowTextAppearance;
private int counterTextAppearance;
@Nullable private ColorStateList counterTextColor;
@Nullable private ColorStateList counterOverflowTextColor;
private boolean hintEnabled;
private CharSequence hint;
/**
* {@code true} when providing a hint on behalf of a child {@link EditText}. If the child is an
* instance of {@link TextInputEditText}, this value defines the behavior of its {@link
* TextInputEditText#getHint()} method.
*/
private boolean isProvidingHint;
private MaterialShapeDrawable boxBackground;
private MaterialShapeDrawable boxUnderline;
private final ShapeAppearanceModel shapeAppearanceModel;
private final int boxLabelCutoutPaddingPx;
@BoxBackgroundMode private int boxBackgroundMode;
private final int boxCollapsedPaddingTopPx;
private int boxStrokeWidthPx;
private final int boxStrokeWidthDefaultPx;
private final int boxStrokeWidthFocusedPx;
@ColorInt private int boxStrokeColor;
@ColorInt private int boxBackgroundColor;
/**
* Values for box background mode. There is either a filled background, an outline background, or
* no background.
*/
@IntDef({BOX_BACKGROUND_NONE, BOX_BACKGROUND_FILLED, BOX_BACKGROUND_OUTLINE})
@Retention(RetentionPolicy.SOURCE)
public @interface BoxBackgroundMode {}
public static final int BOX_BACKGROUND_NONE = 0;
public static final int BOX_BACKGROUND_FILLED = 1;
public static final int BOX_BACKGROUND_OUTLINE = 2;
private final Rect tmpRect = new Rect();
private final Rect tmpBoundsRect = new Rect();
private final RectF tmpRectF = new RectF();
private Typeface typeface;
private final CheckableImageButton startIconView;
private ColorStateList startIconTintList;
private boolean hasStartIconTintList;
private PorterDuff.Mode startIconTintMode;
private boolean hasStartIconTintMode;
private Drawable startIconDummyDrawable;
/** Values for the end icon mode. */
@IntDef({
END_ICON_CUSTOM,
END_ICON_NONE,
END_ICON_PASSWORD_TOGGLE,
END_ICON_CLEAR_TEXT,
END_ICON_DROPDOWN_MENU
})
@Retention(RetentionPolicy.SOURCE)
public @interface EndIconMode {}
/**
* The TextInputLayout will show a custom icon specified by the user.
*
* @see #setEndIconMode(int)
* @see #getEndIconMode()
* @see #setEndIconDrawable(Drawable)
* @see #setEndIconContentDescription(CharSequence)
* @see #setEndIconOnClickListener(OnClickListener) (optionally)
* @see #addOnEditTextAttachedListener(OnEditTextAttachedListener) (optionally)
* @see #addOnEndIconChangedListener(OnEndIconChangedListener) (optionally)
*/
public static final int END_ICON_CUSTOM = -1;
/**
* Default for the TextInputLayout. It will not display an end icon.
*
* @see #setEndIconMode(int)
* @see #getEndIconMode()
*/
public static final int END_ICON_NONE = 0;
/**
* The TextInputLayout will show a password toggle button if its EditText displays a password.
* When this end icon is clicked, the password is shown as plain-text if it was disguised, or
* vice-versa.
*
* @see #setEndIconMode(int)
* @see #getEndIconMode()
*/
public static final int END_ICON_PASSWORD_TOGGLE = 1;
/**
* The TextInputLayout will show a clear text button while there is input in the EditText.
* Clicking it will clear out the text and hide the icon.
*
* @see #setEndIconMode(int)
* @see #getEndIconMode()
*/
public static final int END_ICON_CLEAR_TEXT = 2;
/**
* The TextInputLayout will show a dropdown button if the EditText is an {@link
* AutoCompleteTextView} and a {@code
* Widget.MaterialComponents.TextInputLayout.(...).ExposedDropdownMenu} style is being used.
*
* <p>Clicking the button will display a popup with a list of options. The current selected option
* is displayed on the EditText.
*/
public static final int END_ICON_DROPDOWN_MENU = 3;
/**
* Callback interface invoked when the view's {@link EditText} is attached, or from {@link
* #addOnEditTextAttachedListener(OnEditTextAttachedListener)} if the edit text is already
* present.
*
* @see #addOnEditTextAttachedListener(OnEditTextAttachedListener)
*/
public interface OnEditTextAttachedListener {
/**
* Called when the {@link EditText} is attached, or from {@link
* #addOnEditTextAttachedListener(OnEditTextAttachedListener)} if the edit text is already
* present.
*
* @param editText the {@link EditText}
*/
void onEditTextAttached(EditText editText);
}
/**
* Callback interface invoked when the view's end icon changes.
*
* @see #setEndIconMode(int)
*/
public interface OnEndIconChangedListener {
/**
* Called when the end icon changes.
*
* @param previousIcon the {@link EndIconMode} the view previously had set
*/
void onEndIconChanged(@EndIconMode int previousIcon);
}
private final LinkedHashSet<OnEditTextAttachedListener> editTextAttachedListeners =
new LinkedHashSet<>();
@EndIconMode private int endIconMode = END_ICON_NONE;
private final SparseArray<EndIconDelegate> endIconDelegates = new SparseArray<>();
private final CheckableImageButton endIconView;
private final LinkedHashSet<OnEndIconChangedListener> endIconChangedListeners =
new LinkedHashSet<>();
private ColorStateList endIconTintList;
private boolean hasEndIconTintList;
private PorterDuff.Mode endIconTintMode;
private boolean hasEndIconTintMode;
private Drawable endIconDummyDrawable;
private Drawable originalEditTextEndDrawable;
private final CheckableImageButton errorIconView;
private ColorStateList defaultHintTextColor;
private ColorStateList focusedTextColor;
@ColorInt private final int defaultStrokeColor;
@ColorInt private final int hoveredStrokeColor;
@ColorInt private int focusedStrokeColor;
@ColorInt private int defaultFilledBackgroundColor;
@ColorInt private final int disabledFilledBackgroundColor;
@ColorInt private final int hoveredFilledBackgroundColor;
@ColorInt private final int disabledColor;
// Only used for testing
private boolean hintExpanded;
final CollapsingTextHelper collapsingTextHelper = new CollapsingTextHelper(this);
private boolean hintAnimationEnabled;
private ValueAnimator animator;
private boolean inDrawableStateChanged;
private boolean restoringSavedState;
public TextInputLayout(Context context) {
this(context, null);
}
public TextInputLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.textInputStyle);
}
public TextInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(createThemedContext(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
// Ensure we are using the correctly themed context rather than the context that was passed in.
context = getContext();
setOrientation(VERTICAL);
setWillNotDraw(false);
setAddStatesFromChildren(true);
inputFrame = new FrameLayout(context);
inputFrame.setAddStatesFromChildren(true);
addView(inputFrame);
collapsingTextHelper.setTextSizeInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
collapsingTextHelper.setPositionInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
collapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
final TintTypedArray a =
ThemeEnforcement.obtainTintedStyledAttributes(
context,
attrs,
R.styleable.TextInputLayout,
defStyleAttr,
DEF_STYLE_RES,
R.styleable.TextInputLayout_counterTextAppearance,
R.styleable.TextInputLayout_counterOverflowTextAppearance,
R.styleable.TextInputLayout_errorTextAppearance,
R.styleable.TextInputLayout_helperTextTextAppearance,
R.styleable.TextInputLayout_hintTextAppearance);
hintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
setHint(a.getText(R.styleable.TextInputLayout_android_hint));
hintAnimationEnabled = a.getBoolean(R.styleable.TextInputLayout_hintAnimationEnabled, true);
shapeAppearanceModel = new ShapeAppearanceModel(context, attrs, defStyleAttr, DEF_STYLE_RES);
boxLabelCutoutPaddingPx =
context
.getResources()
.getDimensionPixelOffset(R.dimen.mtrl_textinput_box_label_cutout_padding);
boxCollapsedPaddingTopPx =
a.getDimensionPixelOffset(R.styleable.TextInputLayout_boxCollapsedPaddingTop, 0);
boxStrokeWidthDefaultPx =
context
.getResources()
.getDimensionPixelSize(R.dimen.mtrl_textinput_box_stroke_width_default);
boxStrokeWidthFocusedPx =
context
.getResources()
.getDimensionPixelSize(R.dimen.mtrl_textinput_box_stroke_width_focused);
boxStrokeWidthPx = boxStrokeWidthDefaultPx;
float boxCornerRadiusTopStart =
a.getDimension(R.styleable.TextInputLayout_boxCornerRadiusTopStart, -1f);
float boxCornerRadiusTopEnd =
a.getDimension(R.styleable.TextInputLayout_boxCornerRadiusTopEnd, -1f);
float boxCornerRadiusBottomEnd =
a.getDimension(R.styleable.TextInputLayout_boxCornerRadiusBottomEnd, -1f);
float boxCornerRadiusBottomStart =
a.getDimension(R.styleable.TextInputLayout_boxCornerRadiusBottomStart, -1f);
if (boxCornerRadiusTopStart >= 0) {
shapeAppearanceModel.getTopLeftCorner().setCornerSize(boxCornerRadiusTopStart);
}
if (boxCornerRadiusTopEnd >= 0) {
shapeAppearanceModel.getTopRightCorner().setCornerSize(boxCornerRadiusTopEnd);
}
if (boxCornerRadiusBottomEnd >= 0) {
shapeAppearanceModel.getBottomRightCorner().setCornerSize(boxCornerRadiusBottomEnd);
}
if (boxCornerRadiusBottomStart >= 0) {
shapeAppearanceModel.getBottomLeftCorner().setCornerSize(boxCornerRadiusBottomStart);
}
ColorStateList filledBackgroundColorStateList =
MaterialResources.getColorStateList(
context, a, R.styleable.TextInputLayout_boxBackgroundColor);
if (filledBackgroundColorStateList != null) {
defaultFilledBackgroundColor = filledBackgroundColorStateList.getDefaultColor();
boxBackgroundColor = defaultFilledBackgroundColor;
if (filledBackgroundColorStateList.isStateful()) {
disabledFilledBackgroundColor =
filledBackgroundColorStateList.getColorForState(
new int[] {-android.R.attr.state_enabled}, -1);
hoveredFilledBackgroundColor =
filledBackgroundColorStateList.getColorForState(
new int[] {android.R.attr.state_hovered}, -1);
} else {
ColorStateList mtrlFilledBackgroundColorStateList =
AppCompatResources.getColorStateList(context, R.color.mtrl_filled_background_color);
disabledFilledBackgroundColor =
mtrlFilledBackgroundColorStateList.getColorForState(
new int[] {-android.R.attr.state_enabled}, -1);
hoveredFilledBackgroundColor =
mtrlFilledBackgroundColorStateList.getColorForState(
new int[] {android.R.attr.state_hovered}, -1);
}
} else {
boxBackgroundColor = Color.TRANSPARENT;
defaultFilledBackgroundColor = Color.TRANSPARENT;
disabledFilledBackgroundColor = Color.TRANSPARENT;
hoveredFilledBackgroundColor = Color.TRANSPARENT;
}
if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) {
defaultHintTextColor =
focusedTextColor = a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint);
}
ColorStateList boxStrokeColorStateList =
MaterialResources.getColorStateList(context, a, R.styleable.TextInputLayout_boxStrokeColor);
if (boxStrokeColorStateList != null && boxStrokeColorStateList.isStateful()) {
defaultStrokeColor = boxStrokeColorStateList.getDefaultColor();
disabledColor =
boxStrokeColorStateList.getColorForState(new int[] {-android.R.attr.state_enabled}, -1);
hoveredStrokeColor =
boxStrokeColorStateList.getColorForState(new int[] {android.R.attr.state_hovered}, -1);
focusedStrokeColor =
boxStrokeColorStateList.getColorForState(new int[] {android.R.attr.state_focused}, -1);
} else {
// If attribute boxStrokeColor is not a color state list but only a single value, its value
// will be applied to the box's focus state.
focusedStrokeColor =
a.getColor(R.styleable.TextInputLayout_boxStrokeColor, Color.TRANSPARENT);
defaultStrokeColor =
ContextCompat.getColor(context, R.color.mtrl_textinput_default_box_stroke_color);
disabledColor = ContextCompat.getColor(context, R.color.mtrl_textinput_disabled_color);
hoveredStrokeColor =
ContextCompat.getColor(context, R.color.mtrl_textinput_hovered_box_stroke_color);
}
final int hintAppearance = a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, -1);
if (hintAppearance != -1) {
setHintTextAppearance(a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0));
}
final int errorTextAppearance =
a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
// Initialize error icon view.
errorIconView =
(CheckableImageButton)
LayoutInflater.from(getContext())
.inflate(R.layout.design_text_input_end_icon, inputFrame, false);
inputFrame.addView(errorIconView);
errorIconView.setVisibility(GONE);
if (a.hasValue(R.styleable.TextInputLayout_errorIconDrawable)) {
setErrorIconDrawable(a.getDrawable(R.styleable.TextInputLayout_errorIconDrawable));
}
if (a.hasValue(R.styleable.TextInputLayout_errorIconTint)) {
setErrorIconTintList(
MaterialResources.getColorStateList(
context, a, R.styleable.TextInputLayout_errorIconTint));
}
if (a.hasValue(R.styleable.TextInputLayout_errorIconTintMode)) {
setErrorIconTintMode(
ViewUtils.parseTintMode(
a.getInt(R.styleable.TextInputLayout_errorIconTintMode, -1), null));
}
errorIconView.setContentDescription(
getResources().getText(R.string.error_icon_content_description));
ViewCompat
.setImportantForAccessibility(errorIconView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
errorIconView.setClickable(false);
errorIconView.setFocusable(false);
final int helperTextTextAppearance =
a.getResourceId(R.styleable.TextInputLayout_helperTextTextAppearance, 0);
final boolean helperTextEnabled =
a.getBoolean(R.styleable.TextInputLayout_helperTextEnabled, false);
final CharSequence helperText = a.getText(R.styleable.TextInputLayout_helperText);
final boolean counterEnabled = a.getBoolean(R.styleable.TextInputLayout_counterEnabled, false);
setCounterMaxLength(a.getInt(R.styleable.TextInputLayout_counterMaxLength, INVALID_MAX_LENGTH));
counterTextAppearance = a.getResourceId(R.styleable.TextInputLayout_counterTextAppearance, 0);
counterOverflowTextAppearance =
a.getResourceId(R.styleable.TextInputLayout_counterOverflowTextAppearance, 0);
// Initialize start icon view.
startIconView =
(CheckableImageButton)
LayoutInflater.from(getContext())
.inflate(R.layout.design_text_input_start_icon, inputFrame, false);
inputFrame.addView(startIconView);
startIconView.setVisibility(GONE);
setStartIconOnClickListener(null);
// Set up start icon if any.
if (a.hasValue(R.styleable.TextInputLayout_startIconDrawable)) {
setStartIconDrawable(a.getDrawable(R.styleable.TextInputLayout_startIconDrawable));
if (a.hasValue(R.styleable.TextInputLayout_startIconContentDescription)) {
setStartIconContentDescription(
a.getText(R.styleable.TextInputLayout_startIconContentDescription));
}
setStartIconCheckable(a.getBoolean(R.styleable.TextInputLayout_startIconCheckable, true));
}
// Default tint for a start icon or value specified by user.
if (a.hasValue(R.styleable.TextInputLayout_startIconTint)) {
setStartIconTintList(
MaterialResources.getColorStateList(
context, a, R.styleable.TextInputLayout_startIconTint));
}
// Default tint mode for a start icon or value specified by user.
if (a.hasValue(R.styleable.TextInputLayout_startIconTintMode)) {
setStartIconTintMode(
ViewUtils.parseTintMode(
a.getInt(R.styleable.TextInputLayout_startIconTintMode, -1), null));
}
setHelperTextEnabled(helperTextEnabled);
setHelperText(helperText);
setHelperTextTextAppearance(helperTextTextAppearance);
setErrorEnabled(errorEnabled);
setErrorTextAppearance(errorTextAppearance);
setCounterTextAppearance(counterTextAppearance);
setCounterOverflowTextAppearance(counterOverflowTextAppearance);
if (a.hasValue(R.styleable.TextInputLayout_errorTextColor)) {
setErrorTextColor(a.getColorStateList(R.styleable.TextInputLayout_errorTextColor));
}
if (a.hasValue(R.styleable.TextInputLayout_helperTextTextColor)) {
setHelperTextColor(a.getColorStateList(R.styleable.TextInputLayout_helperTextTextColor));
}
if (a.hasValue(R.styleable.TextInputLayout_hintTextColor)) {
setHintTextColor(a.getColorStateList(R.styleable.TextInputLayout_hintTextColor));
}
if (a.hasValue(R.styleable.TextInputLayout_counterTextColor)) {
setCounterTextColor(a.getColorStateList(R.styleable.TextInputLayout_counterTextColor));
}
if (a.hasValue(R.styleable.TextInputLayout_counterOverflowTextColor)) {
setCounterOverflowTextColor(
a.getColorStateList(R.styleable.TextInputLayout_counterOverflowTextColor));
}
setCounterEnabled(counterEnabled);
setBoxBackgroundMode(
a.getInt(R.styleable.TextInputLayout_boxBackgroundMode, BOX_BACKGROUND_NONE));
// Initialize end icon view.
endIconView =
(CheckableImageButton)
LayoutInflater.from(getContext())
.inflate(R.layout.design_text_input_end_icon, inputFrame, false);
inputFrame.addView(endIconView);
endIconView.setVisibility(GONE);
endIconDelegates.append(END_ICON_CUSTOM, new CustomEndIconDelegate(this));
endIconDelegates.append(END_ICON_NONE, new NoEndIconDelegate(this));
endIconDelegates.append(END_ICON_PASSWORD_TOGGLE, new PasswordToggleEndIconDelegate(this));
endIconDelegates.append(END_ICON_CLEAR_TEXT, new ClearTextEndIconDelegate(this));
endIconDelegates.append(END_ICON_DROPDOWN_MENU, new DropdownMenuEndIconDelegate(this));
// Set up the end icon if any.
if (a.hasValue(R.styleable.TextInputLayout_endIconMode)) {
// Specific defaults depending on which end icon mode is set
setEndIconMode(a.getInt(R.styleable.TextInputLayout_endIconMode, END_ICON_NONE));
// Overwrite default values if user specified any different ones
if (a.hasValue(R.styleable.TextInputLayout_endIconDrawable)) {
setEndIconDrawable(a.getDrawable(R.styleable.TextInputLayout_endIconDrawable));
}
if (a.hasValue(R.styleable.TextInputLayout_endIconContentDescription)) {
setEndIconContentDescription(
a.getText(R.styleable.TextInputLayout_endIconContentDescription));
}
setEndIconCheckable(a.getBoolean(R.styleable.TextInputLayout_endIconCheckable, true));
} else if (a.hasValue(R.styleable.TextInputLayout_passwordToggleEnabled)) {
// Support for deprecated attributes related to the password toggle end icon
boolean passwordToggleEnabled =
a.getBoolean(R.styleable.TextInputLayout_passwordToggleEnabled, false);
setEndIconMode(passwordToggleEnabled ? END_ICON_PASSWORD_TOGGLE : END_ICON_NONE);
setEndIconDrawable(a.getDrawable(R.styleable.TextInputLayout_passwordToggleDrawable));
setEndIconContentDescription(
a.getText(R.styleable.TextInputLayout_passwordToggleContentDescription));
if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTint)) {
setEndIconTintList(
MaterialResources.getColorStateList(
context, a, R.styleable.TextInputLayout_passwordToggleTint));
}
if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTintMode)) {
setEndIconTintMode(
ViewUtils.parseTintMode(
a.getInt(R.styleable.TextInputLayout_passwordToggleTintMode, -1), null));
}
}
if (!a.hasValue(R.styleable.TextInputLayout_passwordToggleEnabled)) {
// Default tint for any end icon or value specified by user
if (a.hasValue(R.styleable.TextInputLayout_endIconTint)) {
setEndIconTintList(
MaterialResources.getColorStateList(
context, a, R.styleable.TextInputLayout_endIconTint));
}
// Default tint mode for any end icon or value specified by user
if (a.hasValue(R.styleable.TextInputLayout_endIconTintMode)) {
setEndIconTintMode(
ViewUtils.parseTintMode(
a.getInt(R.styleable.TextInputLayout_endIconTintMode, -1), null));
}
}
a.recycle();
// For accessibility, consider TextInputLayout itself to be a simple container for an EditText,
// and do not expose it to accessibility services.
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
@Override
public void addView(View child, int index, final ViewGroup.LayoutParams params) {
if (child instanceof EditText) {
// Make sure that the EditText is vertically at the bottom, so that it sits on the
// EditText's underline
FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK);
inputFrame.addView(child, flp);
// Now use the EditText's LayoutParams as our own and update them to make enough space
// for the label
inputFrame.setLayoutParams(params);
updateInputLayoutMargins();
setEditText((EditText) child);
} else {
// Carry on adding the View...
super.addView(child, index, params);
}
}
@NonNull
MaterialShapeDrawable getBoxBackground() {
if (boxBackgroundMode == BOX_BACKGROUND_FILLED || boxBackgroundMode == BOX_BACKGROUND_OUTLINE) {
return boxBackground;
}
throw new IllegalStateException();
}
/**
* Set the box background mode (filled, outline, or none).
*
* <p>May be one of {@link #BOX_BACKGROUND_NONE}, {@link #BOX_BACKGROUND_FILLED}, or {@link
* #BOX_BACKGROUND_OUTLINE}.
*
* <p>Note: This method defines TextInputLayout's internal behavior (for example, it allows the
* hint to be displayed inline with the stroke in a cutout), but doesn't set all attributes that
* are set in the styles provided for the box background modes. To achieve the look of an outlined
* or filled text field, supplement this method with other methods that modify the box, such as
* {@link #setBoxStrokeColor(int)} and {@link #setBoxBackgroundColor(int)}.
*
* @param boxBackgroundMode box's background mode
* @throws IllegalArgumentException if boxBackgroundMode is not a @BoxBackgroundMode constant
*/
public void setBoxBackgroundMode(@BoxBackgroundMode int boxBackgroundMode) {
if (boxBackgroundMode == this.boxBackgroundMode) {
return;
}
this.boxBackgroundMode = boxBackgroundMode;
if (editText != null) {
onApplyBoxBackgroundMode();
}
}
/**
* Get the box background mode (filled, outline, or none).
*
* <p>May be one of {@link #BOX_BACKGROUND_NONE}, {@link #BOX_BACKGROUND_FILLED}, or {@link
* #BOX_BACKGROUND_OUTLINE}.
*/
@BoxBackgroundMode
public int getBoxBackgroundMode() {
return boxBackgroundMode;
}
private void onApplyBoxBackgroundMode() {
assignBoxBackgroundByMode();
setEditTextBoxBackground();
updateTextInputBoxState();
if (boxBackgroundMode != BOX_BACKGROUND_NONE) {
updateInputLayoutMargins();
}
}
private void assignBoxBackgroundByMode() {
switch (boxBackgroundMode) {
case BOX_BACKGROUND_FILLED:
boxBackground = new MaterialShapeDrawable(shapeAppearanceModel);
boxUnderline = new MaterialShapeDrawable();
break;
case BOX_BACKGROUND_OUTLINE:
if (hintEnabled && !(boxBackground instanceof CutoutDrawable)) {
boxBackground = new CutoutDrawable(shapeAppearanceModel);
} else {
boxBackground = new MaterialShapeDrawable(shapeAppearanceModel);
}
boxUnderline = null;
break;
case BOX_BACKGROUND_NONE:
boxBackground = null;
boxUnderline = null;
break;
default:
throw new IllegalArgumentException(
boxBackgroundMode + " is illegal; only @BoxBackgroundMode constants are supported.");
}
}
private void setEditTextBoxBackground() {
// Set the EditText background to boxBackground if we should use that as the box background.
if (shouldUseEditTextBackgroundForBoxBackground()) {
ViewCompat.setBackground(editText, boxBackground);
}
}
private boolean shouldUseEditTextBackgroundForBoxBackground() {
// When the text field's EditText's background is null, use the EditText's background for the
// box background.
return editText != null
&& boxBackground != null
&& editText.getBackground() == null
&& boxBackgroundMode != BOX_BACKGROUND_NONE;
}
/**
* Set the outline box's stroke color.
*
* <p>Calling this method when not in outline box mode will do nothing.
*
* @param boxStrokeColor the color to use for the box's stroke
* @see #getBoxStrokeColor()
*/
public void setBoxStrokeColor(@ColorInt int boxStrokeColor) {
if (focusedStrokeColor != boxStrokeColor) {
focusedStrokeColor = boxStrokeColor;
updateTextInputBoxState();
}
}
/**
* Returns the box's stroke color.
*
* @return the color used for the box's stroke
* @see #setBoxStrokeColor(int)
*/
public int getBoxStrokeColor() {
return focusedStrokeColor;
}
/**
* Set the resource used for the filled box's background color.
*
* <p>Note: The background color is only supported for filled boxes. When used with box variants
* other than {@link BoxBackgroundMode#BOX_BACKGROUND_FILLED}, the box background color may not
* work as intended.
*
* @param boxBackgroundColorId the resource to use for the box's background color
*/
public void setBoxBackgroundColorResource(@ColorRes int boxBackgroundColorId) {
setBoxBackgroundColor(ContextCompat.getColor(getContext(), boxBackgroundColorId));
}
/**
* Set the filled box's background color.
*
* <p>Note: The background color is only supported for filled boxes. When used with box variants
* other than {@link BoxBackgroundMode#BOX_BACKGROUND_FILLED}, the box background color may not
* work as intended.
*
* @param boxBackgroundColor the color to use for the filled box's background
* @see #getBoxBackgroundColor()
*/
public void setBoxBackgroundColor(@ColorInt int boxBackgroundColor) {
if (this.boxBackgroundColor != boxBackgroundColor) {
this.boxBackgroundColor = boxBackgroundColor;
defaultFilledBackgroundColor = boxBackgroundColor;
applyBoxAttributes();
}
}
/**
* Returns the filled box's background color.
*
* @return the color used for the filled box's background
* @see #setBoxBackgroundColor(int)
*/
public int getBoxBackgroundColor() {
return boxBackgroundColor;
}
/**
* Set the resources used for the box's corner radii.
*
* @param boxCornerRadiusTopStartId the resource to use for the box's top start corner radius
* @param boxCornerRadiusTopEndId the resource to use for the box's top end corner radius
* @param boxCornerRadiusBottomEndId the resource to use for the box's bottom end corner radius
* @param boxCornerRadiusBottomStartId the resource to use for the box's bottom start corner
* radius
*/
public void setBoxCornerRadiiResources(
@DimenRes int boxCornerRadiusTopStartId,
@DimenRes int boxCornerRadiusTopEndId,
@DimenRes int boxCornerRadiusBottomEndId,
@DimenRes int boxCornerRadiusBottomStartId) {
setBoxCornerRadii(
getContext().getResources().getDimension(boxCornerRadiusTopStartId),
getContext().getResources().getDimension(boxCornerRadiusTopEndId),
getContext().getResources().getDimension(boxCornerRadiusBottomStartId),
getContext().getResources().getDimension(boxCornerRadiusBottomEndId));
}
/**
* Set the box's corner radii.
*
* @param boxCornerRadiusTopStart the value to use for the box's top start corner radius
* @param boxCornerRadiusTopEnd the value to use for the box's top end corner radius
* @param boxCornerRadiusBottomEnd the value to use for the box's bottom end corner radius
* @param boxCornerRadiusBottomStart the value to use for the box's bottom start corner radius
* @see #getBoxCornerRadiusTopStart()
* @see #getBoxCornerRadiusTopEnd()
* @see #getBoxCornerRadiusBottomEnd()
* @see #getBoxCornerRadiusBottomStart()
*/
public void setBoxCornerRadii(
float boxCornerRadiusTopStart,
float boxCornerRadiusTopEnd,
float boxCornerRadiusBottomStart,
float boxCornerRadiusBottomEnd) {
if (shapeAppearanceModel.getTopLeftCorner().getCornerSize() != boxCornerRadiusTopStart
|| shapeAppearanceModel.getTopRightCorner().getCornerSize() != boxCornerRadiusTopEnd
|| shapeAppearanceModel.getBottomRightCorner().getCornerSize() != boxCornerRadiusBottomEnd
|| shapeAppearanceModel.getBottomLeftCorner().getCornerSize()
!= boxCornerRadiusBottomStart) {
shapeAppearanceModel.getTopLeftCorner().setCornerSize(boxCornerRadiusTopStart);
shapeAppearanceModel.getTopRightCorner().setCornerSize(boxCornerRadiusTopEnd);
shapeAppearanceModel.getBottomRightCorner().setCornerSize(boxCornerRadiusBottomEnd);
shapeAppearanceModel.getBottomLeftCorner().setCornerSize(boxCornerRadiusBottomStart);
applyBoxAttributes();
}
}
/**
* Returns the box's top start corner radius.
*
* @return the value used for the box's top start corner radius
* @see #setBoxCornerRadii(float, float, float, float)
*/
public float getBoxCornerRadiusTopStart() {
return shapeAppearanceModel.getTopLeftCorner().getCornerSize();
}
/**
* Returns the box's top end corner radius.
*
* @return the value used for the box's top end corner radius
* @see #setBoxCornerRadii(float, float, float, float)
*/
public float getBoxCornerRadiusTopEnd() {
return shapeAppearanceModel.getTopRightCorner().getCornerSize();
}
/**
* Returns the box's bottom end corner radius.
*
* @return the value used for the box's bottom end corner radius
* @see #setBoxCornerRadii(float, float, float, float)
*/
public float getBoxCornerRadiusBottomEnd() {
return shapeAppearanceModel.getBottomLeftCorner().getCornerSize();
}
/**
* Returns the box's bottom start corner radius.
*
* @return the value used for the box's bottom start corner radius
* @see #setBoxCornerRadii(float, float, float, float)
*/
public float getBoxCornerRadiusBottomStart() {
return shapeAppearanceModel.getBottomRightCorner().getCornerSize();
}
/**
* Set the typeface to use for the hint and any label views (such as counter and error views).
*
* @param typeface typeface to use, or {@code null} to use the default.
*/
@SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
public void setTypeface(@Nullable Typeface typeface) {
if (typeface != this.typeface) {
this.typeface = typeface;
collapsingTextHelper.setTypefaces(typeface);
indicatorViewController.setTypefaces(typeface);
if (counterView != null) {
counterView.setTypeface(typeface);
}
}
}
/**
* Returns the typeface used for the hint and any label views (such as counter and error views).
*/
@Nullable
public Typeface getTypeface() {
return typeface;
}
@Override
public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
if (originalHint == null || editText == null) {
super.dispatchProvideAutofillStructure(structure, flags);
return;
}
// Temporarily sets child's hint to its original value so it is properly set in the
// child's ViewStructure.
boolean wasProvidingHint = isProvidingHint;
// Ensures a child TextInputEditText does not retrieve its hint from this TextInputLayout.
isProvidingHint = false;
final CharSequence hint = editText.getHint();
editText.setHint(originalHint);
try {
super.dispatchProvideAutofillStructure(structure, flags);
} finally {
editText.setHint(hint);
isProvidingHint = wasProvidingHint;
}
}
private void setEditText(EditText editText) {
// If we already have an EditText, throw an exception
if (this.editText != null) {
throw new IllegalArgumentException("We already have an EditText, can only have one");
}
if (endIconMode != END_ICON_DROPDOWN_MENU && !(editText instanceof TextInputEditText)) {
Log.i(
LOG_TAG,
"EditText added is not a TextInputEditText. Please switch to using that"
+ " class instead.");
}
this.editText = editText;
onApplyBoxBackgroundMode();
setTextInputAccessibilityDelegate(new AccessibilityDelegate(this));
// Use the EditText's typeface, and its text size for our expanded text.
collapsingTextHelper.setTypefaces(this.editText.getTypeface());
collapsingTextHelper.setExpandedTextSize(this.editText.getTextSize());
final int editTextGravity = this.editText.getGravity();
collapsingTextHelper.setCollapsedTextGravity(
Gravity.TOP | (editTextGravity & ~Gravity.VERTICAL_GRAVITY_MASK));
collapsingTextHelper.setExpandedTextGravity(editTextGravity);
// Add a TextWatcher so that we know when the text input has changed.
this.editText.addTextChangedListener(
new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
updateLabelState(!restoringSavedState);
if (counterEnabled) {
updateCounter(s.length());
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
});
// Use the EditText's hint colors if we don't have one set
if (defaultHintTextColor == null) {
defaultHintTextColor = this.editText.getHintTextColors();
}
// If we do not have a valid hint, try and retrieve it from the EditText, if enabled
if (hintEnabled) {
if (TextUtils.isEmpty(hint)) {
// Save the hint so it can be restored on dispatchProvideAutofillStructure();
originalHint = this.editText.getHint();
setHint(originalHint);
// Clear the EditText's hint as we will display it ourselves
this.editText.setHint(null);
}
this.isProvidingHint = true;
}
if (counterView != null) {
updateCounter(this.editText.getText().length());
}
updateEditTextBackground();
indicatorViewController.adjustIndicatorPadding();
startIconView.bringToFront();
endIconView.bringToFront();
errorIconView.bringToFront();
dispatchOnEditTextAttached();
// Update the label visibility with no animation, but force a state change
updateLabelState(false, true);
}
private void updateInputLayoutMargins() {
// Create/update the LayoutParams so that we can add enough top margin
// to the EditText to make room for the label.
if (boxBackgroundMode != BOX_BACKGROUND_FILLED) {
final LayoutParams lp = (LayoutParams) inputFrame.getLayoutParams();
final int newTopMargin = calculateLabelMarginTop();
if (newTopMargin != lp.topMargin) {
lp.topMargin = newTopMargin;
inputFrame.requestLayout();
}
}
}
@Override
public int getBaseline() {
if (editText != null) {
return editText.getBaseline() + getPaddingTop() + calculateLabelMarginTop();
} else {
return super.getBaseline();
}
}
void updateLabelState(boolean animate) {
updateLabelState(animate, false);
}
private void updateLabelState(boolean animate, boolean force) {
final boolean isEnabled = isEnabled();
final boolean hasText = editText != null && !TextUtils.isEmpty(editText.getText());
final boolean hasFocus = editText != null && editText.hasFocus();
final boolean errorShouldBeShown = indicatorViewController.errorShouldBeShown();
// Set the expanded and collapsed labels to the default text color.
if (defaultHintTextColor != null) {
collapsingTextHelper.setCollapsedTextColor(defaultHintTextColor);
collapsingTextHelper.setExpandedTextColor(defaultHintTextColor);
}
// Set the collapsed and expanded label text colors based on the current state.
if (!isEnabled) {
collapsingTextHelper.setCollapsedTextColor(ColorStateList.valueOf(disabledColor));
collapsingTextHelper.setExpandedTextColor(ColorStateList.valueOf(disabledColor));
} else if (errorShouldBeShown) {
collapsingTextHelper.setCollapsedTextColor(indicatorViewController.getErrorViewTextColors());
} else if (counterOverflowed && counterView != null) {
collapsingTextHelper.setCollapsedTextColor(counterView.getTextColors());
} else if (hasFocus && focusedTextColor != null) {
collapsingTextHelper.setCollapsedTextColor(focusedTextColor);
} // If none of these states apply, leave the expanded and collapsed colors as they are.
if (hasText || (isEnabled() && (hasFocus || errorShouldBeShown))) {
// We should be showing the label so do so if it isn't already
if (force || hintExpanded) {
collapseHint(animate);
}
} else {
// We should not be showing the label so hide it
if (force || !hintExpanded) {
expandHint(animate);
}
}
}
/** Returns the {@link android.widget.EditText} used for text input. */
@Nullable
public EditText getEditText() {
return editText;
}
/**
* Set the hint to be displayed in the floating label, if enabled.
*
* @see #setHintEnabled(boolean)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_android_hint
*/
public void setHint(@Nullable CharSequence hint) {
if (hintEnabled) {
setHintInternal(hint);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
}
private void setHintInternal(CharSequence hint) {
if (!TextUtils.equals(hint, this.hint)) {
this.hint = hint;
collapsingTextHelper.setText(hint);
// Reset the cutout to make room for a larger hint.
if (!hintExpanded) {
openCutout();
}
}
}
/**
* Returns the hint which is displayed in the floating label, if enabled.
*
* @return the hint, or null if there isn't one set, or the hint is not enabled.
* @attr ref com.google.android.material.R.styleable#TextInputLayout_android_hint
*/
@Nullable
public CharSequence getHint() {
return hintEnabled ? hint : null;
}
/**
* Sets whether the floating label functionality is enabled or not in this layout.
*
* <p>If enabled, any non-empty hint in the child EditText will be moved into the floating hint,
* and its existing hint will be cleared. If disabled, then any non-empty floating hint in this
* layout will be moved into the EditText, and this layout's hint will be cleared.
*
* @see #setHint(CharSequence)
* @see #isHintEnabled()
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintEnabled
*/
public void setHintEnabled(boolean enabled) {
if (enabled != hintEnabled) {
hintEnabled = enabled;
if (!hintEnabled) {
// Ensures a child TextInputEditText provides its internal hint, not this TextInputLayout's.
isProvidingHint = false;
if (!TextUtils.isEmpty(hint) && TextUtils.isEmpty(editText.getHint())) {
// If the child EditText has no hint, but this layout does, restore it on the child.
editText.setHint(hint);
}
// Now clear out any set hint
setHintInternal(null);
} else {
final CharSequence editTextHint = editText.getHint();
if (!TextUtils.isEmpty(editTextHint)) {
// If the hint is now enabled and the EditText has one set, we'll use it if
// we don't already have one, and clear the EditText's
if (TextUtils.isEmpty(hint)) {
setHint(editTextHint);
}
editText.setHint(null);
}
isProvidingHint = true;
}
// Now update the EditText top margin
if (editText != null) {
updateInputLayoutMargins();
}
}
}
/**
* Returns whether the floating label functionality is enabled or not in this layout.
*
* @see #setHintEnabled(boolean)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintEnabled
*/
public boolean isHintEnabled() {
return hintEnabled;
}
/**
* Returns whether or not this layout is actively managing a child {@link EditText}'s hint. If the
* child is an instance of {@link TextInputEditText}, this value defines the behavior of {@link
* TextInputEditText#getHint()}.
*/
boolean isProvidingHint() {
return isProvidingHint;
}
/**
* Sets the collapsed hint text color, size, style from the specified TextAppearance resource.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintTextAppearance
*/
public void setHintTextAppearance(@StyleRes int resId) {
collapsingTextHelper.setCollapsedTextAppearance(resId);
focusedTextColor = collapsingTextHelper.getCollapsedTextColor();
if (editText != null) {
updateLabelState(false);
// Text size might have changed so update the top margin
updateInputLayoutMargins();
}
}
/**
* Sets the collapsed hint text color from the specified ColorStateList resource.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintTextColor
*/
public void setHintTextColor(@Nullable ColorStateList hintTextColor) {
if (focusedTextColor != hintTextColor) {
if (defaultHintTextColor == null) {
collapsingTextHelper.setCollapsedTextColor(hintTextColor);
}
focusedTextColor = hintTextColor;
if (editText != null) {
updateLabelState(false);
}
}
}
/**
* Gets the collapsed hint text color.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintTextColor
*/
@Nullable
public ColorStateList getHintTextColor() {
return focusedTextColor;
}
/** Sets the text color used by the hint in both the collapsed and expanded states. */
public void setDefaultHintTextColor(@Nullable ColorStateList textColor) {
defaultHintTextColor = textColor;
focusedTextColor = textColor;
if (editText != null) {
updateLabelState(false);
}
}
/**
* Returns the text color used by the hint in both the collapsed and expanded states, or null if
* no color has been set.
*/
@Nullable
public ColorStateList getDefaultHintTextColor() {
return defaultHintTextColor;
}
/**
* Whether the error functionality is enabled or not in this layout. Enabling this functionality
* before setting an error message via {@link #setError(CharSequence)}, will mean that this layout
* will not change size when an error is displayed.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorEnabled
*/
public void setErrorEnabled(boolean enabled) {
indicatorViewController.setErrorEnabled(enabled);
}
/**
* Sets the text color and size for the error message from the specified TextAppearance resource.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorTextAppearance
*/
public void setErrorTextAppearance(@StyleRes int errorTextAppearance) {
indicatorViewController.setErrorTextAppearance(errorTextAppearance);
}
/** Sets the text color used by the error message in all states. */
public void setErrorTextColor(@Nullable ColorStateList errorTextColor) {
indicatorViewController.setErrorViewTextColor(errorTextColor);
}
/** Returns the text color used by the error message in current state. */
@ColorInt
public int getErrorCurrentTextColors() {
return indicatorViewController.getErrorViewCurrentTextColor();
}
/**
* Sets the text color and size for the helper text from the specified TextAppearance resource.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_helperTextTextAppearance
*/
public void setHelperTextTextAppearance(@StyleRes int helperTextTextAppearance) {
indicatorViewController.setHelperTextAppearance(helperTextTextAppearance);
}
/** Sets the text color used by the helper text in all states. */
public void setHelperTextColor(@Nullable ColorStateList helperTextColor) {
indicatorViewController.setHelperTextViewTextColor(helperTextColor);
}
/**
* Returns whether the error functionality is enabled or not in this layout.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorEnabled
* @see #setErrorEnabled(boolean)
*/
public boolean isErrorEnabled() {
return indicatorViewController.isErrorEnabled();
}
/**
* Whether the helper text functionality is enabled or not in this layout. Enabling this
* functionality before setting a helper message via {@link #setHelperText(CharSequence)} will
* mean that this layout will not change size when a helper message is displayed.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_helperTextEnabled
*/
public void setHelperTextEnabled(boolean enabled) {
indicatorViewController.setHelperTextEnabled(enabled);
}
/**
* Sets a helper message that will be displayed below the {@link EditText}. If the {@code helper}
* is {@code null}, the helper text functionality will be disabled and the helper message will be
* hidden.
*
* <p>If the helper text functionality has not been enabled via {@link
* #setHelperTextEnabled(boolean)}, then it will be automatically enabled if {@code helper} is not
* empty.
*
* @param helperText Helper text to display
* @see #getHelperText()
*/
public void setHelperText(@Nullable final CharSequence helperText) {
// If helper text is null, disable helper if it's enabled.
if (TextUtils.isEmpty(helperText)) {
if (isHelperTextEnabled()) {
setHelperTextEnabled(false);
}
} else {
if (!isHelperTextEnabled()) {
setHelperTextEnabled(true);
}
indicatorViewController.showHelper(helperText);
}
}
/**
* Returns whether the helper text functionality is enabled or not in this layout.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_helperTextEnabled
* @see #setHelperTextEnabled(boolean)
*/
public boolean isHelperTextEnabled() {
return indicatorViewController.isHelperTextEnabled();
}
/** Returns the text color used by the helper text in the current states. */
@ColorInt
public int getHelperTextCurrentTextColor() {
return indicatorViewController.getHelperTextViewCurrentTextColor();
}
/**
* Sets an error message that will be displayed below our {@link EditText}. If the {@code error}
* is {@code null}, the error message will be cleared.
*
* <p>If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
* it will be automatically enabled if {@code error} is not empty.
*
* @param errorText Error message to display, or null to clear
* @see #getError()
*/
public void setError(@Nullable final CharSequence errorText) {
if (!indicatorViewController.isErrorEnabled()) {
if (TextUtils.isEmpty(errorText)) {
// If error isn't enabled, and the error is empty, just return
return;
}
// Else, we'll assume that they want to enable the error functionality
setErrorEnabled(true);
}
if (!TextUtils.isEmpty(errorText)) {
indicatorViewController.showError(errorText);
} else {
indicatorViewController.hideError();
}
}
/**
* Set the drawable to use for the error icon.
*
* @param resId resource id of the drawable to set, or 0 to clear the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconDrawable
*/
public void setErrorIconDrawable(@DrawableRes int resId) {
setErrorIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
}
/**
* Set the drawable to use for the error icon.
*
* @param errorIconDrawable Drawable to set, may be null to clear the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconDrawable
*/
public void setErrorIconDrawable(@Nullable Drawable errorIconDrawable) {
errorIconView.setImageDrawable(errorIconDrawable);
setErrorIconVisible(errorIconDrawable != null);
}
/**
* Returns the drawable currently used for the error icon.
*
* @see #setErrorIconDrawable(Drawable)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconDrawable
*/
@Nullable
public Drawable getErrorIconDrawable() {
return errorIconView.getDrawable();
}
/**
* Applies a tint to the error icon drawable.
*
* @param errorIconTintList the tint to apply, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconTint
*/
public void setErrorIconTintList(@Nullable ColorStateList errorIconTintList) {
Drawable icon = errorIconView.getDrawable();
if (icon != null) {
icon = DrawableCompat.wrap(icon).mutate();
DrawableCompat.setTintList(icon, errorIconTintList);
}
if (errorIconView.getDrawable() != icon) {
errorIconView.setImageDrawable(icon);
}
}
/**
* Specifies the blending mode used to apply tint to the end icon drawable. The default mode is
* {@link PorterDuff.Mode#SRC_IN}.
*
* @param errorIconTintMode the blending mode used to apply the tint, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconTintMode
*/
public void setErrorIconTintMode(@Nullable PorterDuff.Mode errorIconTintMode) {
Drawable icon = errorIconView.getDrawable();
if (icon != null) {
icon = DrawableCompat.wrap(icon).mutate();
DrawableCompat.setTintMode(icon, errorIconTintMode);
}
if (errorIconView.getDrawable() != icon) {
errorIconView.setImageDrawable(icon);
}
}
/**
* Whether the character counter functionality is enabled or not in this layout.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterEnabled
*/
public void setCounterEnabled(boolean enabled) {
if (counterEnabled != enabled) {
if (enabled) {
counterView = new AppCompatTextView(getContext());
counterView.setId(R.id.textinput_counter);
if (typeface != null) {
counterView.setTypeface(typeface);
}
counterView.setMaxLines(1);
indicatorViewController.addIndicator(counterView, COUNTER_INDEX);
updateCounterTextAppearanceAndColor();
updateCounter();
} else {
indicatorViewController.removeIndicator(counterView, COUNTER_INDEX);
counterView = null;
}
counterEnabled = enabled;
}
}
/**
* Sets the text color and size for the character counter using the specified TextAppearance
* resource.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterTextAppearance
* @see #setCounterTextColor(ColorStateList)
*/
public void setCounterTextAppearance(int counterTextAppearance) {
if (this.counterTextAppearance != counterTextAppearance) {
this.counterTextAppearance = counterTextAppearance;
updateCounterTextAppearanceAndColor();
}
}
/**
* Sets the text color for the character counter using a ColorStateList.
*
* <p>This text color takes precedence over a text color set in counterTextAppearance.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterTextColor
* @param counterTextColor text color used for the character counter
*/
public void setCounterTextColor(@Nullable ColorStateList counterTextColor) {
if (this.counterTextColor != counterTextColor) {
this.counterTextColor = counterTextColor;
updateCounterTextAppearanceAndColor();
}
}
/**
* Returns the text color used for the character counter, or null if one has not been set.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterOverflowTextColor
* @see #setCounterTextAppearance(int)
* @return the text color used for the character counter
*/
@Nullable
public ColorStateList getCounterTextColor() {
return counterTextColor;
}
/**
* Sets the text color and size for the overflowed character counter using the specified
* TextAppearance resource.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterOverflowTextAppearance
* @see #setCounterOverflowTextColor(ColorStateList)
*/
public void setCounterOverflowTextAppearance(int counterOverflowTextAppearance) {
if (this.counterOverflowTextAppearance != counterOverflowTextAppearance) {
this.counterOverflowTextAppearance = counterOverflowTextAppearance;
updateCounterTextAppearanceAndColor();
}
}
/**
* Sets the text color for the overflowed character counter using a ColorStateList.
*
* <p>This text color takes precedence over a text color set in counterOverflowTextAppearance.
*
* @see #setCounterOverflowTextAppearance(int)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterOverflowTextColor
* @param counterOverflowTextColor the text color used for the overflowed character counter
*/
public void setCounterOverflowTextColor(@Nullable ColorStateList counterOverflowTextColor) {
if (this.counterOverflowTextColor != counterOverflowTextColor) {
this.counterOverflowTextColor = counterOverflowTextColor;
updateCounterTextAppearanceAndColor();
}
}
/**
* Returns the text color used for the overflowed character counter, or null if one has not been
* set.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterOverflowTextColor
* @see #setCounterOverflowTextAppearance(int)
* @return the text color used for the overflowed character counter
*/
@Nullable
public ColorStateList getCounterOverflowTextColor() {
return counterTextColor;
}
/**
* Returns whether the character counter functionality is enabled or not in this layout.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterEnabled
* @see #setCounterEnabled(boolean)
*/
public boolean isCounterEnabled() {
return counterEnabled;
}
/**
* Sets the max length to display at the character counter.
*
* @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown.
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterMaxLength
*/
public void setCounterMaxLength(int maxLength) {
if (counterMaxLength != maxLength) {
if (maxLength > 0) {
counterMaxLength = maxLength;
} else {
counterMaxLength = INVALID_MAX_LENGTH;
}
if (counterEnabled) {
updateCounter();
}
}
}
private void updateCounter() {
if (counterView != null) {
updateCounter(editText == null ? 0 : editText.getText().length());
}
}
void updateCounter(int length) {
boolean wasCounterOverflowed = counterOverflowed;
if (counterMaxLength == INVALID_MAX_LENGTH) {
counterView.setText(String.valueOf(length));
counterView.setContentDescription(null);
counterOverflowed = false;
} else {
// Make sure the counter view region is not live to prevent spamming the user with the counter
// overflow message on every key press.
if (ViewCompat.getAccessibilityLiveRegion(counterView)
== ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE) {
ViewCompat.setAccessibilityLiveRegion(
counterView, ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE);
}
counterOverflowed = length > counterMaxLength;
updateCounterContentDescription(
getContext(), counterView, length, counterMaxLength, counterOverflowed);
if (wasCounterOverflowed != counterOverflowed) {
updateCounterTextAppearanceAndColor();
// Announce when the character limit is exceeded.
if (counterOverflowed) {
ViewCompat.setAccessibilityLiveRegion(
counterView, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
}
}
counterView.setText(
getContext().getString(R.string.character_counter_pattern, length, counterMaxLength));
}
if (editText != null && wasCounterOverflowed != counterOverflowed) {
updateLabelState(false);
updateTextInputBoxState();
updateEditTextBackground();
}
}
private static void updateCounterContentDescription(
Context context,
TextView counterView,
int length,
int counterMaxLength,
boolean counterOverflowed) {
counterView.setContentDescription(
context.getString(
counterOverflowed
? R.string.character_counter_overflowed_content_description
: R.string.character_counter_content_description,
length,
counterMaxLength));
}
@Override
public void setEnabled(boolean enabled) {
// Since we're set to addStatesFromChildren, we need to make sure that we set all
// children to enabled/disabled otherwise any enabled children will wipe out our disabled
// drawable state
recursiveSetEnabled(this, enabled);
super.setEnabled(enabled);
}
private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) {
for (int i = 0, count = vg.getChildCount(); i < count; i++) {
final View child = vg.getChildAt(i);
child.setEnabled(enabled);
if (child instanceof ViewGroup) {
recursiveSetEnabled((ViewGroup) child, enabled);
}
}
}
/**
* Returns the max length shown at the character counter.
*
* @attr ref com.google.android.material.R.styleable#TextInputLayout_counterMaxLength
*/
public int getCounterMaxLength() {
return counterMaxLength;
}
/**
* Returns the {@code contentDescription} for accessibility purposes of the counter view, or
* {@code null} if the counter is not enabled, not overflowed, or has no description.
*/
@Nullable
CharSequence getCounterOverflowDescription() {
if (counterEnabled && counterOverflowed && (counterView != null)) {
return counterView.getContentDescription();
}
return null;
}
private void updateCounterTextAppearanceAndColor() {
if (counterView != null) {
setTextAppearanceCompatWithErrorFallback(
counterView, counterOverflowed ? counterOverflowTextAppearance : counterTextAppearance);
if (!counterOverflowed && counterTextColor != null) {
counterView.setTextColor(counterTextColor);
}
if (counterOverflowed && counterOverflowTextColor != null) {
counterView.setTextColor(counterOverflowTextColor);
}
}
}
void setTextAppearanceCompatWithErrorFallback(TextView textView, @StyleRes int textAppearance) {
boolean useDefaultColor = false;
try {
TextViewCompat.setTextAppearance(textView, textAppearance);
if (VERSION.SDK_INT >= VERSION_CODES.M
&& textView.getTextColors().getDefaultColor() == Color.MAGENTA) {
// Caused by our theme not extending from Theme.Design*. On API 23 and
// above, unresolved theme attrs result in MAGENTA rather than an exception.
// Flag so that we use a decent default
useDefaultColor = true;
}
} catch (Exception e) {
// Caused by our theme not extending from Theme.Design*. Flag so that we use
// a decent default
useDefaultColor = true;
}
if (useDefaultColor) {
// Probably caused by our theme not extending from Theme.Design*. Instead
// we manually set something appropriate
TextViewCompat.setTextAppearance(textView, R.style.TextAppearance_AppCompat_Caption);
textView.setTextColor(ContextCompat.getColor(getContext(), R.color.design_error));
}
}
private int calculateLabelMarginTop() {
if (!hintEnabled) {
return 0;
}
switch (boxBackgroundMode) {
case BOX_BACKGROUND_OUTLINE:
return (int) (collapsingTextHelper.getCollapsedTextHeight() / 2);
case BOX_BACKGROUND_FILLED:
case BOX_BACKGROUND_NONE:
return (int) collapsingTextHelper.getCollapsedTextHeight();
default:
return 0;
}
}
private Rect calculateCollapsedTextBounds(Rect rect) {
if (editText == null) {
throw new IllegalStateException();
}
Rect bounds = tmpBoundsRect;
bounds.bottom = rect.bottom;
switch (boxBackgroundMode) {
case BOX_BACKGROUND_OUTLINE:
bounds.left = rect.left + editText.getPaddingLeft();
bounds.top = rect.top - calculateLabelMarginTop();
bounds.right = rect.right - editText.getPaddingRight();
return bounds;
case BOX_BACKGROUND_FILLED:
bounds.left = rect.left + editText.getCompoundPaddingLeft();
bounds.top = rect.top + boxCollapsedPaddingTopPx;
bounds.right = rect.right - editText.getCompoundPaddingRight();
return bounds;
default:
bounds.left = rect.left + editText.getCompoundPaddingLeft();
bounds.top = getPaddingTop();
bounds.right = rect.right - editText.getCompoundPaddingRight();
return bounds;
}
}
private Rect calculateExpandedTextBounds(Rect rect) {
if (editText == null) {
throw new IllegalStateException();
}
Rect bounds = tmpBoundsRect;
float labelHeight = collapsingTextHelper.getExpandedTextHeight();
bounds.left = rect.left + editText.getCompoundPaddingLeft();
bounds.top = calculateExpandedLabelTop(rect, labelHeight);
bounds.right = rect.right - editText.getCompoundPaddingRight();
bounds.bottom = calculateExpandedLabelBottom(rect, bounds, labelHeight);
return bounds;
}
private int calculateExpandedLabelTop(Rect rect, float labelHeight) {
if (isSingleLineFilledTextField()) {
return (int) (rect.centerY() - labelHeight / 2);
}
return rect.top + editText.getCompoundPaddingTop();
}
private int calculateExpandedLabelBottom(Rect rect, Rect bounds, float labelHeight) {
if (boxBackgroundMode == BOX_BACKGROUND_FILLED) {
// Add the label's height to the top of the bounds rather than calculating from the vertical
// center for both the top and bottom of the label. This prevents a potential fractional loss
// of label height caused by the float to int conversion.
return (int) (bounds.top + labelHeight);
}
return rect.bottom - editText.getCompoundPaddingBottom();
}
private boolean isSingleLineFilledTextField() {
return boxBackgroundMode == BOX_BACKGROUND_FILLED
&& (VERSION.SDK_INT < 16 || editText.getMinLines() <= 1);
}
/*
* Calculates the box background color that should be set.
*
* The filled text field has a surface layer with value {@code ?attr/colorSurface} underneath its
* background that is taken into account when calculating the background color.
*/
private int calculateBoxBackgroundColor() {
int backgroundColor = boxBackgroundColor;
if (boxBackgroundMode == BOX_BACKGROUND_FILLED) {
int surfaceLayerColor = MaterialColors.getColor(this, R.attr.colorSurface, Color.TRANSPARENT);
backgroundColor = MaterialColors.layer(surfaceLayerColor, boxBackgroundColor);
}
return backgroundColor;
}
private void applyBoxAttributes() {
if (boxBackground == null) {
return;
}
if (canDrawOutlineStroke()) {
boxBackground.setStroke(boxStrokeWidthPx, boxStrokeColor);
}
boxBackgroundColor = calculateBoxBackgroundColor();
boxBackground.setFillColor(ColorStateList.valueOf(boxBackgroundColor));
if (endIconMode == END_ICON_DROPDOWN_MENU) {
// Makes sure the exposed dropdown menu gets updated properly.
editText.getBackground().invalidateSelf();
}
applyBoxUnderlineAttributes();
invalidate();
}
private void applyBoxUnderlineAttributes() {
// Exit if the underline is not being drawn by TextInputLayout.
if (boxUnderline == null) {
return;
}
if (canDrawStroke()) {
boxUnderline.setFillColor(ColorStateList.valueOf(boxStrokeColor));
}
invalidate();
}
private boolean canDrawOutlineStroke() {
return boxBackgroundMode == BOX_BACKGROUND_OUTLINE && canDrawStroke();
}
private boolean canDrawStroke() {
return boxStrokeWidthPx > -1 && boxStrokeColor != Color.TRANSPARENT;
}
void updateEditTextBackground() {
// Only update the color filter for the legacy text field, since we can directly change the
// Paint colors of the MaterialShapeDrawable box background without having to use color filters.
if (editText == null || boxBackgroundMode != BOX_BACKGROUND_NONE) {
return;
}
Drawable editTextBackground = editText.getBackground();
if (editTextBackground == null) {
return;
}
if (androidx.appcompat.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
editTextBackground = editTextBackground.mutate();
}
if (indicatorViewController.errorShouldBeShown()) {
// Set a color filter for the error color
editTextBackground.setColorFilter(
AppCompatDrawableManager.getPorterDuffColorFilter(
indicatorViewController.getErrorViewCurrentTextColor(), PorterDuff.Mode.SRC_IN));
} else if (counterOverflowed && counterView != null) {
// Set a color filter of the counter color
editTextBackground.setColorFilter(
AppCompatDrawableManager.getPorterDuffColorFilter(
counterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
} else {
// Else reset the color filter and refresh the drawable state so that the
// normal tint is used
DrawableCompat.clearColorFilter(editTextBackground);
editText.refreshDrawableState();
}
}
static class SavedState extends AbsSavedState {
CharSequence error;
boolean isEndIconChecked;
SavedState(Parcelable superState) {
super(superState);
}
SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
isEndIconChecked = (source.readInt() == 1);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
TextUtils.writeToParcel(error, dest, flags);
dest.writeInt(isEndIconChecked ? 1 : 0);
}
@Override
public String toString() {
return "TextInputLayout.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " error="
+ error
+ "}";
}
public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
if (indicatorViewController.errorShouldBeShown()) {
ss.error = getError();
}
ss.isEndIconChecked = hasEndIcon() && endIconView.isChecked();
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setError(ss.error);
if (ss.isEndIconChecked) {
endIconView.performClick();
// Skip animation
endIconView.jumpDrawablesToCurrentState();
}
requestLayout();
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
restoringSavedState = true;
super.dispatchRestoreInstanceState(container);
restoringSavedState = false;
}
/**
* Returns the error message that was set to be displayed with {@link #setError(CharSequence)}, or
* <code>null</code> if no error was set or if error displaying is not enabled.
*
* @see #setError(CharSequence)
*/
@Nullable
public CharSequence getError() {
return indicatorViewController.isErrorEnabled() ? indicatorViewController.getErrorText() : null;
}
/**
* Returns the helper message that was set to be displayed with {@link
* #setHelperText(CharSequence)}, or <code>null</code> if no helper text was set or if helper text
* functionality is not enabled.
*
* @see #setHelperText(CharSequence)
*/
@Nullable
public CharSequence getHelperText() {
return indicatorViewController.isHelperTextEnabled()
? indicatorViewController.getHelperText()
: null;
}
/**
* Returns whether any hint state changes, due to being focused or non-empty text, are animated.
*
* @see #setHintAnimationEnabled(boolean)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintAnimationEnabled
*/
public boolean isHintAnimationEnabled() {
return hintAnimationEnabled;
}
/**
* Set whether any hint state changes, due to being focused or non-empty text, are animated.
*
* @see #isHintAnimationEnabled()
* @attr ref com.google.android.material.R.styleable#TextInputLayout_hintAnimationEnabled
*/
public void setHintAnimationEnabled(boolean enabled) {
hintAnimationEnabled = enabled;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
boolean updatedHeight = updateEditTextHeightBasedOnIcon();
boolean updatedIcon = updateIconDummyDrawables();
if (updatedHeight || updatedIcon) {
editText.post(
new Runnable() {
@Override
public void run() {
editText.requestLayout();
}
});
}
}
private boolean updateEditTextHeightBasedOnIcon() {
if (editText == null) {
return false;
}
// We need to make sure that the EditText's height is at least the same as the end or start
// icon's height (whichever is bigger). This ensures focus works properly, and there is no
// visual jump if the icon is enabled/disabled.
int maxIconHeight =
Math.max(endIconView.getMeasuredHeight(), startIconView.getMeasuredHeight());
if (editText.getMeasuredHeight() < maxIconHeight) {
editText.setMinimumHeight(maxIconHeight);
return true;
}
return false;
}
/**
* Sets the start icon.
*
* <p>If you use an icon you should also set a description for its action using {@link
* #setStartIconContentDescription(CharSequence)}. This is used for accessibility.
*
* @param resId resource id of the drawable to set, or 0 to clear and remove the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconDrawable
*/
public void setStartIconDrawable(@DrawableRes int resId) {
setStartIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
}
/**
* Sets the start icon.
*
* <p>If you use an icon you should also set a description for its action using {@link
* #setStartIconContentDescription(CharSequence)}. This is used for accessibility.
*
* @param startIconDrawable Drawable to set, may be null to clear and remove the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconDrawable
*/
public void setStartIconDrawable(@Nullable Drawable startIconDrawable) {
startIconView.setImageDrawable(startIconDrawable);
if (startIconDrawable != null) {
setStartIconVisible(true);
applyStartIconTint();
} else {
setStartIconVisible(false);
setStartIconOnClickListener(null);
setStartIconContentDescription(null);
}
}
/**
* Returns the start icon.
*
* @see #setStartIconDrawable(Drawable)
* @return the drawable used for the start icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconDrawable
*/
@Nullable
public Drawable getStartIconDrawable() {
return startIconView.getDrawable();
}
/**
* Sets the start icon's functionality that is performed when the start icon is clicked. The icon
* will not be clickable if its listener is null.
*
* @param startIconOnClickListener the {@link android.view.View.OnClickListener} the start icon
* view will have, or null to clear it.
*/
public void setStartIconOnClickListener(OnClickListener startIconOnClickListener) {
setIconOnClickListener(startIconView, startIconOnClickListener);
}
/**
* Sets the start icon to be VISIBLE or GONE.
*
* @param visible whether the icon should be set to visible
*/
public void setStartIconVisible(boolean visible) {
if (isStartIconVisible() != visible) {
startIconView.setVisibility(visible ? View.VISIBLE : View.GONE);
updateIconDummyDrawables();
}
}
/**
* Returns whether the current start icon is visible.
*
* @see #setStartIconVisible(boolean)
*/
public boolean isStartIconVisible() {
return startIconView.getVisibility() == View.VISIBLE;
}
/**
* Sets the current start icon to be checkable or not.
*
* <p>If the icon works just as a button and the fact that it's checked or not doesn't affect its
* behavior, such as the clear text end icon, calling this method is encouraged so that screen
* readers will not announce the icon's checked state.
*
* @param startIconCheckable whether the icon should be checkable
* @attr com.google.android.material.R.styleable#TextInputLayout_startIconCheckable
*/
public void setStartIconCheckable(boolean startIconCheckable) {
startIconView.setCheckable(startIconCheckable);
}
/**
* Returns whether the start icon is checkable.
*
* @see #setStartIconCheckable(boolean)
*/
public boolean isStartIconCheckable() {
return startIconView.isCheckable();
}
/**
* Set a content description for the start icon.
*
* <p>The content description will be read via screen readers or other accessibility systems to
* explain the purpose or action of the icon.
*
* @param resId Resource ID of a content description string to set, or 0 to clear the description
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconContentDescription
*/
public void setStartIconContentDescription(@StringRes int resId) {
setStartIconContentDescription(resId != 0 ? getResources().getText(resId) : null);
}
/**
* Set a content description for the start icon.
*
* <p>The content description will be read via screen readers or other accessibility systems to
* explain the purpose or action of the icon.
*
* @param startIconContentDescription Content description to set, or null to clear the content
* description
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconContentDescription
*/
public void setStartIconContentDescription(@Nullable CharSequence startIconContentDescription) {
if (getStartIconContentDescription() != startIconContentDescription) {
startIconView.setContentDescription(startIconContentDescription);
}
}
/**
* Returns the currently configured content description for the start icon.
*
* <p>This will be used to describe the navigation action to users through mechanisms such as
* screen readers.
*/
@Nullable
public CharSequence getStartIconContentDescription() {
return startIconView.getContentDescription();
}
/**
* Applies a tint to the start icon drawable. Does not modify the current tint mode, which is
* {@link PorterDuff.Mode#SRC_IN} by default.
*
* <p>Subsequent calls to {@link #setStartIconDrawable(Drawable)} will automatically mutate the
* drawable and apply the specified tint and tint mode using {@link
* DrawableCompat#setTintList(Drawable, ColorStateList)}.
*
* @param startIconTintList the tint to apply, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconTint
*/
public void setStartIconTintList(@Nullable ColorStateList startIconTintList) {
if (this.startIconTintList != startIconTintList) {
this.startIconTintList = startIconTintList;
hasStartIconTintList = true;
applyStartIconTint();
}
}
/**
* Specifies the blending mode used to apply the tint specified by {@link
* #setEndIconTintList(ColorStateList)} to the start icon drawable. The default mode is {@link
* PorterDuff.Mode#SRC_IN}.
*
* @param startIconTintMode the blending mode used to apply the tint, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_startIconTintMode
*/
public void setStartIconTintMode(@Nullable PorterDuff.Mode startIconTintMode) {
if (this.startIconTintMode != startIconTintMode) {
this.startIconTintMode = startIconTintMode;
hasStartIconTintMode = true;
applyStartIconTint();
}
}
/**
* Set up the {@link EndIconMode}. When set, a button is placed at the end of the EditText which
* enables the user to perform the specific icon's functionality.
*
* @param endIconMode the {@link EndIconMode} to be set, or END_ICON_NONE to clear the current
* icon if any
* @attr com.google.android.material.R.styleable#TextInputLayout_endIconMode
*/
public void setEndIconMode(@EndIconMode int endIconMode) {
int previousEndIconMode = this.endIconMode;
this.endIconMode = endIconMode;
setEndIconVisible(endIconMode != END_ICON_NONE);
if (getEndIconDelegate().isBoxBackgroundModeSupported(boxBackgroundMode)) {
getEndIconDelegate().initialize();
} else {
throw new IllegalStateException(
"The current box background mode "
+ boxBackgroundMode
+ " is not supported by the end icon mode "
+ endIconMode);
}
applyEndIconTint();
dispatchOnEndIconChanged(previousEndIconMode);
}
/**
* Returns the current {@link EndIconMode}.
*
* @return the end icon mode enum
* @see #setEndIconMode(int)
* @attr com.google.android.material.R.styleable#TextInputLayout_endIconMode
*/
@EndIconMode
public int getEndIconMode() {
return endIconMode;
}
/**
* Sets the end icon's functionality that is performed when the icon is clicked.
*
* @param endIconOnClickListener the {@link android.view.View.OnClickListener} the end icon view
* will have
*/
public void setEndIconOnClickListener(@Nullable OnClickListener endIconOnClickListener) {
setIconOnClickListener(endIconView, endIconOnClickListener);
}
/**
* Sets the current end icon to be VISIBLE or INVISIBLE.
*
* @param visible whether the icon should be set to visible
*/
public void setEndIconVisible(boolean visible) {
if (isEndIconVisible() != visible) {
endIconView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
updateIconDummyDrawables();
}
}
/**
* Returns whether the current end icon is visible.
*
* @see #setEndIconVisible(boolean)
*/
public boolean isEndIconVisible() {
return endIconView.getParent() != null && endIconView.getVisibility() == View.VISIBLE;
}
/**
* Sets the current end icon's state to be activated or not.
*
* @param endIconActivated whether the icon should be activated
*/
public void setEndIconActivated(boolean endIconActivated) {
endIconView.setActivated(endIconActivated);
}
/**
* Sets the current end icon to be checkable or not.
*
* <p>If the icon works just as a button and the fact that it's checked or not doesn't affect its
* behavior, such as the clear text end icon, calling this method is encouraged so that screen
* readers will not announce the icon's checked state.
*
* @param endIconCheckable whether the icon should be checkable
* @attr com.google.android.material.R.styleable#TextInputLayout_endIconCheckable
*/
public void setEndIconCheckable(boolean endIconCheckable) {
endIconView.setCheckable(endIconCheckable);
}
/**
* Returns whether the end icon is checkable.
*
* @see #setEndIconCheckable(boolean)
*/
public boolean isEndIconCheckable() {
return endIconView.isCheckable();
}
/**
* Set the icon to use for the end icon.
*
* <p>If you use an icon you should also set a description for its action using {@link
* #setEndIconContentDescription(CharSequence)}. This is used for accessibility.
*
* @param resId resource id of the drawable to set, or 0 to clear the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconDrawable
*/
public void setEndIconDrawable(@DrawableRes int resId) {
setEndIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
}
/**
* Set the icon to use for the end icon.
*
* <p>If you use an icon you should also set a description for its action using {@link
* #setEndIconContentDescription(CharSequence)}. This is used for accessibility.
*
* @param endIconDrawable Drawable to set, may be null to clear the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconDrawable
*/
public void setEndIconDrawable(@Nullable Drawable endIconDrawable) {
endIconView.setImageDrawable(endIconDrawable);
}
/**
* Returns the drawable currently used for the end icon.
*
* @see #setEndIconDrawable(Drawable)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconDrawable
*/
@Nullable
public Drawable getEndIconDrawable() {
return endIconView.getDrawable();
}
/**
* Set a content description for the end icon.
*
* <p>The content description will be read via screen readers or other accessibility systems to
* explain the action of the icon.
*
* @param resId Resource ID of a content description string to set, or 0 to clear the description
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconContentDescription
*/
public void setEndIconContentDescription(@StringRes int resId) {
setEndIconContentDescription(resId != 0 ? getResources().getText(resId) : null);
}
/**
* Set a content description for the end icon.
*
* <p>The content description will be read via screen readers or other accessibility systems to
* explain the action of the icon.
*
* @param endIconContentDescription Content description to set, or null to clear the content
* description
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconContentDescription
*/
public void setEndIconContentDescription(@Nullable CharSequence endIconContentDescription) {
if (getEndIconContentDescription() != endIconContentDescription) {
endIconView.setContentDescription(endIconContentDescription);
}
}
/**
* Returns the currently configured content description for the end icon.
*
* <p>This will be used to describe the navigation action to users through mechanisms such as
* screen readers.
*/
@Nullable
public CharSequence getEndIconContentDescription() {
return endIconView.getContentDescription();
}
/**
* Applies a tint to the end icon drawable. Does not modify the current tint mode, which is {@link
* PorterDuff.Mode#SRC_IN} by default.
*
* <p>Subsequent calls to {@link #setEndIconDrawable(Drawable)} will automatically mutate the
* drawable and apply the specified tint and tint mode using {@link
* DrawableCompat#setTintList(Drawable, ColorStateList)}.
*
* @param endIconTintList the tint to apply, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconTint
*/
public void setEndIconTintList(@Nullable ColorStateList endIconTintList) {
if (this.endIconTintList != endIconTintList) {
this.endIconTintList = endIconTintList;
hasEndIconTintList = true;
applyEndIconTint();
}
}
/**
* Specifies the blending mode used to apply the tint specified by {@link
* #setEndIconTintList(ColorStateList)} to the end icon drawable. The default mode is {@link
* PorterDuff.Mode#SRC_IN}.
*
* @param endIconTintMode the blending mode used to apply the tint, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_endIconTintMode
*/
public void setEndIconTintMode(@Nullable PorterDuff.Mode endIconTintMode) {
if (this.endIconTintMode != endIconTintMode) {
this.endIconTintMode = endIconTintMode;
hasEndIconTintMode = true;
applyEndIconTint();
}
}
/**
* Add a {@link TextInputLayout.OnEndIconChangedListener} that will be invoked when the end icon
* gets changed.
*
* <p>Components that add a listener should take care to remove it when finished via {@link
* #removeOnEndIconChangedListener(OnEndIconChangedListener)}.
*
* @param listener listener to add
*/
public void addOnEndIconChangedListener(OnEndIconChangedListener listener) {
endIconChangedListeners.add(listener);
}
/**
* Remove the given {@link TextInputLayout.OnEndIconChangedListener} that was previously added via
* {@link #addOnEndIconChangedListener(OnEndIconChangedListener)}.
*
* @param listener listener to remove
*/
public void removeOnEndIconChangedListener(OnEndIconChangedListener listener) {
endIconChangedListeners.remove(listener);
}
/** Remove all previously added {@link TextInputLayout.OnEndIconChangedListener}s. */
public void clearOnEndIconChangedListeners() {
endIconChangedListeners.clear();
}
/**
* Add a {@link OnEditTextAttachedListener} that will be invoked when the edit text is attached,
* or from this method if the EditText is already present.
*
* <p>Components that add a listener should take care to remove it when finished via {@link
* #removeOnEditTextAttachedListener(OnEditTextAttachedListener)}.
*
* @param listener listener to add
*/
public void addOnEditTextAttachedListener(OnEditTextAttachedListener listener) {
editTextAttachedListeners.add(listener);
if (editText != null) {
listener.onEditTextAttached(editText);
}
}
/**
* Remove the given {@link OnEditTextAttachedListener} that was previously added via {@link
* #addOnEditTextAttachedListener(OnEditTextAttachedListener)}.
*
* @param listener listener to remove
*/
public void removeOnEditTextAttachedListener(OnEditTextAttachedListener listener) {
editTextAttachedListeners.remove(listener);
}
/** Remove all previously added {@link OnEditTextAttachedListener}s. */
public void clearOnEditTextAttachedListeners() {
editTextAttachedListeners.clear();
}
/**
* Set the icon to use for the password visibility toggle button.
*
* <p>If you use an icon you should also set a description for its action using {@link
* #setPasswordVisibilityToggleContentDescription(CharSequence)}. This is used for accessibility.
*
* @param resId resource id of the drawable to set, or 0 to clear the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleDrawable
* @deprecated Use {@link #setEndIconDrawable(int)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) {
setPasswordVisibilityToggleDrawable(
resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
}
/**
* Set the icon to use for the password visibility toggle button.
*
* <p>If you use an icon you should also set a description for its action using {@link
* #setPasswordVisibilityToggleContentDescription(CharSequence)}. This is used for accessibility.
*
* @param icon Drawable to set, may be null to clear the icon
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleDrawable
* @deprecated Use {@link #setEndIconDrawable(Drawable)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) {
endIconView.setImageDrawable(icon);
}
/**
* Set a content description for the navigation button if one is present.
*
* <p>The content description will be read via screen readers or other accessibility systems to
* explain the action of the password visibility toggle.
*
* @param resId Resource ID of a content description string to set, or 0 to clear the description
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleContentDescription
* @deprecated Use {@link #setEndIconContentDescription(int)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleContentDescription(@StringRes int resId) {
setPasswordVisibilityToggleContentDescription(
resId != 0 ? getResources().getText(resId) : null);
}
/**
* Set a content description for the navigation button if one is present.
*
* <p>The content description will be read via screen readers or other accessibility systems to
* explain the action of the password visibility toggle.
*
* @param description Content description to set, or null to clear the content description
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleContentDescription
* @deprecated Use {@link #setEndIconContentDescription(CharSequence)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) {
endIconView.setContentDescription(description);
}
/**
* Returns the icon currently used for the password visibility toggle button.
*
* @see #setPasswordVisibilityToggleDrawable(Drawable)
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleDrawable
* @deprecated Use {@link #getEndIconDrawable()} instead.
*/
@Nullable
@Deprecated
public Drawable getPasswordVisibilityToggleDrawable() {
return endIconView.getDrawable();
}
/**
* Returns the currently configured content description for the password visibility toggle button.
*
* <p>This will be used to describe the navigation action to users through mechanisms such as
* screen readers.
*
* @deprecated Use {@link #getEndIconContentDescription()} instead.
*/
@Nullable
@Deprecated
public CharSequence getPasswordVisibilityToggleContentDescription() {
return endIconView.getContentDescription();
}
/**
* Returns whether the password visibility toggle functionality is currently enabled.
*
* @see #setPasswordVisibilityToggleEnabled(boolean)
* @deprecated Use {@link #getEndIconMode()} instead.
*/
@Deprecated
public boolean isPasswordVisibilityToggleEnabled() {
return endIconMode == END_ICON_PASSWORD_TOGGLE;
}
/**
* Enables or disable the password visibility toggle functionality.
*
* <p>When enabled, a button is placed at the end of the EditText which enables the user to switch
* between the field's input being visibly disguised or not.
*
* @param enabled true to enable the functionality
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleEnabled
* @deprecated Use {@link #setEndIconMode(int)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleEnabled(final boolean enabled) {
if (enabled && endIconMode != END_ICON_PASSWORD_TOGGLE) {
// Set password toggle end icon if it's not already set
setEndIconMode(END_ICON_PASSWORD_TOGGLE);
} else if (!enabled) {
// Set end icon to null
setEndIconMode(END_ICON_NONE);
}
}
/**
* Applies a tint to the password visibility toggle drawable. Does not modify the current tint
* mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
*
* <p>Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will
* automatically mutate the drawable and apply the specified tint and tint mode using {@link
* DrawableCompat#setTintList(Drawable, ColorStateList)}.
*
* @param tintList the tint to apply, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleTint
* @deprecated Use {@link #setEndIconTintList(ColorStateList)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) {
endIconTintList = tintList;
hasEndIconTintList = true;
applyEndIconTint();
}
/**
* Specifies the blending mode used to apply the tint specified by {@link
* #setPasswordVisibilityToggleTintList(ColorStateList)} to the password visibility toggle
* drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
*
* @param mode the blending mode used to apply the tint, may be null to clear tint
* @attr ref com.google.android.material.R.styleable#TextInputLayout_passwordToggleTintMode
* @deprecated Use {@link #setEndIconTintMode(PorterDuff.Mode)} instead.
*/
@Deprecated
public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) {
endIconTintMode = mode;
hasEndIconTintMode = true;
applyEndIconTint();
}
/**
* Handles visibility for a password toggle icon when changing obfuscation in a password edit
* text. Public so that clients can override this method for custom UI changes when toggling the
* display of password text
*
* @param shouldSkipAnimations true if the password toggle indicator icon should not animate
* changes
* @deprecated The password toggle will show as checked or unchecked depending on whether the
* {@link EditText}'s {@link android.text.method.TransformationMethod} is of type {@link
* android.text.method.PasswordTransformationMethod}
*/
@Deprecated
public void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) {
if (endIconMode == END_ICON_PASSWORD_TOGGLE) {
endIconView.performClick();
if (shouldSkipAnimations) {
endIconView.jumpDrawablesToCurrentState();
}
}
}
/**
* Sets an {@link TextInputLayout.AccessibilityDelegate} providing an accessibility implementation
* for the {@link EditText} used by this layout.
*
* <p>Note: This method should be used in place of providing an {@link AccessibilityDelegate}
* directly on the {@link EditText}.
*/
public void setTextInputAccessibilityDelegate(TextInputLayout.AccessibilityDelegate delegate) {
if (editText != null) {
ViewCompat.setAccessibilityDelegate(editText, delegate);
}
}
CheckableImageButton getEndIconView() {
return endIconView;
}
private EndIconDelegate getEndIconDelegate() {
EndIconDelegate endIconDelegate = endIconDelegates.get(endIconMode);
return endIconDelegate != null ? endIconDelegate : endIconDelegates.get(END_ICON_NONE);
}
private void dispatchOnEditTextAttached() {
for (OnEditTextAttachedListener listener : editTextAttachedListeners) {
listener.onEditTextAttached(editText);
}
}
private boolean hasStartIcon() {
return getStartIconDrawable() != null;
}
private void applyStartIconTint() {
applyIconTint(
startIconView,
hasStartIconTintList,
startIconTintList,
hasStartIconTintMode,
startIconTintMode);
}
private boolean hasEndIcon() {
return endIconMode != END_ICON_NONE;
}
private void dispatchOnEndIconChanged(@EndIconMode int previousIcon) {
for (OnEndIconChangedListener listener : endIconChangedListeners) {
listener.onEndIconChanged(previousIcon);
}
}
private void tintEndIconOnError(boolean tintEndIconOnError) {
if (tintEndIconOnError && getEndIconDrawable() != null) {
// Setting the tint here instead of calling setEndIconTintList() in order to preserve and
// restore the icon's original tint.
Drawable endIconDrawable = DrawableCompat.wrap(getEndIconDrawable()).mutate();
DrawableCompat.setTint(
endIconDrawable, indicatorViewController.getErrorViewCurrentTextColor());
endIconView.setImageDrawable(endIconDrawable);
} else {
applyEndIconTint();
}
}
private void applyEndIconTint() {
applyIconTint(
endIconView, hasEndIconTintList, endIconTintList, hasEndIconTintMode, endIconTintMode);
}
/*
* We need to add a dummy drawable as the start and/or end compound drawables so that the text is
* indented and doesn't display below the icon views.
*/
private boolean updateIconDummyDrawables() {
if (editText == null) {
return false;
}
boolean updatedIcon = false;
// Update start icon drawable if needed.
if (hasStartIcon() && isStartIconVisible() && startIconView.getMeasuredWidth() > 0) {
if (startIconDummyDrawable == null) {
startIconDummyDrawable = new ColorDrawable();
int right =
startIconView.getMeasuredWidth()
- editText.getPaddingLeft()
+ MarginLayoutParamsCompat.getMarginEnd(
((MarginLayoutParams) startIconView.getLayoutParams()));
startIconDummyDrawable.setBounds(0, 0, right, 1);
}
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(editText);
if (compounds[0] != startIconDummyDrawable) {
TextViewCompat.setCompoundDrawablesRelative(
editText, startIconDummyDrawable, compounds[1], compounds[2], compounds[3]);
updatedIcon = true;
}
} else if (startIconDummyDrawable != null) {
// Remove the dummy start compound drawable if it exists and clear it.
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(editText);
TextViewCompat.setCompoundDrawablesRelative(
editText, null, compounds[1], compounds[2], compounds[3]);
startIconDummyDrawable = null;
updatedIcon = true;
}
// Update end icon or error icon drawable if needed.
CheckableImageButton iconView = getEndIconToUpdateDummyDrawable();
if (iconView != null && iconView.getMeasuredWidth() > 0) {
if (endIconDummyDrawable == null) {
endIconDummyDrawable = new ColorDrawable();
int right =
iconView.getMeasuredWidth()
- editText.getPaddingRight()
+ MarginLayoutParamsCompat.getMarginStart(
((MarginLayoutParams) iconView.getLayoutParams()));
endIconDummyDrawable.setBounds(0, 0, right, 1);
}
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(editText);
// Store the user defined end compound drawable so that we can restore it later.
if (compounds[2] != endIconDummyDrawable) {
originalEditTextEndDrawable = compounds[2];
TextViewCompat.setCompoundDrawablesRelative(
editText, compounds[0], compounds[1], endIconDummyDrawable, compounds[3]);
updatedIcon = true;
}
} else if (endIconDummyDrawable != null) {
// Remove the dummy end compound drawable if it exists and clear it.
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(editText);
if (compounds[2] == endIconDummyDrawable) {
TextViewCompat.setCompoundDrawablesRelative(
editText, compounds[0], compounds[1], originalEditTextEndDrawable, compounds[3]);
updatedIcon = true;
}
endIconDummyDrawable = null;
}
return updatedIcon;
}
@Nullable
private CheckableImageButton getEndIconToUpdateDummyDrawable() {
if (errorIconView.getVisibility() == VISIBLE) {
return errorIconView;
} else if (hasEndIcon() && isEndIconVisible()) {
return endIconView;
} else {
return null;
}
}
private void applyIconTint(
CheckableImageButton iconView,
boolean hasIconTintList,
ColorStateList iconTintList,
boolean hasIconTintMode,
PorterDuff.Mode iconTintMode) {
Drawable icon = iconView.getDrawable();
if (icon != null && (hasIconTintList || hasIconTintMode)) {
icon = DrawableCompat.wrap(icon).mutate();
if (hasIconTintList) {
DrawableCompat.setTintList(icon, iconTintList);
}
if (hasIconTintMode) {
DrawableCompat.setTintMode(icon, iconTintMode);
}
}
if (iconView.getDrawable() != icon) {
iconView.setImageDrawable(icon);
}
}
private void setIconOnClickListener(@NonNull View iconView, OnClickListener onClickListener) {
boolean clickable = onClickListener != null;
iconView.setOnClickListener(onClickListener);
iconView.setFocusable(clickable);
iconView.setClickable(clickable);
int importantForAccessibility =
clickable
? ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
: ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO;
ViewCompat.setImportantForAccessibility(iconView, importantForAccessibility);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (editText != null) {
Rect rect = tmpRect;
DescendantOffsetUtils.getDescendantRect(this, editText, rect);
updateBoxUnderlineBounds(rect);
if (hintEnabled) {
collapsingTextHelper.setCollapsedBounds(calculateCollapsedTextBounds(rect));
collapsingTextHelper.setExpandedBounds(calculateExpandedTextBounds(rect));
collapsingTextHelper.recalculate();
// If the label should be collapsed, set the cutout bounds on the CutoutDrawable to make
// sure it draws with a cutout in draw().
if (cutoutEnabled() && !hintExpanded) {
openCutout();
}
}
}
}
private void updateBoxUnderlineBounds(Rect bounds) {
if (boxUnderline != null) {
int top = bounds.bottom - boxStrokeWidthFocusedPx;
boxUnderline.setBounds(bounds.left, top, bounds.right, bounds.bottom);
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
drawHint(canvas);
drawBoxUnderline(canvas);
}
private void drawHint(Canvas canvas) {
if (hintEnabled) {
collapsingTextHelper.draw(canvas);
}
}
private void drawBoxUnderline(Canvas canvas) {
if (boxUnderline != null) {
// Draw using the current boxStrokeWidth.
Rect underlineBounds = boxUnderline.getBounds();
underlineBounds.top = underlineBounds.bottom - boxStrokeWidthPx;
boxUnderline.draw(canvas);
}
}
private void collapseHint(boolean animate) {
if (animator != null && animator.isRunning()) {
animator.cancel();
}
if (animate && hintAnimationEnabled) {
animateToExpansionFraction(1f);
} else {
collapsingTextHelper.setExpansionFraction(1f);
}
hintExpanded = false;
if (cutoutEnabled()) {
openCutout();
}
}
private boolean cutoutEnabled() {
return hintEnabled && !TextUtils.isEmpty(hint) && boxBackground instanceof CutoutDrawable;
}
private void openCutout() {
if (!cutoutEnabled()) {
return;
}
final RectF cutoutBounds = tmpRectF;
collapsingTextHelper.getCollapsedTextActualBounds(cutoutBounds);
applyCutoutPadding(cutoutBounds);
// Offset the cutout bounds by the TextInputLayout's left padding to ensure that the cutout is
// inset relative to the TextInputLayout's bounds.
cutoutBounds.offset(-getPaddingLeft(), 0);
((CutoutDrawable) boxBackground).setCutout(cutoutBounds);
}
private void closeCutout() {
if (cutoutEnabled()) {
((CutoutDrawable) boxBackground).removeCutout();
}
}
private void applyCutoutPadding(RectF cutoutBounds) {
cutoutBounds.left -= boxLabelCutoutPaddingPx;
cutoutBounds.top -= boxLabelCutoutPaddingPx;
cutoutBounds.right += boxLabelCutoutPaddingPx;
cutoutBounds.bottom += boxLabelCutoutPaddingPx;
}
@VisibleForTesting
boolean cutoutIsOpen() {
return cutoutEnabled() && ((CutoutDrawable) boxBackground).hasCutout();
}
@Override
protected void drawableStateChanged() {
if (inDrawableStateChanged) {
// Some of the calls below will update the drawable state of child views. Since we're
// using addStatesFromChildren we can get into infinite recursion, hence we'll just
// exit in this instance
return;
}
inDrawableStateChanged = true;
super.drawableStateChanged();
final int[] state = getDrawableState();
boolean changed = false;
if (collapsingTextHelper != null) {
changed |= collapsingTextHelper.setState(state);
}
// Drawable state has changed so see if we need to update the label
updateLabelState(ViewCompat.isLaidOut(this) && isEnabled());
updateEditTextBackground();
updateTextInputBoxState();
if (changed) {
invalidate();
}
inDrawableStateChanged = false;
}
void updateTextInputBoxState() {
if (boxBackground == null || boxBackgroundMode == BOX_BACKGROUND_NONE) {
return;
}
final boolean hasFocus = isFocused() || (editText != null && editText.hasFocus());
final boolean isHovered = isHovered() || (editText != null && editText.isHovered());
// Update the text box's stroke color based on the current state.
if (!isEnabled()) {
boxStrokeColor = disabledColor;
} else if (indicatorViewController.errorShouldBeShown()) {
boxStrokeColor = indicatorViewController.getErrorViewCurrentTextColor();
} else if (counterOverflowed && counterView != null) {
boxStrokeColor = counterView.getCurrentTextColor();
} else if (hasFocus) {
boxStrokeColor = focusedStrokeColor;
} else if (isHovered) {
boxStrokeColor = hoveredStrokeColor;
} else {
boxStrokeColor = defaultStrokeColor;
}
tintEndIconOnError(
indicatorViewController.errorShouldBeShown()
&& getEndIconDelegate().shouldTintIconOnError());
setErrorIconVisible(
getErrorIconDrawable() != null && indicatorViewController.errorShouldBeShown());
// Update the text box's stroke width based on the current state.
if ((isHovered || hasFocus) && isEnabled()) {
boxStrokeWidthPx = boxStrokeWidthFocusedPx;
} else {
boxStrokeWidthPx = boxStrokeWidthDefaultPx;
}
// Update the text box's background color based on the current state.
if (boxBackgroundMode == BOX_BACKGROUND_FILLED) {
if (!isEnabled()) {
boxBackgroundColor = disabledFilledBackgroundColor;
} else if (isHovered) {
boxBackgroundColor = hoveredFilledBackgroundColor;
} else {
boxBackgroundColor = defaultFilledBackgroundColor;
}
}
applyBoxAttributes();
}
private void setErrorIconVisible(boolean errorIconVisible) {
int newErrorIconVisibility = errorIconVisible ? VISIBLE : GONE;
if (errorIconView.getVisibility() == newErrorIconVisibility) {
return;
}
errorIconView.setVisibility(newErrorIconVisibility);
if (errorIconVisible) {
inputFrame.removeView(endIconView);
} else if (endIconView.getParent() == null) {
inputFrame.addView(endIconView);
}
if (!hasEndIcon()) {
updateIconDummyDrawables();
}
}
private void expandHint(boolean animate) {
if (animator != null && animator.isRunning()) {
animator.cancel();
}
if (animate && hintAnimationEnabled) {
animateToExpansionFraction(0f);
} else {
collapsingTextHelper.setExpansionFraction(0f);
}
if (cutoutEnabled() && ((CutoutDrawable) boxBackground).hasCutout()) {
closeCutout();
}
hintExpanded = true;
}
@VisibleForTesting
void animateToExpansionFraction(final float target) {
if (collapsingTextHelper.getExpansionFraction() == target) {
return;
}
if (this.animator == null) {
this.animator = new ValueAnimator();
this.animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
this.animator.setDuration(LABEL_SCALE_ANIMATION_DURATION);
this.animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
collapsingTextHelper.setExpansionFraction((float) animator.getAnimatedValue());
}
});
}
this.animator.setFloatValues(collapsingTextHelper.getExpansionFraction(), target);
this.animator.start();
}
@VisibleForTesting
final boolean isHintExpanded() {
return hintExpanded;
}
@VisibleForTesting
final boolean isHelperTextDisplayed() {
return indicatorViewController.helperTextIsDisplayed();
}
@VisibleForTesting
final int getHintCurrentCollapsedTextColor() {
return collapsingTextHelper.getCurrentCollapsedTextColor();
}
@VisibleForTesting
final float getHintCollapsedTextHeight() {
return collapsingTextHelper.getCollapsedTextHeight();
}
@VisibleForTesting
final int getErrorTextCurrentColor() {
return indicatorViewController.getErrorViewCurrentTextColor();
}
/**
* An AccessibilityDelegate intended to be set on an {@link EditText} or {@link TextInputEditText}
* with {@link
* TextInputLayout#setTextInputAccessibilityDelegate(TextInputLayout.AccessibilityDelegate) to
* provide attributes for accessibility that are managed by {@link TextInputLayout}.
*/
public static class AccessibilityDelegate extends AccessibilityDelegateCompat {
private final TextInputLayout layout;
public AccessibilityDelegate(TextInputLayout layout) {
this.layout = layout;
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
EditText editText = layout.getEditText();
CharSequence text = (editText != null) ? editText.getText() : null;
CharSequence hintText = layout.getHint();
CharSequence errorText = layout.getError();
CharSequence counterDesc = layout.getCounterOverflowDescription();
boolean showingText = !TextUtils.isEmpty(text);
boolean hasHint = !TextUtils.isEmpty(hintText);
boolean showingError = !TextUtils.isEmpty(errorText);
boolean contentInvalid = showingError || !TextUtils.isEmpty(counterDesc);
if (showingText) {
info.setText(text);
} else if (hasHint) {
info.setText(hintText);
}
if (hasHint) {
info.setHintText(hintText);
info.setShowingHintText(!showingText && hasHint);
}
if (contentInvalid) {
info.setError(showingError ? errorText : counterDesc);
info.setContentInvalid(true);
}
}
}
}