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