1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.design.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.graphics.drawable.ColorDrawable;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.DrawableContainer;
30import android.os.Build;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.support.annotation.DrawableRes;
34import android.support.annotation.NonNull;
35import android.support.annotation.Nullable;
36import android.support.annotation.StringRes;
37import android.support.annotation.StyleRes;
38import android.support.annotation.VisibleForTesting;
39import android.support.design.R;
40import android.support.v4.content.ContextCompat;
41import android.support.v4.graphics.drawable.DrawableCompat;
42import android.support.v4.os.ParcelableCompat;
43import android.support.v4.os.ParcelableCompatCreatorCallbacks;
44import android.support.v4.view.AbsSavedState;
45import android.support.v4.view.AccessibilityDelegateCompat;
46import android.support.v4.view.GravityCompat;
47import android.support.v4.view.ViewCompat;
48import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
49import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
50import android.support.v4.widget.Space;
51import android.support.v4.widget.TextViewCompat;
52import android.support.v7.content.res.AppCompatResources;
53import android.support.v7.widget.AppCompatDrawableManager;
54import android.support.v7.widget.TintTypedArray;
55import android.text.Editable;
56import android.text.TextUtils;
57import android.text.TextWatcher;
58import android.text.method.PasswordTransformationMethod;
59import android.util.AttributeSet;
60import android.util.Log;
61import android.view.Gravity;
62import android.view.LayoutInflater;
63import android.view.View;
64import android.view.ViewGroup;
65import android.view.accessibility.AccessibilityEvent;
66import android.view.animation.AccelerateInterpolator;
67import android.widget.EditText;
68import android.widget.FrameLayout;
69import android.widget.LinearLayout;
70import android.widget.TextView;
71
72/**
73 * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label
74 * when the hint is hidden due to the user inputting text.
75 *
76 * <p>Also supports showing an error via {@link #setErrorEnabled(boolean)} and
77 * {@link #setError(CharSequence)}, and a character counter via
78 * {@link #setCounterEnabled(boolean)}.</p>
79 *
80 * <p>Password visibility toggling is also supported via the
81 * {@link #setPasswordVisibilityToggleEnabled(boolean)} API and related attribute.
82 * If enabled, a button is displayed to toggle between the password being displayed as plain-text
83 * or disguised, when your EditText is set to display a password.</p>
84 *
85 * <p><strong>Note:</strong> When using the password toggle functionality, the 'end' compound
86 * drawable of the EditText will be overridden while the toggle is enabled. To ensure that any
87 * existing drawables are restored correctly, you should set those compound drawables relatively
88 * (start/end), opposed to absolutely (left/right).</p>
89 *
90 * The {@link TextInputEditText} class is provided to be used as a child of this layout. Using
91 * TextInputEditText allows TextInputLayout greater control over the visual aspects of any
92 * text input. An example usage is as so:
93 *
94 * <pre>
95 * &lt;android.support.design.widget.TextInputLayout
96 *         android:layout_width=&quot;match_parent&quot;
97 *         android:layout_height=&quot;wrap_content&quot;&gt;
98 *
99 *     &lt;android.support.design.widget.TextInputEditText
100 *             android:layout_width=&quot;match_parent&quot;
101 *             android:layout_height=&quot;wrap_content&quot;
102 *             android:hint=&quot;@string/form_username&quot;/&gt;
103 *
104 * &lt;/android.support.design.widget.TextInputLayout&gt;
105 * </pre>
106 *
107 * <p><strong>Note:</strong> The actual view hierarchy present under TextInputLayout is
108 * <strong>NOT</strong> guaranteed to match the view hierarchy as written in XML. As a result,
109 * calls to getParent() on children of the TextInputLayout -- such as an TextInputEditText --
110 * may not return the TextInputLayout itself, but rather an intermediate View. If you need
111 * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}.
112 */
113public class TextInputLayout extends LinearLayout {
114
115    private static final int ANIMATION_DURATION = 200;
116    private static final int INVALID_MAX_LENGTH = -1;
117
118    private static final String LOG_TAG = "TextInputLayout";
119
120    private final FrameLayout mInputFrame;
121    EditText mEditText;
122
123    private boolean mHintEnabled;
124    private CharSequence mHint;
125
126    private Paint mTmpPaint;
127    private final Rect mTmpRect = new Rect();
128
129    private LinearLayout mIndicatorArea;
130    private int mIndicatorsAdded;
131
132    private boolean mErrorEnabled;
133    TextView mErrorView;
134    private int mErrorTextAppearance;
135    private boolean mErrorShown;
136    private CharSequence mError;
137
138    boolean mCounterEnabled;
139    private TextView mCounterView;
140    private int mCounterMaxLength;
141    private int mCounterTextAppearance;
142    private int mCounterOverflowTextAppearance;
143    private boolean mCounterOverflowed;
144
145    private boolean mPasswordToggleEnabled;
146    private Drawable mPasswordToggleDrawable;
147    private CharSequence mPasswordToggleContentDesc;
148    private CheckableImageButton mPasswordToggleView;
149    private boolean mPasswordToggledVisible;
150    private Drawable mPasswordToggleDummyDrawable;
151    private Drawable mOriginalEditTextEndDrawable;
152
153    private ColorStateList mPasswordToggleTintList;
154    private boolean mHasPasswordToggleTintList;
155    private PorterDuff.Mode mPasswordToggleTintMode;
156    private boolean mHasPasswordToggleTintMode;
157
158    private ColorStateList mDefaultTextColor;
159    private ColorStateList mFocusedTextColor;
160
161    // Only used for testing
162    private boolean mHintExpanded;
163
164    final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this);
165
166    private boolean mHintAnimationEnabled;
167    private ValueAnimatorCompat mAnimator;
168
169    private boolean mHasReconstructedEditTextBackground;
170    private boolean mInDrawableStateChanged;
171
172    public TextInputLayout(Context context) {
173        this(context, null);
174    }
175
176    public TextInputLayout(Context context, AttributeSet attrs) {
177        this(context, attrs, 0);
178    }
179
180    public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
181        // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10
182        super(context, attrs);
183
184        ThemeUtils.checkAppCompatTheme(context);
185
186        setOrientation(VERTICAL);
187        setWillNotDraw(false);
188        setAddStatesFromChildren(true);
189
190        mInputFrame = new FrameLayout(context);
191        mInputFrame.setAddStatesFromChildren(true);
192        addView(mInputFrame);
193
194        mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
195        mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
196        mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
197
198        mHintExpanded = mCollapsingTextHelper.getExpansionFraction() == 1f;
199
200        final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
201                R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
202        mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
203        setHint(a.getText(R.styleable.TextInputLayout_android_hint));
204        mHintAnimationEnabled = a.getBoolean(
205                R.styleable.TextInputLayout_hintAnimationEnabled, true);
206
207        if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) {
208            mDefaultTextColor = mFocusedTextColor =
209                    a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint);
210        }
211
212        final int hintAppearance = a.getResourceId(
213                R.styleable.TextInputLayout_hintTextAppearance, -1);
214        if (hintAppearance != -1) {
215            setHintTextAppearance(
216                    a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0));
217        }
218
219        mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
220        final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
221
222        final boolean counterEnabled = a.getBoolean(
223                R.styleable.TextInputLayout_counterEnabled, false);
224        setCounterMaxLength(
225                a.getInt(R.styleable.TextInputLayout_counterMaxLength, INVALID_MAX_LENGTH));
226        mCounterTextAppearance = a.getResourceId(
227                R.styleable.TextInputLayout_counterTextAppearance, 0);
228        mCounterOverflowTextAppearance = a.getResourceId(
229                R.styleable.TextInputLayout_counterOverflowTextAppearance, 0);
230
231        mPasswordToggleEnabled = a.getBoolean(
232                R.styleable.TextInputLayout_passwordToggleEnabled, false);
233        mPasswordToggleDrawable = a.getDrawable(R.styleable.TextInputLayout_passwordToggleDrawable);
234        mPasswordToggleContentDesc = a.getText(
235                R.styleable.TextInputLayout_passwordToggleContentDescription);
236        if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTint)) {
237            mHasPasswordToggleTintList = true;
238            mPasswordToggleTintList = a.getColorStateList(
239                    R.styleable.TextInputLayout_passwordToggleTint);
240        }
241        if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTintMode)) {
242            mHasPasswordToggleTintMode = true;
243            mPasswordToggleTintMode = ViewUtils.parseTintMode(
244                    a.getInt(R.styleable.TextInputLayout_passwordToggleTintMode, -1), null);
245        }
246
247        a.recycle();
248
249        setErrorEnabled(errorEnabled);
250        setCounterEnabled(counterEnabled);
251        applyPasswordToggleTint();
252
253        if (ViewCompat.getImportantForAccessibility(this)
254                == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
255            // Make sure we're important for accessibility if we haven't been explicitly not
256            ViewCompat.setImportantForAccessibility(this,
257                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
258        }
259
260        ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate());
261    }
262
263    @Override
264    public void addView(View child, int index, final ViewGroup.LayoutParams params) {
265        if (child instanceof EditText) {
266            mInputFrame.addView(child, new FrameLayout.LayoutParams(params));
267
268            // Now use the EditText's LayoutParams as our own and update them to make enough space
269            // for the label
270            mInputFrame.setLayoutParams(params);
271            updateInputLayoutMargins();
272
273            setEditText((EditText) child);
274        } else {
275            // Carry on adding the View...
276            super.addView(child, index, params);
277        }
278    }
279
280    /**
281     * Set the typeface to use for both the expanded and floating hint.
282     *
283     * @param typeface typeface to use, or {@code null} to use the default.
284     */
285    public void setTypeface(@Nullable Typeface typeface) {
286        mCollapsingTextHelper.setTypefaces(typeface);
287    }
288
289    /**
290     * Returns the typeface used for both the expanded and floating hint.
291     */
292    @NonNull
293    public Typeface getTypeface() {
294        // This could be either the collapsed or expanded
295        return mCollapsingTextHelper.getCollapsedTypeface();
296    }
297
298    private void setEditText(EditText editText) {
299        // If we already have an EditText, throw an exception
300        if (mEditText != null) {
301            throw new IllegalArgumentException("We already have an EditText, can only have one");
302        }
303
304        if (!(editText instanceof TextInputEditText)) {
305            Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that"
306                    + " class instead.");
307        }
308
309        mEditText = editText;
310
311        final boolean hasPasswordTransformation = hasPasswordTransformation();
312
313        // Use the EditText's typeface, and it's text size for our expanded text
314        if (!hasPasswordTransformation) {
315            // We don't want a monospace font just because we have a password field
316            mCollapsingTextHelper.setTypefaces(mEditText.getTypeface());
317        }
318        mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize());
319
320        final int editTextGravity = mEditText.getGravity();
321        mCollapsingTextHelper.setCollapsedTextGravity(
322                Gravity.TOP | (editTextGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK));
323        mCollapsingTextHelper.setExpandedTextGravity(editTextGravity);
324
325        // Add a TextWatcher so that we know when the text input has changed
326        mEditText.addTextChangedListener(new TextWatcher() {
327            @Override
328            public void afterTextChanged(Editable s) {
329                updateLabelState(true);
330                if (mCounterEnabled) {
331                    updateCounter(s.length());
332                }
333            }
334
335            @Override
336            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
337
338            @Override
339            public void onTextChanged(CharSequence s, int start, int before, int count) {}
340        });
341
342        // Use the EditText's hint colors if we don't have one set
343        if (mDefaultTextColor == null) {
344            mDefaultTextColor = mEditText.getHintTextColors();
345        }
346
347        // If we do not have a valid hint, try and retrieve it from the EditText, if enabled
348        if (mHintEnabled && TextUtils.isEmpty(mHint)) {
349            setHint(mEditText.getHint());
350            // Clear the EditText's hint as we will display it ourselves
351            mEditText.setHint(null);
352        }
353
354        if (mCounterView != null) {
355            updateCounter(mEditText.getText().length());
356        }
357
358        if (mIndicatorArea != null) {
359            adjustIndicatorPadding();
360        }
361
362        updatePasswordToggleView();
363
364        // Update the label visibility with no animation
365        updateLabelState(false);
366    }
367
368    private void updateInputLayoutMargins() {
369        // Create/update the LayoutParams so that we can add enough top margin
370        // to the EditText so make room for the label
371        final LayoutParams lp = (LayoutParams) mInputFrame.getLayoutParams();
372        final int newTopMargin;
373
374        if (mHintEnabled) {
375            if (mTmpPaint == null) {
376                mTmpPaint = new Paint();
377            }
378            mTmpPaint.setTypeface(mCollapsingTextHelper.getCollapsedTypeface());
379            mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize());
380            newTopMargin = (int) -mTmpPaint.ascent();
381        } else {
382            newTopMargin = 0;
383        }
384
385        if (newTopMargin != lp.topMargin) {
386            lp.topMargin = newTopMargin;
387            mInputFrame.requestLayout();
388        }
389    }
390
391    void updateLabelState(boolean animate) {
392        final boolean isEnabled = isEnabled();
393        final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
394        final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
395        final boolean isErrorShowing = !TextUtils.isEmpty(getError());
396
397        if (mDefaultTextColor != null) {
398            mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor);
399        }
400
401        if (isEnabled && mCounterOverflowed && mCounterView != null) {
402            mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors());
403        } else if (isEnabled && isFocused && mFocusedTextColor != null) {
404            mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor);
405        } else if (mDefaultTextColor != null) {
406            mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor);
407        }
408
409        if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
410            // We should be showing the label so do so if it isn't already
411            collapseHint(animate);
412        } else {
413            // We should not be showing the label so hide it
414            expandHint(animate);
415        }
416    }
417
418    /**
419     * Returns the {@link android.widget.EditText} used for text input.
420     */
421    @Nullable
422    public EditText getEditText() {
423        return mEditText;
424    }
425
426    /**
427     * Set the hint to be displayed in the floating label, if enabled.
428     *
429     * @see #setHintEnabled(boolean)
430     *
431     * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
432     */
433    public void setHint(@Nullable CharSequence hint) {
434        if (mHintEnabled) {
435            setHintInternal(hint);
436            sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
437        }
438    }
439
440    private void setHintInternal(CharSequence hint) {
441        mHint = hint;
442        mCollapsingTextHelper.setText(hint);
443    }
444
445    /**
446     * Returns the hint which is displayed in the floating label, if enabled.
447     *
448     * @return the hint, or null if there isn't one set, or the hint is not enabled.
449     *
450     * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
451     */
452    @Nullable
453    public CharSequence getHint() {
454        return mHintEnabled ? mHint : null;
455    }
456
457    /**
458     * Sets whether the floating label functionality is enabled or not in this layout.
459     *
460     * <p>If enabled, any non-empty hint in the child EditText will be moved into the floating
461     * hint, and its existing hint will be cleared. If disabled, then any non-empty floating hint
462     * in this layout will be moved into the EditText, and this layout's hint will be cleared.</p>
463     *
464     * @see #setHint(CharSequence)
465     * @see #isHintEnabled()
466     *
467     * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
468     */
469    public void setHintEnabled(boolean enabled) {
470        if (enabled != mHintEnabled) {
471            mHintEnabled = enabled;
472
473            final CharSequence editTextHint = mEditText.getHint();
474            if (!mHintEnabled) {
475                if (!TextUtils.isEmpty(mHint) && TextUtils.isEmpty(editTextHint)) {
476                    // If the hint is disabled, but we have a hint set, and the EditText doesn't,
477                    // pass it through...
478                    mEditText.setHint(mHint);
479                }
480                // Now clear out any set hint
481                setHintInternal(null);
482            } else {
483                if (!TextUtils.isEmpty(editTextHint)) {
484                    // If the hint is now enabled and the EditText has one set, we'll use it if
485                    // we don't already have one, and clear the EditText's
486                    if (TextUtils.isEmpty(mHint)) {
487                        setHint(editTextHint);
488                    }
489                    mEditText.setHint(null);
490                }
491            }
492
493            // Now update the EditText top margin
494            if (mEditText != null) {
495                updateInputLayoutMargins();
496            }
497        }
498    }
499
500    /**
501     * Returns whether the floating label functionality is enabled or not in this layout.
502     *
503     * @see #setHintEnabled(boolean)
504     *
505     * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
506     */
507    public boolean isHintEnabled() {
508        return mHintEnabled;
509    }
510
511    /**
512     * Sets the hint text color, size, style from the specified TextAppearance resource.
513     *
514     * @attr ref android.support.design.R.styleable#TextInputLayout_hintTextAppearance
515     */
516    public void setHintTextAppearance(@StyleRes int resId) {
517        mCollapsingTextHelper.setCollapsedTextAppearance(resId);
518        mFocusedTextColor = mCollapsingTextHelper.getCollapsedTextColor();
519
520        if (mEditText != null) {
521            updateLabelState(false);
522            // Text size might have changed so update the top margin
523            updateInputLayoutMargins();
524        }
525    }
526
527    private void addIndicator(TextView indicator, int index) {
528        if (mIndicatorArea == null) {
529            mIndicatorArea = new LinearLayout(getContext());
530            mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL);
531            addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT,
532                    LinearLayout.LayoutParams.WRAP_CONTENT);
533
534            // Add a flexible spacer in the middle so that the left/right views stay pinned
535            final Space spacer = new Space(getContext());
536            final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f);
537            mIndicatorArea.addView(spacer, spacerLp);
538
539            if (mEditText != null) {
540                adjustIndicatorPadding();
541            }
542        }
543        mIndicatorArea.setVisibility(View.VISIBLE);
544        mIndicatorArea.addView(indicator, index);
545        mIndicatorsAdded++;
546    }
547
548    private void adjustIndicatorPadding() {
549        // Add padding to the error and character counter so that they match the EditText
550        ViewCompat.setPaddingRelative(mIndicatorArea, ViewCompat.getPaddingStart(mEditText),
551                0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
552    }
553
554    private void removeIndicator(TextView indicator) {
555        if (mIndicatorArea != null) {
556            mIndicatorArea.removeView(indicator);
557            if (--mIndicatorsAdded == 0) {
558                mIndicatorArea.setVisibility(View.GONE);
559            }
560        }
561    }
562
563    /**
564     * Whether the error functionality is enabled or not in this layout. Enabling this
565     * functionality before setting an error message via {@link #setError(CharSequence)}, will mean
566     * that this layout will not change size when an error is displayed.
567     *
568     * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
569     */
570    public void setErrorEnabled(boolean enabled) {
571        if (mErrorEnabled != enabled) {
572            if (mErrorView != null) {
573                ViewCompat.animate(mErrorView).cancel();
574            }
575
576            if (enabled) {
577                mErrorView = new TextView(getContext());
578                boolean useDefaultColor = false;
579                try {
580                    TextViewCompat.setTextAppearance(mErrorView, mErrorTextAppearance);
581
582                    if (Build.VERSION.SDK_INT >= 23
583                            && mErrorView.getTextColors().getDefaultColor() == Color.MAGENTA) {
584                        // Caused by our theme not extending from Theme.Design*. On API 23 and
585                        // above, unresolved theme attrs result in MAGENTA rather than an exception.
586                        // Flag so that we use a decent default
587                        useDefaultColor = true;
588                    }
589                } catch (Exception e) {
590                    // Caused by our theme not extending from Theme.Design*. Flag so that we use
591                    // a decent default
592                    useDefaultColor = true;
593                }
594                if (useDefaultColor) {
595                    // Probably caused by our theme not extending from Theme.Design*. Instead
596                    // we manually set something appropriate
597                    TextViewCompat.setTextAppearance(mErrorView,
598                            android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
599                    mErrorView.setTextColor(ContextCompat.getColor(
600                            getContext(), R.color.design_textinput_error_color_light));
601                }
602                mErrorView.setVisibility(INVISIBLE);
603                ViewCompat.setAccessibilityLiveRegion(mErrorView,
604                        ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
605                addIndicator(mErrorView, 0);
606            } else {
607                mErrorShown = false;
608                updateEditTextBackground();
609                removeIndicator(mErrorView);
610                mErrorView = null;
611            }
612            mErrorEnabled = enabled;
613        }
614    }
615
616    /**
617     * Returns whether the error functionality is enabled or not in this layout.
618     *
619     * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
620     *
621     * @see #setErrorEnabled(boolean)
622     */
623    public boolean isErrorEnabled() {
624        return mErrorEnabled;
625    }
626
627    /**
628     * Sets an error message that will be displayed below our {@link EditText}. If the
629     * {@code error} is {@code null}, the error message will be cleared.
630     * <p>
631     * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
632     * it will be automatically enabled if {@code error} is not empty.
633     *
634     * @param error Error message to display, or null to clear
635     *
636     * @see #getError()
637     */
638    public void setError(@Nullable final CharSequence error) {
639        // Only animate if we're enabled, laid out, and we have a different error message
640        setError(error, ViewCompat.isLaidOut(this) && isEnabled()
641                && (mErrorView == null || !TextUtils.equals(mErrorView.getText(), error)));
642    }
643
644    private void setError(@Nullable final CharSequence error, final boolean animate) {
645        mError = error;
646
647        if (!mErrorEnabled) {
648            if (TextUtils.isEmpty(error)) {
649                // If error isn't enabled, and the error is empty, just return
650                return;
651            }
652            // Else, we'll assume that they want to enable the error functionality
653            setErrorEnabled(true);
654        }
655
656        mErrorShown = !TextUtils.isEmpty(error);
657
658        // Cancel any on-going animation
659        ViewCompat.animate(mErrorView).cancel();
660
661        if (mErrorShown) {
662            mErrorView.setText(error);
663            mErrorView.setVisibility(VISIBLE);
664
665            if (animate) {
666                if (ViewCompat.getAlpha(mErrorView) == 1f) {
667                    // If it's currently 100% show, we'll animate it from 0
668                    ViewCompat.setAlpha(mErrorView, 0f);
669                }
670                ViewCompat.animate(mErrorView)
671                        .alpha(1f)
672                        .setDuration(ANIMATION_DURATION)
673                        .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
674                        .setListener(new ViewPropertyAnimatorListenerAdapter() {
675                            @Override
676                            public void onAnimationStart(View view) {
677                                view.setVisibility(VISIBLE);
678                            }
679                        }).start();
680            } else {
681                // Set alpha to 1f, just in case
682                ViewCompat.setAlpha(mErrorView, 1f);
683            }
684        } else {
685            if (mErrorView.getVisibility() == VISIBLE) {
686                if (animate) {
687                    ViewCompat.animate(mErrorView)
688                            .alpha(0f)
689                            .setDuration(ANIMATION_DURATION)
690                            .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
691                            .setListener(new ViewPropertyAnimatorListenerAdapter() {
692                                @Override
693                                public void onAnimationEnd(View view) {
694                                    mErrorView.setText(error);
695                                    view.setVisibility(INVISIBLE);
696                                }
697                            }).start();
698                } else {
699                    mErrorView.setText(error);
700                    mErrorView.setVisibility(INVISIBLE);
701                }
702            }
703        }
704
705        updateEditTextBackground();
706        updateLabelState(animate);
707    }
708
709    /**
710     * Whether the character counter functionality is enabled or not in this layout.
711     *
712     * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
713     */
714    public void setCounterEnabled(boolean enabled) {
715        if (mCounterEnabled != enabled) {
716            if (enabled) {
717                mCounterView = new TextView(getContext());
718                mCounterView.setMaxLines(1);
719                try {
720                    TextViewCompat.setTextAppearance(mCounterView, mCounterTextAppearance);
721                } catch (Exception e) {
722                    // Probably caused by our theme not extending from Theme.Design*. Instead
723                    // we manually set something appropriate
724                    TextViewCompat.setTextAppearance(mCounterView,
725                            android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
726                    mCounterView.setTextColor(ContextCompat.getColor(
727                            getContext(), R.color.design_textinput_error_color_light));
728                }
729                addIndicator(mCounterView, -1);
730                if (mEditText == null) {
731                    updateCounter(0);
732                } else {
733                    updateCounter(mEditText.getText().length());
734                }
735            } else {
736                removeIndicator(mCounterView);
737                mCounterView = null;
738            }
739            mCounterEnabled = enabled;
740        }
741    }
742
743    /**
744     * Returns whether the character counter functionality is enabled or not in this layout.
745     *
746     * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
747     *
748     * @see #setCounterEnabled(boolean)
749     */
750    public boolean isCounterEnabled() {
751        return mCounterEnabled;
752    }
753
754    /**
755     * Sets the max length to display at the character counter.
756     *
757     * @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown.
758     *
759     * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
760     */
761    public void setCounterMaxLength(int maxLength) {
762        if (mCounterMaxLength != maxLength) {
763            if (maxLength > 0) {
764                mCounterMaxLength = maxLength;
765            } else {
766                mCounterMaxLength = INVALID_MAX_LENGTH;
767            }
768            if (mCounterEnabled) {
769                updateCounter(mEditText == null ? 0 : mEditText.getText().length());
770            }
771        }
772    }
773
774    @Override
775    public void setEnabled(boolean enabled) {
776        // Since we're set to addStatesFromChildren, we need to make sure that we set all
777        // children to enabled/disabled otherwise any enabled children will wipe out our disabled
778        // drawable state
779        recursiveSetEnabled(this, enabled);
780        super.setEnabled(enabled);
781    }
782
783    private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) {
784        for (int i = 0, count = vg.getChildCount(); i < count; i++) {
785            final View child = vg.getChildAt(i);
786            child.setEnabled(enabled);
787            if (child instanceof ViewGroup) {
788                recursiveSetEnabled((ViewGroup) child, enabled);
789            }
790        }
791    }
792
793    /**
794     * Returns the max length shown at the character counter.
795     *
796     * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
797     */
798    public int getCounterMaxLength() {
799        return mCounterMaxLength;
800    }
801
802    void updateCounter(int length) {
803        boolean wasCounterOverflowed = mCounterOverflowed;
804        if (mCounterMaxLength == INVALID_MAX_LENGTH) {
805            mCounterView.setText(String.valueOf(length));
806            mCounterOverflowed = false;
807        } else {
808            mCounterOverflowed = length > mCounterMaxLength;
809            if (wasCounterOverflowed != mCounterOverflowed) {
810                TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed ?
811                        mCounterOverflowTextAppearance : mCounterTextAppearance);
812            }
813            mCounterView.setText(getContext().getString(R.string.character_counter_pattern,
814                    length, mCounterMaxLength));
815        }
816        if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) {
817            updateLabelState(false);
818            updateEditTextBackground();
819        }
820    }
821
822    private void updateEditTextBackground() {
823        if (mEditText == null) {
824            return;
825        }
826
827        Drawable editTextBackground = mEditText.getBackground();
828        if (editTextBackground == null) {
829            return;
830        }
831
832        ensureBackgroundDrawableStateWorkaround();
833
834        if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
835            editTextBackground = editTextBackground.mutate();
836        }
837
838        if (mErrorShown && mErrorView != null) {
839            // Set a color filter of the error color
840            editTextBackground.setColorFilter(
841                    AppCompatDrawableManager.getPorterDuffColorFilter(
842                            mErrorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
843        } else if (mCounterOverflowed && mCounterView != null) {
844            // Set a color filter of the counter color
845            editTextBackground.setColorFilter(
846                    AppCompatDrawableManager.getPorterDuffColorFilter(
847                            mCounterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
848        } else {
849            // Else reset the color filter and refresh the drawable state so that the
850            // normal tint is used
851            DrawableCompat.clearColorFilter(editTextBackground);
852            mEditText.refreshDrawableState();
853        }
854    }
855
856    private void ensureBackgroundDrawableStateWorkaround() {
857        final int sdk = Build.VERSION.SDK_INT;
858        if (sdk != 21 && sdk != 22) {
859            // The workaround is only required on API 21-22
860            return;
861        }
862        final Drawable bg = mEditText.getBackground();
863        if (bg == null) {
864            return;
865        }
866
867        if (!mHasReconstructedEditTextBackground) {
868            // This is gross. There is an issue in the platform which affects container Drawables
869            // where the first drawable retrieved from resources will propagate any changes
870            // (like color filter) to all instances from the cache. We'll try to workaround it...
871
872            final Drawable newBg = bg.getConstantState().newDrawable();
873
874            if (bg instanceof DrawableContainer) {
875                // If we have a Drawable container, we can try and set it's constant state via
876                // reflection from the new Drawable
877                mHasReconstructedEditTextBackground =
878                        DrawableUtils.setContainerConstantState(
879                                (DrawableContainer) bg, newBg.getConstantState());
880            }
881
882            if (!mHasReconstructedEditTextBackground) {
883                // If we reach here then we just need to set a brand new instance of the Drawable
884                // as the background. This has the unfortunate side-effect of wiping out any
885                // user set padding, but I'd hope that use of custom padding on an EditText
886                // is limited.
887                ViewCompat.setBackground(mEditText, newBg);
888                mHasReconstructedEditTextBackground = true;
889            }
890        }
891    }
892
893    static class SavedState extends AbsSavedState {
894        CharSequence error;
895
896        SavedState(Parcelable superState) {
897            super(superState);
898        }
899
900        public SavedState(Parcel source, ClassLoader loader) {
901            super(source, loader);
902            error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
903
904        }
905
906        @Override
907        public void writeToParcel(Parcel dest, int flags) {
908            super.writeToParcel(dest, flags);
909            TextUtils.writeToParcel(error, dest, flags);
910        }
911
912        @Override
913        public String toString() {
914            return "TextInputLayout.SavedState{"
915                    + Integer.toHexString(System.identityHashCode(this))
916                    + " error=" + error + "}";
917        }
918
919        public static final Creator<SavedState> CREATOR = ParcelableCompat.newCreator(
920                new ParcelableCompatCreatorCallbacks<SavedState>() {
921                    @Override
922                    public SavedState createFromParcel(Parcel in, ClassLoader loader) {
923                        return new SavedState(in, loader);
924                    }
925
926                    @Override
927                    public SavedState[] newArray(int size) {
928                        return new SavedState[size];
929                    }
930                });
931    }
932
933    @Override
934    public Parcelable onSaveInstanceState() {
935        Parcelable superState = super.onSaveInstanceState();
936        SavedState ss = new SavedState(superState);
937        if (mErrorShown) {
938            ss.error = getError();
939        }
940        return ss;
941    }
942
943    @Override
944    protected void onRestoreInstanceState(Parcelable state) {
945        if (!(state instanceof SavedState)) {
946            super.onRestoreInstanceState(state);
947            return;
948        }
949        SavedState ss = (SavedState) state;
950        super.onRestoreInstanceState(ss.getSuperState());
951        setError(ss.error);
952        requestLayout();
953    }
954
955    /**
956     * Returns the error message that was set to be displayed with
957     * {@link #setError(CharSequence)}, or <code>null</code> if no error was set
958     * or if error displaying is not enabled.
959     *
960     * @see #setError(CharSequence)
961     */
962    @Nullable
963    public CharSequence getError() {
964        return mErrorEnabled ? mError : null;
965    }
966
967    /**
968     * Returns whether any hint state changes, due to being focused or non-empty text, are
969     * animated.
970     *
971     * @see #setHintAnimationEnabled(boolean)
972     *
973     * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
974     */
975    public boolean isHintAnimationEnabled() {
976        return mHintAnimationEnabled;
977    }
978
979    /**
980     * Set whether any hint state changes, due to being focused or non-empty text, are
981     * animated.
982     *
983     * @see #isHintAnimationEnabled()
984     *
985     * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
986     */
987    public void setHintAnimationEnabled(boolean enabled) {
988        mHintAnimationEnabled = enabled;
989    }
990
991    @Override
992    public void draw(Canvas canvas) {
993        super.draw(canvas);
994
995        if (mHintEnabled) {
996            mCollapsingTextHelper.draw(canvas);
997        }
998    }
999
1000    @Override
1001    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1002        updatePasswordToggleView();
1003        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1004    }
1005
1006    private void updatePasswordToggleView() {
1007        if (mEditText == null) {
1008            // If there is no EditText, there is nothing to update
1009            return;
1010        }
1011
1012        if (shouldShowPasswordIcon()) {
1013            if (mPasswordToggleView == null) {
1014                mPasswordToggleView = (CheckableImageButton) LayoutInflater.from(getContext())
1015                        .inflate(R.layout.design_text_input_password_icon, mInputFrame, false);
1016                mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
1017                mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc);
1018                mInputFrame.addView(mPasswordToggleView);
1019
1020                mPasswordToggleView.setOnClickListener(new View.OnClickListener() {
1021                    @Override
1022                    public void onClick(View view) {
1023                        passwordVisibilityToggleRequested();
1024                    }
1025                });
1026            }
1027
1028            mPasswordToggleView.setVisibility(VISIBLE);
1029
1030            // We need to add a dummy drawable as the end compound drawable so that the text is
1031            // indented and doesn't display below the toggle view
1032            if (mPasswordToggleDummyDrawable == null) {
1033                mPasswordToggleDummyDrawable = new ColorDrawable();
1034            }
1035            mPasswordToggleDummyDrawable.setBounds(0, 0, mPasswordToggleView.getMeasuredWidth(), 1);
1036
1037            final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
1038            // Store the user defined end compound drawable so that we can restore it later
1039            if (compounds[2] != mPasswordToggleDummyDrawable) {
1040                mOriginalEditTextEndDrawable = compounds[2];
1041            }
1042            TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], compounds[1],
1043                    mPasswordToggleDummyDrawable, compounds[3]);
1044
1045            // Copy over the EditText's padding so that we match
1046            mPasswordToggleView.setPadding(mEditText.getPaddingLeft(),
1047                    mEditText.getPaddingTop(), mEditText.getPaddingRight(),
1048                    mEditText.getPaddingBottom());
1049        } else {
1050            if (mPasswordToggleView != null && mPasswordToggleView.getVisibility() == VISIBLE) {
1051                mPasswordToggleView.setVisibility(View.GONE);
1052            }
1053
1054            if (mPasswordToggleDummyDrawable != null) {
1055                // Make sure that we remove the dummy end compound drawable if it exists, and then
1056                // clear it
1057                final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
1058                if (compounds[2] == mPasswordToggleDummyDrawable) {
1059                    TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0],
1060                            compounds[1], mOriginalEditTextEndDrawable, compounds[3]);
1061                    mPasswordToggleDummyDrawable = null;
1062                }
1063            }
1064        }
1065    }
1066
1067    /**
1068     * Set the icon to use for the password visibility toggle button.
1069     *
1070     * <p>If you use an icon you should also set a description for its action
1071     * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
1072     * This is used for accessibility.</p>
1073     *
1074     * @param resId resource id of the drawable to set, or 0 to clear the icon
1075     *
1076     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
1077     */
1078    public void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) {
1079        setPasswordVisibilityToggleDrawable(resId != 0
1080                ? AppCompatResources.getDrawable(getContext(), resId)
1081                : null);
1082    }
1083
1084    /**
1085     * Set the icon to use for the password visibility toggle button.
1086     *
1087     * <p>If you use an icon you should also set a description for its action
1088     * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
1089     * This is used for accessibility.</p>
1090     *
1091     * @param icon Drawable to set, may be null to clear the icon
1092     *
1093     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
1094     */
1095    public void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) {
1096        mPasswordToggleDrawable = icon;
1097        if (mPasswordToggleView != null) {
1098            mPasswordToggleView.setImageDrawable(icon);
1099        }
1100    }
1101
1102    /**
1103     * Set a content description for the navigation button if one is present.
1104     *
1105     * <p>The content description will be read via screen readers or other accessibility
1106     * systems to explain the action of the password visibility toggle.</p>
1107     *
1108     * @param resId Resource ID of a content description string to set,
1109     *              or 0 to clear the description
1110     *
1111     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription
1112     */
1113    public void setPasswordVisibilityToggleContentDescription(@StringRes int resId) {
1114        setPasswordVisibilityToggleContentDescription(
1115                resId != 0 ? getResources().getText(resId) : null);
1116    }
1117
1118    /**
1119     * Set a content description for the navigation button if one is present.
1120     *
1121     * <p>The content description will be read via screen readers or other accessibility
1122     * systems to explain the action of the password visibility toggle.</p>
1123     *
1124     * @param description Content description to set, or null to clear the content description
1125     *
1126     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription
1127     */
1128    public void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) {
1129        mPasswordToggleContentDesc = description;
1130        if (mPasswordToggleView != null) {
1131            mPasswordToggleView.setContentDescription(description);
1132        }
1133    }
1134
1135    /**
1136     * Returns the icon currently used for the password visibility toggle button.
1137     *
1138     * @see #setPasswordVisibilityToggleDrawable(Drawable)
1139     *
1140     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
1141     */
1142    @Nullable
1143    public Drawable getPasswordVisibilityToggleDrawable() {
1144        return mPasswordToggleDrawable;
1145    }
1146
1147    /**
1148     * Returns the currently configured content description for the password visibility
1149     * toggle button.
1150     *
1151     * <p>This will be used to describe the navigation action to users through mechanisms
1152     * such as screen readers.</p>
1153     */
1154    @Nullable
1155    public CharSequence getPasswordVisibilityToggleContentDescription() {
1156        return mPasswordToggleContentDesc;
1157    }
1158
1159    /**
1160     * Returns whether the password visibility toggle functionality is currently enabled.
1161     *
1162     * @see #setPasswordVisibilityToggleEnabled(boolean)
1163     */
1164    public boolean isPasswordVisibilityToggleEnabled() {
1165        return mPasswordToggleEnabled;
1166    }
1167
1168    /**
1169     * Returns whether the password visibility toggle functionality is enabled or not.
1170     *
1171     * <p>When enabled, a button is placed at the end of the EditText which enables the user
1172     * to switch between the field's input being visibly disguised or not.</p>
1173     *
1174     * @param enabled true to enable the functionality
1175     *
1176     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleEnabled
1177     */
1178    public void setPasswordVisibilityToggleEnabled(final boolean enabled) {
1179        if (mPasswordToggleEnabled != enabled) {
1180            mPasswordToggleEnabled = enabled;
1181
1182            if (!enabled && mPasswordToggledVisible && mEditText != null) {
1183                // If the toggle is no longer enabled, but we remove the PasswordTransformation
1184                // to make the password visible, add it back
1185                mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
1186            }
1187
1188            // Reset the visibility tracking flag
1189            mPasswordToggledVisible = false;
1190
1191            updatePasswordToggleView();
1192        }
1193    }
1194
1195    /**
1196     * Applies a tint to the the password visibility toggle drawable. Does not modify the current
1197     * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
1198     *
1199     * <p>Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will
1200     * automatically mutate the drawable and apply the specified tint and tint mode using
1201     * {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.</p>
1202     *
1203     * @param tintList the tint to apply, may be null to clear tint
1204     *
1205     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTint
1206     */
1207    public void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) {
1208        mPasswordToggleTintList = tintList;
1209        mHasPasswordToggleTintList = true;
1210        applyPasswordToggleTint();
1211    }
1212
1213    /**
1214     * Specifies the blending mode used to apply the tint specified by
1215     * {@link #setPasswordVisibilityToggleTintList(ColorStateList)} to the password
1216     * visibility toggle drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.</p>
1217     *
1218     * @param mode the blending mode used to apply the tint, may be null to clear tint
1219     *
1220     * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTintMode
1221     */
1222    public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) {
1223        mPasswordToggleTintMode = mode;
1224        mHasPasswordToggleTintMode = true;
1225        applyPasswordToggleTint();
1226    }
1227
1228   void passwordVisibilityToggleRequested() {
1229       if (mPasswordToggleEnabled) {
1230           // Store the current cursor position
1231           final int selection = mEditText.getSelectionEnd();
1232
1233           if (hasPasswordTransformation()) {
1234               mEditText.setTransformationMethod(null);
1235               mPasswordToggledVisible = true;
1236           } else {
1237               mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
1238               mPasswordToggledVisible = false;
1239           }
1240
1241           mPasswordToggleView.setChecked(mPasswordToggledVisible);
1242
1243           // And restore the cursor position
1244           mEditText.setSelection(selection);
1245       }
1246    }
1247
1248    private boolean hasPasswordTransformation() {
1249        return mEditText != null
1250                && mEditText.getTransformationMethod() instanceof PasswordTransformationMethod;
1251    }
1252
1253    private boolean shouldShowPasswordIcon() {
1254        return mPasswordToggleEnabled && (hasPasswordTransformation() || mPasswordToggledVisible);
1255    }
1256
1257    private void applyPasswordToggleTint() {
1258        if (mPasswordToggleDrawable != null
1259                && (mHasPasswordToggleTintList || mHasPasswordToggleTintMode)) {
1260            mPasswordToggleDrawable = DrawableCompat.wrap(mPasswordToggleDrawable).mutate();
1261
1262            if (mHasPasswordToggleTintList) {
1263                DrawableCompat.setTintList(mPasswordToggleDrawable, mPasswordToggleTintList);
1264            }
1265            if (mHasPasswordToggleTintMode) {
1266                DrawableCompat.setTintMode(mPasswordToggleDrawable, mPasswordToggleTintMode);
1267            }
1268
1269            if (mPasswordToggleView != null
1270                    && mPasswordToggleView.getDrawable() != mPasswordToggleDrawable) {
1271                mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
1272            }
1273        }
1274    }
1275
1276    @Override
1277    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1278        super.onLayout(changed, left, top, right, bottom);
1279
1280        if (mHintEnabled && mEditText != null) {
1281            final Rect rect = mTmpRect;
1282            ViewGroupUtils.getDescendantRect(this, mEditText, rect);
1283
1284            final int l = rect.left + mEditText.getCompoundPaddingLeft();
1285            final int r = rect.right - mEditText.getCompoundPaddingRight();
1286
1287            mCollapsingTextHelper.setExpandedBounds(
1288                    l, rect.top + mEditText.getCompoundPaddingTop(),
1289                    r, rect.bottom - mEditText.getCompoundPaddingBottom());
1290
1291            // Set the collapsed bounds to be the the full height (minus padding) to match the
1292            // EditText's editable area
1293            mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(),
1294                    r, bottom - top - getPaddingBottom());
1295
1296            mCollapsingTextHelper.recalculate();
1297        }
1298    }
1299
1300    private void collapseHint(boolean animate) {
1301        if (mAnimator != null && mAnimator.isRunning()) {
1302            mAnimator.cancel();
1303        }
1304        if (animate && mHintAnimationEnabled) {
1305            animateToExpansionFraction(1f);
1306        } else {
1307            mCollapsingTextHelper.setExpansionFraction(1f);
1308        }
1309        mHintExpanded = false;
1310    }
1311
1312    @Override
1313    protected void drawableStateChanged() {
1314        if (mInDrawableStateChanged) {
1315            // Some of the calls below will update the drawable state of child views. Since we're
1316            // using addStatesFromChildren we can get into infinite recursion, hence we'll just
1317            // exit in this instance
1318            return;
1319        }
1320
1321        mInDrawableStateChanged = true;
1322
1323        super.drawableStateChanged();
1324
1325        final int[] state = getDrawableState();
1326        boolean changed = false;
1327
1328        // Drawable state has changed so see if we need to update the label
1329        updateLabelState(ViewCompat.isLaidOut(this) && isEnabled());
1330
1331        updateEditTextBackground();
1332
1333        if (mCollapsingTextHelper != null) {
1334            changed |= mCollapsingTextHelper.setState(state);
1335        }
1336
1337        if (changed) {
1338            invalidate();
1339        }
1340
1341        mInDrawableStateChanged = false;
1342    }
1343
1344    private void expandHint(boolean animate) {
1345        if (mAnimator != null && mAnimator.isRunning()) {
1346            mAnimator.cancel();
1347        }
1348        if (animate && mHintAnimationEnabled) {
1349            animateToExpansionFraction(0f);
1350        } else {
1351            mCollapsingTextHelper.setExpansionFraction(0f);
1352        }
1353        mHintExpanded = true;
1354    }
1355
1356    private void animateToExpansionFraction(final float target) {
1357        if (mCollapsingTextHelper.getExpansionFraction() == target) {
1358            return;
1359        }
1360        if (mAnimator == null) {
1361            mAnimator = ViewUtils.createAnimator();
1362            mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
1363            mAnimator.setDuration(ANIMATION_DURATION);
1364            mAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
1365                @Override
1366                public void onAnimationUpdate(ValueAnimatorCompat animator) {
1367                    mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue());
1368                }
1369            });
1370        }
1371        mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
1372        mAnimator.start();
1373    }
1374
1375    @VisibleForTesting
1376    final boolean isHintExpanded() {
1377        return mHintExpanded;
1378    }
1379
1380    private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat {
1381        TextInputAccessibilityDelegate() {
1382        }
1383
1384        @Override
1385        public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
1386            super.onInitializeAccessibilityEvent(host, event);
1387            event.setClassName(TextInputLayout.class.getSimpleName());
1388        }
1389
1390        @Override
1391        public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
1392            super.onPopulateAccessibilityEvent(host, event);
1393
1394            final CharSequence text = mCollapsingTextHelper.getText();
1395            if (!TextUtils.isEmpty(text)) {
1396                event.getText().add(text);
1397            }
1398        }
1399
1400        @Override
1401        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
1402            super.onInitializeAccessibilityNodeInfo(host, info);
1403            info.setClassName(TextInputLayout.class.getSimpleName());
1404
1405            final CharSequence text = mCollapsingTextHelper.getText();
1406            if (!TextUtils.isEmpty(text)) {
1407                info.setText(text);
1408            }
1409            if (mEditText != null) {
1410                info.setLabelFor(mEditText);
1411            }
1412            final CharSequence error = mErrorView != null ? mErrorView.getText() : null;
1413            if (!TextUtils.isEmpty(error)) {
1414                info.setContentInvalid(true);
1415                info.setError(error);
1416            }
1417        }
1418    }
1419
1420    private static boolean arrayContains(int[] array, int value) {
1421        for (int v : array) {
1422            if (v == value) {
1423                return true;
1424            }
1425        }
1426        return false;
1427    }
1428}