Skip to content

Commit 3f73804

Browse files
leticiarossidsn5ft
authored andcommitted
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
1 parent 1876bc5 commit 3f73804

9 files changed

Lines changed: 307 additions & 7 deletions

File tree

lib/java/com/google/android/material/textfield/DropdownMenuEndIconDelegate.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ public void onEditTextAttached(EditText editText) {
126126
autoCompleteTextView.setThreshold(0);
127127
editText.removeTextChangedListener(exposedDropdownEndIconTextWatcher);
128128
editText.addTextChangedListener(exposedDropdownEndIconTextWatcher);
129+
textInputLayout.setErrorIconDrawable(null);
129130
textInputLayout.setTextInputAccessibilityDelegate(accessibilityDelegate);
130131

131132
textInputLayout.setEndIconVisible(true);

lib/java/com/google/android/material/textfield/TextInputLayout.java

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ public interface OnEndIconChangedListener {
349349
private boolean hasEndIconTintMode;
350350
private Drawable endIconDummyDrawable;
351351
private Drawable originalEditTextEndDrawable;
352+
private final CheckableImageButton errorIconView;
352353

353354
private ColorStateList defaultHintTextColor;
354355
private ColorStateList focusedTextColor;
@@ -522,6 +523,32 @@ public TextInputLayout(Context context, @Nullable AttributeSet attrs, int defSty
522523
final int errorTextAppearance =
523524
a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
524525
final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
526+
// Initialize error icon view.
527+
errorIconView =
528+
(CheckableImageButton)
529+
LayoutInflater.from(getContext())
530+
.inflate(R.layout.design_text_input_end_icon, inputFrame, false);
531+
inputFrame.addView(errorIconView);
532+
errorIconView.setVisibility(GONE);
533+
if (a.hasValue(R.styleable.TextInputLayout_errorIconDrawable)) {
534+
setErrorIconDrawable(a.getDrawable(R.styleable.TextInputLayout_errorIconDrawable));
535+
}
536+
if (a.hasValue(R.styleable.TextInputLayout_errorIconTint)) {
537+
setErrorIconTintList(
538+
MaterialResources.getColorStateList(
539+
context, a, R.styleable.TextInputLayout_errorIconTint));
540+
}
541+
if (a.hasValue(R.styleable.TextInputLayout_errorIconTintMode)) {
542+
setErrorIconTintMode(
543+
ViewUtils.parseTintMode(
544+
a.getInt(R.styleable.TextInputLayout_errorIconTintMode, -1), null));
545+
}
546+
errorIconView.setContentDescription(
547+
getResources().getText(R.string.error_icon_content_description));
548+
ViewCompat
549+
.setImportantForAccessibility(errorIconView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
550+
errorIconView.setClickable(false);
551+
errorIconView.setFocusable(false);
525552

526553
final int helperTextTextAppearance =
527554
a.getResourceId(R.styleable.TextInputLayout_helperTextTextAppearance, 0);
@@ -1051,6 +1078,7 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {}
10511078

10521079
startIconView.bringToFront();
10531080
endIconView.bringToFront();
1081+
errorIconView.bringToFront();
10541082
dispatchOnEditTextAttached();
10551083

10561084
// Update the label visibility with no animation, but force a state change
@@ -1422,6 +1450,75 @@ public void setError(@Nullable final CharSequence errorText) {
14221450
}
14231451
}
14241452

1453+
/**
1454+
* Set the drawable to use for the error icon.
1455+
*
1456+
* @param resId resource id of the drawable to set, or 0 to clear the icon
1457+
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconDrawable
1458+
*/
1459+
public void setErrorIconDrawable(@DrawableRes int resId) {
1460+
setErrorIconDrawable(resId != 0 ? AppCompatResources.getDrawable(getContext(), resId) : null);
1461+
}
1462+
1463+
/**
1464+
* Set the drawable to use for the error icon.
1465+
*
1466+
* @param errorIconDrawable Drawable to set, may be null to clear the icon
1467+
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconDrawable
1468+
*/
1469+
public void setErrorIconDrawable(@Nullable Drawable errorIconDrawable) {
1470+
errorIconView.setImageDrawable(errorIconDrawable);
1471+
setErrorIconVisible(errorIconDrawable != null);
1472+
}
1473+
1474+
/**
1475+
* Returns the drawable currently used for the error icon.
1476+
*
1477+
* @see #setErrorIconDrawable(Drawable)
1478+
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconDrawable
1479+
*/
1480+
@Nullable
1481+
public Drawable getErrorIconDrawable() {
1482+
return errorIconView.getDrawable();
1483+
}
1484+
1485+
/**
1486+
* Applies a tint to the error icon drawable.
1487+
*
1488+
* @param errorIconTintList the tint to apply, may be null to clear tint
1489+
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconTint
1490+
*/
1491+
public void setErrorIconTintList(@Nullable ColorStateList errorIconTintList) {
1492+
Drawable icon = errorIconView.getDrawable();
1493+
if (icon != null) {
1494+
icon = DrawableCompat.wrap(icon).mutate();
1495+
DrawableCompat.setTintList(icon, errorIconTintList);
1496+
}
1497+
1498+
if (errorIconView.getDrawable() != icon) {
1499+
errorIconView.setImageDrawable(icon);
1500+
}
1501+
}
1502+
1503+
/**
1504+
* Specifies the blending mode used to apply tint to the end icon drawable. The default mode is
1505+
* {@link PorterDuff.Mode#SRC_IN}.
1506+
*
1507+
* @param errorIconTintMode the blending mode used to apply the tint, may be null to clear tint
1508+
* @attr ref com.google.android.material.R.styleable#TextInputLayout_errorIconTintMode
1509+
*/
1510+
public void setErrorIconTintMode(@Nullable PorterDuff.Mode errorIconTintMode) {
1511+
Drawable icon = errorIconView.getDrawable();
1512+
if (icon != null) {
1513+
icon = DrawableCompat.wrap(icon).mutate();
1514+
DrawableCompat.setTintMode(icon, errorIconTintMode);
1515+
}
1516+
1517+
if (errorIconView.getDrawable() != icon) {
1518+
errorIconView.setImageDrawable(icon);
1519+
}
1520+
}
1521+
14251522
/**
14261523
* Whether the character counter functionality is enabled or not in this layout.
14271524
*
@@ -2270,7 +2367,7 @@ public void setEndIconVisible(boolean visible) {
22702367
* @see #setEndIconVisible(boolean)
22712368
*/
22722369
public boolean isEndIconVisible() {
2273-
return endIconView.getVisibility() == View.VISIBLE;
2370+
return endIconView.getParent() != null && endIconView.getVisibility() == View.VISIBLE;
22742371
}
22752372

22762373
/**
@@ -2758,15 +2855,16 @@ private boolean updateIconDummyDrawables() {
27582855
updatedIcon = true;
27592856
}
27602857

2761-
// Update end icon drawable if needed.
2762-
if (hasEndIcon() && isEndIconVisible() && endIconView.getMeasuredWidth() > 0) {
2858+
// Update end icon or error icon drawable if needed.
2859+
CheckableImageButton iconView = getEndIconToUpdateDummyDrawable();
2860+
if (iconView != null && iconView.getMeasuredWidth() > 0) {
27632861
if (endIconDummyDrawable == null) {
27642862
endIconDummyDrawable = new ColorDrawable();
27652863
int right =
2766-
endIconView.getMeasuredWidth()
2864+
iconView.getMeasuredWidth()
27672865
- editText.getPaddingRight()
27682866
+ MarginLayoutParamsCompat.getMarginStart(
2769-
((MarginLayoutParams) endIconView.getLayoutParams()));
2867+
((MarginLayoutParams) iconView.getLayoutParams()));
27702868
endIconDummyDrawable.setBounds(0, 0, right, 1);
27712869
}
27722870
final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(editText);
@@ -2791,6 +2889,17 @@ private boolean updateIconDummyDrawables() {
27912889
return updatedIcon;
27922890
}
27932891

2892+
@Nullable
2893+
private CheckableImageButton getEndIconToUpdateDummyDrawable() {
2894+
if (errorIconView.getVisibility() == VISIBLE) {
2895+
return errorIconView;
2896+
} else if (hasEndIcon() && isEndIconVisible()) {
2897+
return endIconView;
2898+
} else {
2899+
return null;
2900+
}
2901+
}
2902+
27942903
private void applyIconTint(
27952904
CheckableImageButton iconView,
27962905
boolean hasIconTintList,
@@ -2987,6 +3096,8 @@ void updateTextInputBoxState() {
29873096
tintEndIconOnError(
29883097
indicatorViewController.errorShouldBeShown()
29893098
&& getEndIconDelegate().shouldTintIconOnError());
3099+
setErrorIconVisible(
3100+
getErrorIconDrawable() != null && indicatorViewController.errorShouldBeShown());
29903101

29913102
// Update the text box's stroke width based on the current state.
29923103
if ((isHovered || hasFocus) && isEnabled()) {
@@ -3009,6 +3120,25 @@ void updateTextInputBoxState() {
30093120
applyBoxAttributes();
30103121
}
30113122

3123+
private void setErrorIconVisible(boolean errorIconVisible) {
3124+
int newErrorIconVisibility = errorIconVisible ? VISIBLE : GONE;
3125+
if (errorIconView.getVisibility() == newErrorIconVisibility) {
3126+
return;
3127+
}
3128+
3129+
errorIconView.setVisibility(newErrorIconVisibility);
3130+
3131+
if (errorIconVisible) {
3132+
inputFrame.removeView(endIconView);
3133+
} else if (endIconView.getParent() == null) {
3134+
inputFrame.addView(endIconView);
3135+
}
3136+
3137+
if (!hasEndIcon()) {
3138+
updateIconDummyDrawables();
3139+
}
3140+
}
3141+
30123142
private void expandHint(boolean animate) {
30133143
if (animator != null && animator.isRunning()) {
30143144
animator.cancel();

lib/java/com/google/android/material/textfield/res-public/values/public.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
<public name="errorEnabled" type="attr"/>
4646
<public name="errorTextAppearance" type="attr"/>
4747
<public name="errorTextColor" type="attr"/>
48+
<public name="errorIconDrawable" type="attr"/>
49+
<public name="errorIconTint" type="attr"/>
50+
<public name="errorIconTintMode" type="attr"/>
4851
<public name="helperText" type="attr"/>
4952
<public name="helperTextEnabled" type="attr"/>
5053
<public name="helperTextTextAppearance" type="attr"/>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (C) 2019 The Android Open Source Project
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
19+
android:height="24dp"
20+
android:viewportHeight="24.0"
21+
android:viewportWidth="24.0"
22+
android:width="24dp">
23+
24+
<path
25+
android:fillColor="@android:color/white"
26+
android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
27+
28+
</vector>

lib/java/com/google/android/material/textfield/res/values/attrs.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@
5252
<!-- Text color for any error message displayed.
5353
If set, this takes precedence over errorTextAppearance. -->
5454
<attr name="errorTextColor" format="color"/>
55+
<!-- End icon to be shown when an error is displayed. -->
56+
<attr name="errorIconDrawable" format="reference"/>
57+
<!-- Tint color to use for the error icon. -->
58+
<attr name="errorIconTint" format="reference"/>
59+
<!-- Blending mode used to apply the error icon tint. -->
60+
<attr name="errorIconTintMode">
61+
<!-- The tint is drawn on top of the drawable.
62+
[Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] -->
63+
<enum name="src_over" value="3"/>
64+
<!-- The tint is masked by the alpha channel of the drawable. The drawable’s
65+
color channels are thrown out. [Sa * Da, Sc * Da] -->
66+
<enum name="src_in" value="5"/>
67+
<!-- The tint is drawn above the drawable, but with the drawable’s alpha
68+
channel masking the result. [Da, Sc * Da + (1 - Sa) * Dc] -->
69+
<enum name="src_atop" value="9"/>
70+
<!-- Multiplies the color and alpha channels of the drawable with those of
71+
the tint. [Sa * Da, Sc * Dc] -->
72+
<enum name="multiply" value="14"/>
73+
<!-- [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] -->
74+
<enum name="screen" value="15"/>
75+
</attr>
5576

5677
<!-- Whether the layout is laid out as if the character counter will be displayed. -->
5778
<attr name="counterEnabled" format="boolean"/>

lib/java/com/google/android/material/textfield/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@
2727
<string name="clear_text_end_icon_content_description">Clear text</string>
2828
<!-- Content description for the exposed dropdown menu button. [CHAR LIMIT=NONE] -->
2929
<string name="exposed_dropdown_menu_content_description">Show dropdown menu</string>
30+
<!-- Content description for the error icon. [CHAR LIMIT=NONE] -->
31+
<string name="error_icon_content_description">Error</string>
3032
</resources>

lib/java/com/google/android/material/textfield/res/values/styles.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<item name="passwordToggleDrawable">@drawable/design_password_eye</item>
2727
<item name="passwordToggleTint">@color/design_icon_tint</item>
2828
<item name="passwordToggleContentDescription">@string/password_toggle_content_description</item>
29+
<item name="errorIconDrawable">@null</item>
2930
<item name="endIconTint">@color/design_icon_tint</item>
3031
<item name="startIconTint">@color/design_icon_tint</item>
3132

@@ -52,6 +53,8 @@
5253

5354
<item name="boxBackgroundMode">outline</item>
5455
<item name="boxBackgroundColor">@null</item>
56+
<item name="errorIconDrawable">@drawable/mtrl_ic_error</item>
57+
<item name="errorIconTint">@color/mtrl_error</item>
5558
<item name="endIconTint">@color/mtrl_outlined_icon_tint</item>
5659
<item name="startIconTint">@color/mtrl_outlined_icon_tint</item>
5760
<item name="boxCollapsedPaddingTop">0dp</item>

tests/javatests/com/google/android/material/textfield/ExposedDropdownMenuTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static androidx.test.espresso.contrib.AccessibilityChecks.accessibilityAssertion;
2929
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
3030
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
31+
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
3132
import static androidx.test.espresso.matcher.ViewMatchers.withId;
3233
import static org.hamcrest.MatcherAssert.assertThat;
3334
import static org.hamcrest.Matchers.allOf;
@@ -140,7 +141,9 @@ public void testEndIconHasDefaultContentDescription() {
140141

141142
@Test
142143
public void testEndIconIsAccessible() {
143-
onView(allOf(withId(R.id.text_input_end_icon), isDescendantOfA(withId(R.id.filled_dropdown))))
144+
onView(allOf(withId(R.id.text_input_end_icon),
145+
withContentDescription(R.string.exposed_dropdown_menu_content_description),
146+
isDescendantOfA(withId(R.id.filled_dropdown))))
144147
.check(accessibilityAssertion());
145148
}
146149
}

0 commit comments

Comments
 (0)