Home | History | Annotate | Download | only in widget
      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 
     17 package android.support.design.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.ColorStateList;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Canvas;
     23 import android.graphics.Paint;
     24 import android.graphics.PorterDuff;
     25 import android.graphics.Typeface;
     26 import android.graphics.drawable.Drawable;
     27 import android.graphics.drawable.DrawableContainer;
     28 import android.graphics.drawable.InsetDrawable;
     29 import android.os.Build;
     30 import android.os.Parcel;
     31 import android.os.Parcelable;
     32 import android.support.annotation.NonNull;
     33 import android.support.annotation.Nullable;
     34 import android.support.annotation.StyleRes;
     35 import android.support.design.R;
     36 import android.support.v4.content.ContextCompat;
     37 import android.support.v4.graphics.drawable.DrawableWrapper;
     38 import android.support.v4.os.ParcelableCompat;
     39 import android.support.v4.os.ParcelableCompatCreatorCallbacks;
     40 import android.support.v4.view.AbsSavedState;
     41 import android.support.v4.view.AccessibilityDelegateCompat;
     42 import android.support.v4.view.GravityCompat;
     43 import android.support.v4.view.ViewCompat;
     44 import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
     45 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     46 import android.support.v4.widget.Space;
     47 import android.support.v7.widget.AppCompatDrawableManager;
     48 import android.text.Editable;
     49 import android.text.TextUtils;
     50 import android.text.TextWatcher;
     51 import android.util.AttributeSet;
     52 import android.util.Log;
     53 import android.view.Gravity;
     54 import android.view.View;
     55 import android.view.ViewGroup;
     56 import android.view.accessibility.AccessibilityEvent;
     57 import android.view.animation.AccelerateInterpolator;
     58 import android.widget.EditText;
     59 import android.widget.LinearLayout;
     60 import android.widget.TextView;
     61 
     62 /**
     63  * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label
     64  * when the hint is hidden due to the user inputting text.
     65  *
     66  * <p>Also supports showing an error via {@link #setErrorEnabled(boolean)} and
     67  * {@link #setError(CharSequence)}, and a character counter via
     68  * {@link #setCounterEnabled(boolean)}.</p>
     69  *
     70  * The {@link TextInputEditText} class is provided to be used as a child of this layout. Using
     71  * TextInputEditText allows TextInputLayout greater control over the visual aspects of any
     72  * text input. An example usage is as so:
     73  *
     74  * <pre>
     75  * &lt;android.support.design.widget.TextInputLayout
     76  *         android:layout_width=&quot;match_parent&quot;
     77  *         android:layout_height=&quot;wrap_content&quot;&gt;
     78  *
     79  *     &lt;android.support.design.widget.TextInputEditText
     80  *             android:layout_width=&quot;match_parent&quot;
     81  *             android:layout_height=&quot;wrap_content&quot;
     82  *             android:hint=&quot;@string/form_username&quot;/&gt;
     83  *
     84  * &lt;/android.support.design.widget.TextInputLayout&gt;
     85  * </pre>
     86  */
     87 public class TextInputLayout extends LinearLayout {
     88 
     89     private static final int ANIMATION_DURATION = 200;
     90     private static final int INVALID_MAX_LENGTH = -1;
     91 
     92     private static final String LOG_TAG = "TextInputLayout";
     93 
     94     private EditText mEditText;
     95 
     96     private boolean mHintEnabled;
     97     private CharSequence mHint;
     98 
     99     private Paint mTmpPaint;
    100 
    101     private LinearLayout mIndicatorArea;
    102     private int mIndicatorsAdded;
    103 
    104     private boolean mErrorEnabled;
    105     private TextView mErrorView;
    106     private int mErrorTextAppearance;
    107     private boolean mErrorShown;
    108     private CharSequence mError;
    109 
    110     private boolean mCounterEnabled;
    111     private TextView mCounterView;
    112     private int mCounterMaxLength;
    113     private int mCounterTextAppearance;
    114     private int mCounterOverflowTextAppearance;
    115     private boolean mCounterOverflowed;
    116 
    117     private ColorStateList mDefaultTextColor;
    118     private ColorStateList mFocusedTextColor;
    119 
    120     private final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this);
    121 
    122     private boolean mHintAnimationEnabled;
    123     private ValueAnimatorCompat mAnimator;
    124 
    125     private boolean mHasReconstructedEditTextBackground;
    126 
    127     public TextInputLayout(Context context) {
    128         this(context, null);
    129     }
    130 
    131     public TextInputLayout(Context context, AttributeSet attrs) {
    132         this(context, attrs, 0);
    133     }
    134 
    135     public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    136         // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10
    137         super(context, attrs);
    138 
    139         ThemeUtils.checkAppCompatTheme(context);
    140 
    141         setOrientation(VERTICAL);
    142         setWillNotDraw(false);
    143         setAddStatesFromChildren(true);
    144 
    145         mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
    146         mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
    147         mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
    148 
    149         final TypedArray a = context.obtainStyledAttributes(attrs,
    150                 R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
    151         mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
    152         setHint(a.getText(R.styleable.TextInputLayout_android_hint));
    153         mHintAnimationEnabled = a.getBoolean(
    154                 R.styleable.TextInputLayout_hintAnimationEnabled, true);
    155 
    156         if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) {
    157             mDefaultTextColor = mFocusedTextColor =
    158                     a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint);
    159         }
    160 
    161         final int hintAppearance = a.getResourceId(
    162                 R.styleable.TextInputLayout_hintTextAppearance, -1);
    163         if (hintAppearance != -1) {
    164             setHintTextAppearance(
    165                     a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0));
    166         }
    167 
    168         mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
    169         final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
    170 
    171         final boolean counterEnabled = a.getBoolean(
    172                 R.styleable.TextInputLayout_counterEnabled, false);
    173         setCounterMaxLength(
    174                 a.getInt(R.styleable.TextInputLayout_counterMaxLength, INVALID_MAX_LENGTH));
    175         mCounterTextAppearance = a.getResourceId(
    176                 R.styleable.TextInputLayout_counterTextAppearance, 0);
    177         mCounterOverflowTextAppearance = a.getResourceId(
    178                 R.styleable.TextInputLayout_counterOverflowTextAppearance, 0);
    179         a.recycle();
    180 
    181         setErrorEnabled(errorEnabled);
    182         setCounterEnabled(counterEnabled);
    183 
    184         if (ViewCompat.getImportantForAccessibility(this)
    185                 == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    186             // Make sure we're important for accessibility if we haven't been explicitly not
    187             ViewCompat.setImportantForAccessibility(this,
    188                     ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    189         }
    190 
    191         ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate());
    192     }
    193 
    194     @Override
    195     public void addView(View child, int index, ViewGroup.LayoutParams params) {
    196         if (child instanceof EditText) {
    197             setEditText((EditText) child);
    198             super.addView(child, 0, updateEditTextMargin(params));
    199         } else {
    200             // Carry on adding the View...
    201             super.addView(child, index, params);
    202         }
    203     }
    204 
    205     /**
    206      * Set the typeface to use for both the expanded and floating hint.
    207      *
    208      * @param typeface typeface to use, or {@code null} to use the default.
    209      */
    210     public void setTypeface(@Nullable Typeface typeface) {
    211         mCollapsingTextHelper.setTypefaces(typeface);
    212     }
    213 
    214     /**
    215      * Returns the typeface used for both the expanded and floating hint.
    216      */
    217     @NonNull
    218     public Typeface getTypeface() {
    219         // This could be either the collapsed or expanded
    220         return mCollapsingTextHelper.getCollapsedTypeface();
    221     }
    222 
    223     private void setEditText(EditText editText) {
    224         // If we already have an EditText, throw an exception
    225         if (mEditText != null) {
    226             throw new IllegalArgumentException("We already have an EditText, can only have one");
    227         }
    228 
    229         if (!(editText instanceof TextInputEditText)) {
    230             Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that"
    231                     + " class instead.");
    232         }
    233 
    234         mEditText = editText;
    235 
    236         // Use the EditText's typeface, and it's text size for our expanded text
    237         mCollapsingTextHelper.setTypefaces(mEditText.getTypeface());
    238         mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize());
    239 
    240         final int editTextGravity = mEditText.getGravity();
    241         mCollapsingTextHelper.setCollapsedTextGravity(
    242                 Gravity.TOP | (editTextGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK));
    243         mCollapsingTextHelper.setExpandedTextGravity(editTextGravity);
    244 
    245         // Add a TextWatcher so that we know when the text input has changed
    246         mEditText.addTextChangedListener(new TextWatcher() {
    247             @Override
    248             public void afterTextChanged(Editable s) {
    249                 updateLabelState(true);
    250                 if (mCounterEnabled) {
    251                     updateCounter(s.length());
    252                 }
    253             }
    254 
    255             @Override
    256             public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
    257 
    258             @Override
    259             public void onTextChanged(CharSequence s, int start, int before, int count) {}
    260         });
    261 
    262         // Use the EditText's hint colors if we don't have one set
    263         if (mDefaultTextColor == null) {
    264             mDefaultTextColor = mEditText.getHintTextColors();
    265         }
    266 
    267         // If we do not have a valid hint, try and retrieve it from the EditText, if enabled
    268         if (mHintEnabled && TextUtils.isEmpty(mHint)) {
    269             setHint(mEditText.getHint());
    270             // Clear the EditText's hint as we will display it ourselves
    271             mEditText.setHint(null);
    272         }
    273 
    274         if (mCounterView != null) {
    275             updateCounter(mEditText.getText().length());
    276         }
    277 
    278         if (mIndicatorArea != null) {
    279             adjustIndicatorPadding();
    280         }
    281 
    282         // Update the label visibility with no animation
    283         updateLabelState(false);
    284     }
    285 
    286     private LayoutParams updateEditTextMargin(ViewGroup.LayoutParams lp) {
    287         // Create/update the LayoutParams so that we can add enough top margin
    288         // to the EditText so make room for the label
    289         LayoutParams llp = lp instanceof LayoutParams ? (LayoutParams) lp : new LayoutParams(lp);
    290 
    291         if (mHintEnabled) {
    292             if (mTmpPaint == null) {
    293                 mTmpPaint = new Paint();
    294             }
    295             mTmpPaint.setTypeface(mCollapsingTextHelper.getCollapsedTypeface());
    296             mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize());
    297             llp.topMargin = (int) -mTmpPaint.ascent();
    298         } else {
    299             llp.topMargin = 0;
    300         }
    301 
    302         return llp;
    303     }
    304 
    305     private void updateLabelState(boolean animate) {
    306         final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
    307         final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
    308         final boolean isErrorShowing = !TextUtils.isEmpty(getError());
    309 
    310         if (mDefaultTextColor != null) {
    311             mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor.getDefaultColor());
    312         }
    313 
    314         if (mCounterOverflowed && mCounterView != null) {
    315             mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getCurrentTextColor());
    316         } else if (isFocused && mFocusedTextColor != null) {
    317             mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor.getDefaultColor());
    318         } else if (mDefaultTextColor != null) {
    319             mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor.getDefaultColor());
    320         }
    321 
    322         if (hasText || isFocused || isErrorShowing) {
    323             // We should be showing the label so do so if it isn't already
    324             collapseHint(animate);
    325         } else {
    326             // We should not be showing the label so hide it
    327             expandHint(animate);
    328         }
    329     }
    330 
    331     /**
    332      * Returns the {@link android.widget.EditText} used for text input.
    333      */
    334     @Nullable
    335     public EditText getEditText() {
    336         return mEditText;
    337     }
    338 
    339     /**
    340      * Set the hint to be displayed in the floating label, if enabled.
    341      *
    342      * @see #setHintEnabled(boolean)
    343      *
    344      * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
    345      */
    346     public void setHint(@Nullable CharSequence hint) {
    347         if (mHintEnabled) {
    348             setHintInternal(hint);
    349             sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
    350         }
    351     }
    352 
    353     private void setHintInternal(CharSequence hint) {
    354         mHint = hint;
    355         mCollapsingTextHelper.setText(hint);
    356     }
    357 
    358     /**
    359      * Returns the hint which is displayed in the floating label, if enabled.
    360      *
    361      * @return the hint, or null if there isn't one set, or the hint is not enabled.
    362      *
    363      * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
    364      */
    365     @Nullable
    366     public CharSequence getHint() {
    367         return mHintEnabled ? mHint : null;
    368     }
    369 
    370     /**
    371      * Sets whether the floating label functionality is enabled or not in this layout.
    372      *
    373      * <p>If enabled, any non-empty hint in the child EditText will be moved into the floating
    374      * hint, and its existing hint will be cleared. If disabled, then any non-empty floating hint
    375      * in this layout will be moved into the EditText, and this layout's hint will be cleared.</p>
    376      *
    377      * @see #setHint(CharSequence)
    378      * @see #isHintEnabled()
    379      *
    380      * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
    381      */
    382     public void setHintEnabled(boolean enabled) {
    383         if (enabled != mHintEnabled) {
    384             mHintEnabled = enabled;
    385 
    386             final CharSequence editTextHint = mEditText.getHint();
    387             if (!mHintEnabled) {
    388                 if (!TextUtils.isEmpty(mHint) && TextUtils.isEmpty(editTextHint)) {
    389                     // If the hint is disabled, but we have a hint set, and the EditText doesn't,
    390                     // pass it through...
    391                     mEditText.setHint(mHint);
    392                 }
    393                 // Now clear out any set hint
    394                 setHintInternal(null);
    395             } else {
    396                 if (!TextUtils.isEmpty(editTextHint)) {
    397                     // If the hint is now enabled and the EditText has one set, we'll use it if
    398                     // we don't already have one, and clear the EditText's
    399                     if (TextUtils.isEmpty(mHint)) {
    400                         setHint(editTextHint);
    401                     }
    402                     mEditText.setHint(null);
    403                 }
    404             }
    405 
    406             // Now update the EditText top margin
    407             if (mEditText != null) {
    408                 final LayoutParams lp = updateEditTextMargin(mEditText.getLayoutParams());
    409                 mEditText.setLayoutParams(lp);
    410             }
    411         }
    412     }
    413 
    414     /**
    415      * Returns whether the floating label functionality is enabled or not in this layout.
    416      *
    417      * @see #setHintEnabled(boolean)
    418      *
    419      * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
    420      */
    421     public boolean isHintEnabled() {
    422         return mHintEnabled;
    423     }
    424 
    425     /**
    426      * Sets the hint text color, size, style from the specified TextAppearance resource.
    427      *
    428      * @attr ref android.support.design.R.styleable#TextInputLayout_hintTextAppearance
    429      */
    430     public void setHintTextAppearance(@StyleRes int resId) {
    431         mCollapsingTextHelper.setCollapsedTextAppearance(resId);
    432         mFocusedTextColor = ColorStateList.valueOf(mCollapsingTextHelper.getCollapsedTextColor());
    433 
    434         if (mEditText != null) {
    435             updateLabelState(false);
    436 
    437             // Text size might have changed so update the top margin
    438             LayoutParams lp = updateEditTextMargin(mEditText.getLayoutParams());
    439             mEditText.setLayoutParams(lp);
    440             mEditText.requestLayout();
    441         }
    442     }
    443 
    444     private void addIndicator(TextView indicator, int index) {
    445         if (mIndicatorArea == null) {
    446             mIndicatorArea = new LinearLayout(getContext());
    447             mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL);
    448             addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT,
    449                     LinearLayout.LayoutParams.WRAP_CONTENT);
    450 
    451             // Add a flexible spacer in the middle so that the left/right views stay pinned
    452             final Space spacer = new Space(getContext());
    453             final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f);
    454             mIndicatorArea.addView(spacer, spacerLp);
    455 
    456             if (mEditText != null) {
    457                 adjustIndicatorPadding();
    458             }
    459         }
    460         mIndicatorArea.setVisibility(View.VISIBLE);
    461         mIndicatorArea.addView(indicator, index);
    462         mIndicatorsAdded++;
    463     }
    464 
    465     private void adjustIndicatorPadding() {
    466         // Add padding to the error and character counter so that they match the EditText
    467         ViewCompat.setPaddingRelative(mIndicatorArea, ViewCompat.getPaddingStart(mEditText),
    468                 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
    469     }
    470 
    471     private void removeIndicator(TextView indicator) {
    472         if (mIndicatorArea != null) {
    473             mIndicatorArea.removeView(indicator);
    474             if (--mIndicatorsAdded == 0) {
    475                 mIndicatorArea.setVisibility(View.GONE);
    476             }
    477         }
    478     }
    479 
    480     /**
    481      * Whether the error functionality is enabled or not in this layout. Enabling this
    482      * functionality before setting an error message via {@link #setError(CharSequence)}, will mean
    483      * that this layout will not change size when an error is displayed.
    484      *
    485      * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
    486      */
    487     public void setErrorEnabled(boolean enabled) {
    488         if (mErrorEnabled != enabled) {
    489             if (mErrorView != null) {
    490                 ViewCompat.animate(mErrorView).cancel();
    491             }
    492 
    493             if (enabled) {
    494                 mErrorView = new TextView(getContext());
    495                 try {
    496                     mErrorView.setTextAppearance(getContext(), mErrorTextAppearance);
    497                 } catch (Exception e) {
    498                     // Probably caused by our theme not extending from Theme.Design*. Instead
    499                     // we manually set something appropriate
    500                     mErrorView.setTextAppearance(getContext(),
    501                             android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
    502                     mErrorView.setTextColor(ContextCompat.getColor(
    503                             getContext(), R.color.design_textinput_error_color_light));
    504                 }
    505                 mErrorView.setVisibility(INVISIBLE);
    506                 ViewCompat.setAccessibilityLiveRegion(mErrorView,
    507                         ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
    508                 addIndicator(mErrorView, 0);
    509             } else {
    510                 mErrorShown = false;
    511                 updateEditTextBackground();
    512                 removeIndicator(mErrorView);
    513                 mErrorView = null;
    514             }
    515             mErrorEnabled = enabled;
    516         }
    517     }
    518 
    519     /**
    520      * Returns whether the error functionality is enabled or not in this layout.
    521      *
    522      * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
    523      *
    524      * @see #setErrorEnabled(boolean)
    525      */
    526     public boolean isErrorEnabled() {
    527         return mErrorEnabled;
    528     }
    529 
    530     /**
    531      * Sets an error message that will be displayed below our {@link EditText}. If the
    532      * {@code error} is {@code null}, the error message will be cleared.
    533      * <p>
    534      * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
    535      * it will be automatically enabled if {@code error} is not empty.
    536      *
    537      * @param error Error message to display, or null to clear
    538      *
    539      * @see #getError()
    540      */
    541     public void setError(@Nullable final CharSequence error) {
    542         mError = error;
    543 
    544         if (!mErrorEnabled) {
    545             if (TextUtils.isEmpty(error)) {
    546                 // If error isn't enabled, and the error is empty, just return
    547                 return;
    548             }
    549             // Else, we'll assume that they want to enable the error functionality
    550             setErrorEnabled(true);
    551         }
    552 
    553         // Only animate if we've been laid out already and we have a different error
    554         final boolean animate = ViewCompat.isLaidOut(this)
    555                 && !TextUtils.equals(mErrorView.getText(), error);
    556         mErrorShown = !TextUtils.isEmpty(error);
    557 
    558         // Cancel any on-going animation
    559         ViewCompat.animate(mErrorView).cancel();
    560 
    561         if (mErrorShown) {
    562             mErrorView.setText(error);
    563             mErrorView.setVisibility(VISIBLE);
    564 
    565             if (animate) {
    566                 if (ViewCompat.getAlpha(mErrorView) == 1f) {
    567                     // If it's currently 100% show, we'll animate it from 0
    568                     ViewCompat.setAlpha(mErrorView, 0f);
    569                 }
    570                 ViewCompat.animate(mErrorView)
    571                         .alpha(1f)
    572                         .setDuration(ANIMATION_DURATION)
    573                         .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
    574                         .setListener(new ViewPropertyAnimatorListenerAdapter() {
    575                             @Override
    576                             public void onAnimationStart(View view) {
    577                                 view.setVisibility(VISIBLE);
    578                             }
    579                         }).start();
    580             } else {
    581                 // Set alpha to 1f, just in case
    582                 ViewCompat.setAlpha(mErrorView, 1f);
    583             }
    584         } else {
    585             if (mErrorView.getVisibility() == VISIBLE) {
    586                 if (animate) {
    587                     ViewCompat.animate(mErrorView)
    588                             .alpha(0f)
    589                             .setDuration(ANIMATION_DURATION)
    590                             .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
    591                             .setListener(new ViewPropertyAnimatorListenerAdapter() {
    592                                 @Override
    593                                 public void onAnimationEnd(View view) {
    594                                     mErrorView.setText(error);
    595                                     view.setVisibility(INVISIBLE);
    596                                 }
    597                             }).start();
    598                 } else {
    599                     mErrorView.setText(error);
    600                     mErrorView.setVisibility(INVISIBLE);
    601                 }
    602             }
    603         }
    604 
    605         updateEditTextBackground();
    606         updateLabelState(true);
    607     }
    608 
    609     /**
    610      * Whether the character counter functionality is enabled or not in this layout.
    611      *
    612      * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
    613      */
    614     public void setCounterEnabled(boolean enabled) {
    615         if (mCounterEnabled != enabled) {
    616             if (enabled) {
    617                 mCounterView = new TextView(getContext());
    618                 mCounterView.setMaxLines(1);
    619                 try {
    620                     mCounterView.setTextAppearance(getContext(), mCounterTextAppearance);
    621                 } catch (Exception e) {
    622                     // Probably caused by our theme not extending from Theme.Design*. Instead
    623                     // we manually set something appropriate
    624                     mCounterView.setTextAppearance(getContext(),
    625                             android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
    626                     mCounterView.setTextColor(ContextCompat.getColor(
    627                             getContext(), R.color.design_textinput_error_color_light));
    628                 }
    629                 addIndicator(mCounterView, -1);
    630                 if (mEditText == null) {
    631                     updateCounter(0);
    632                 } else {
    633                     updateCounter(mEditText.getText().length());
    634                 }
    635             } else {
    636                 removeIndicator(mCounterView);
    637                 mCounterView = null;
    638             }
    639             mCounterEnabled = enabled;
    640         }
    641     }
    642 
    643     /**
    644      * Returns whether the character counter functionality is enabled or not in this layout.
    645      *
    646      * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
    647      *
    648      * @see #setCounterEnabled(boolean)
    649      */
    650     public boolean isCounterEnabled() {
    651         return mCounterEnabled;
    652     }
    653 
    654     /**
    655      * Sets the max length to display at the character counter.
    656      *
    657      * @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown.
    658      *
    659      * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
    660      */
    661     public void setCounterMaxLength(int maxLength) {
    662         if (mCounterMaxLength != maxLength) {
    663             if (maxLength > 0) {
    664                 mCounterMaxLength = maxLength;
    665             } else {
    666                 mCounterMaxLength = INVALID_MAX_LENGTH;
    667             }
    668             if (mCounterEnabled) {
    669                 updateCounter(mEditText == null ? 0 : mEditText.getText().length());
    670             }
    671         }
    672     }
    673 
    674     /**
    675      * Returns the max length shown at the character counter.
    676      *
    677      * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
    678      */
    679     public int getCounterMaxLength() {
    680         return mCounterMaxLength;
    681     }
    682 
    683     private void updateCounter(int length) {
    684         boolean wasCounterOverflowed = mCounterOverflowed;
    685         if (mCounterMaxLength == INVALID_MAX_LENGTH) {
    686             mCounterView.setText(String.valueOf(length));
    687             mCounterOverflowed = false;
    688         } else {
    689             mCounterOverflowed = length > mCounterMaxLength;
    690             if (wasCounterOverflowed != mCounterOverflowed) {
    691                 mCounterView.setTextAppearance(getContext(), mCounterOverflowed ?
    692                         mCounterOverflowTextAppearance : mCounterTextAppearance);
    693             }
    694             mCounterView.setText(getContext().getString(R.string.character_counter_pattern,
    695                     length, mCounterMaxLength));
    696         }
    697         if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) {
    698             updateLabelState(false);
    699             updateEditTextBackground();
    700         }
    701     }
    702 
    703     private void updateEditTextBackground() {
    704         ensureBackgroundDrawableStateWorkaround();
    705 
    706         Drawable editTextBackground = mEditText.getBackground();
    707         if (editTextBackground == null) {
    708             return;
    709         }
    710 
    711         if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
    712             editTextBackground = editTextBackground.mutate();
    713         }
    714 
    715         if (mErrorShown && mErrorView != null) {
    716             // Set a color filter of the error color
    717             editTextBackground.setColorFilter(
    718                     AppCompatDrawableManager.getPorterDuffColorFilter(
    719                             mErrorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
    720         } else if (mCounterOverflowed && mCounterView != null) {
    721             // Set a color filter of the counter color
    722             editTextBackground.setColorFilter(
    723                     AppCompatDrawableManager.getPorterDuffColorFilter(
    724                             mCounterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
    725         } else {
    726             // Else reset the color filter and refresh the drawable state so that the
    727             // normal tint is used
    728             clearColorFilter(editTextBackground);
    729             mEditText.refreshDrawableState();
    730         }
    731     }
    732 
    733     private static void clearColorFilter(@NonNull Drawable drawable) {
    734         drawable.clearColorFilter();
    735 
    736         if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) {
    737             // API 21 + 22 have an issue where clearing a color filter on a DrawableContainer
    738             // will not propagate to all of its children. To workaround this we unwrap the drawable
    739             // to find any DrawableContainers, and then unwrap those to clear the filter on its
    740             // children manually
    741             if (drawable instanceof InsetDrawable) {
    742                 clearColorFilter(((InsetDrawable) drawable).getDrawable());
    743             } else if (drawable instanceof DrawableWrapper) {
    744                 clearColorFilter(((DrawableWrapper) drawable).getWrappedDrawable());
    745             } else if (drawable instanceof DrawableContainer) {
    746                 final DrawableContainer container = (DrawableContainer) drawable;
    747                 final DrawableContainer.DrawableContainerState state =
    748                         (DrawableContainer.DrawableContainerState) container.getConstantState();
    749                 if (state != null) {
    750                     for (int i = 0, count = state.getChildCount(); i < count; i++) {
    751                         clearColorFilter(state.getChild(i));
    752                     }
    753                 }
    754             }
    755         }
    756     }
    757 
    758     private void ensureBackgroundDrawableStateWorkaround() {
    759         final int sdk = Build.VERSION.SDK_INT;
    760         if (sdk != 21 && sdk != 22) {
    761             // The workaround is only required on API 21-22
    762             return;
    763         }
    764         final Drawable bg = mEditText.getBackground();
    765         if (bg == null) {
    766             return;
    767         }
    768 
    769         if (!mHasReconstructedEditTextBackground) {
    770             // This is gross. There is an issue in the platform which affects container Drawables
    771             // where the first drawable retrieved from resources will propogate any changes
    772             // (like color filter) to all instances from the cache. We'll try to workaround it...
    773 
    774             final Drawable newBg = bg.getConstantState().newDrawable();
    775 
    776             if (bg instanceof DrawableContainer) {
    777                 // If we have a Drawable container, we can try and set it's constant state via
    778                 // reflection from the new Drawable
    779                 mHasReconstructedEditTextBackground =
    780                         DrawableUtils.setContainerConstantState(
    781                                 (DrawableContainer) bg, newBg.getConstantState());
    782             }
    783 
    784             if (!mHasReconstructedEditTextBackground) {
    785                 // If we reach here then we just need to set a brand new instance of the Drawable
    786                 // as the background. This has the unfortunate side-effect of wiping out any
    787                 // user set padding, but I'd hope that use of custom padding on an EditText
    788                 // is limited.
    789                 mEditText.setBackgroundDrawable(newBg);
    790                 mHasReconstructedEditTextBackground = true;
    791             }
    792         }
    793     }
    794 
    795     static class SavedState extends AbsSavedState {
    796         CharSequence error;
    797 
    798         SavedState(Parcelable superState) {
    799             super(superState);
    800         }
    801 
    802         public SavedState(Parcel source, ClassLoader loader) {
    803             super(source, loader);
    804             error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
    805 
    806         }
    807 
    808         @Override
    809         public void writeToParcel(Parcel dest, int flags) {
    810             super.writeToParcel(dest, flags);
    811             TextUtils.writeToParcel(error, dest, flags);
    812         }
    813 
    814         @Override
    815         public String toString() {
    816             return "TextInputLayout.SavedState{"
    817                     + Integer.toHexString(System.identityHashCode(this))
    818                     + " error=" + error + "}";
    819         }
    820 
    821         public static final Creator<SavedState> CREATOR = ParcelableCompat.newCreator(
    822                 new ParcelableCompatCreatorCallbacks<SavedState>() {
    823                     @Override
    824                     public SavedState createFromParcel(Parcel in, ClassLoader loader) {
    825                         return new SavedState(in, loader);
    826                     }
    827 
    828                     @Override
    829                     public SavedState[] newArray(int size) {
    830                         return new SavedState[size];
    831                     }
    832                 });
    833     }
    834 
    835     @Override
    836     public Parcelable onSaveInstanceState() {
    837         Parcelable superState = super.onSaveInstanceState();
    838         SavedState ss = new SavedState(superState);
    839         if (mErrorShown) {
    840             ss.error = getError();
    841         }
    842         return ss;
    843     }
    844 
    845     @Override
    846     protected void onRestoreInstanceState(Parcelable state) {
    847         if (!(state instanceof SavedState)) {
    848             super.onRestoreInstanceState(state);
    849             return;
    850         }
    851         SavedState ss = (SavedState) state;
    852         super.onRestoreInstanceState(ss.getSuperState());
    853         setError(ss.error);
    854         requestLayout();
    855     }
    856 
    857     /**
    858      * Returns the error message that was set to be displayed with
    859      * {@link #setError(CharSequence)}, or <code>null</code> if no error was set
    860      * or if error displaying is not enabled.
    861      *
    862      * @see #setError(CharSequence)
    863      */
    864     @Nullable
    865     public CharSequence getError() {
    866         return mErrorEnabled ? mError : null;
    867     }
    868 
    869     /**
    870      * Returns whether any hint state changes, due to being focused or non-empty text, are
    871      * animated.
    872      *
    873      * @see #setHintAnimationEnabled(boolean)
    874      *
    875      * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
    876      */
    877     public boolean isHintAnimationEnabled() {
    878         return mHintAnimationEnabled;
    879     }
    880 
    881     /**
    882      * Set whether any hint state changes, due to being focused or non-empty text, are
    883      * animated.
    884      *
    885      * @see #isHintAnimationEnabled()
    886      *
    887      * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
    888      */
    889     public void setHintAnimationEnabled(boolean enabled) {
    890         mHintAnimationEnabled = enabled;
    891     }
    892 
    893     @Override
    894     public void draw(Canvas canvas) {
    895         super.draw(canvas);
    896 
    897         if (mHintEnabled) {
    898             mCollapsingTextHelper.draw(canvas);
    899         }
    900     }
    901 
    902     @Override
    903     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    904         super.onLayout(changed, left, top, right, bottom);
    905 
    906         if (mHintEnabled && mEditText != null) {
    907             final int l = mEditText.getLeft() + mEditText.getCompoundPaddingLeft();
    908             final int r = mEditText.getRight() - mEditText.getCompoundPaddingRight();
    909 
    910             mCollapsingTextHelper.setExpandedBounds(l,
    911                     mEditText.getTop() + mEditText.getCompoundPaddingTop(),
    912                     r, mEditText.getBottom() - mEditText.getCompoundPaddingBottom());
    913 
    914             // Set the collapsed bounds to be the the full height (minus padding) to match the
    915             // EditText's editable area
    916             mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(),
    917                     r, bottom - top - getPaddingBottom());
    918 
    919             mCollapsingTextHelper.recalculate();
    920         }
    921     }
    922 
    923     @Override
    924     public void refreshDrawableState() {
    925         super.refreshDrawableState();
    926         // Drawable state has changed so see if we need to update the label
    927         updateLabelState(ViewCompat.isLaidOut(this));
    928     }
    929 
    930     private void collapseHint(boolean animate) {
    931         if (mAnimator != null && mAnimator.isRunning()) {
    932             mAnimator.cancel();
    933         }
    934         if (animate && mHintAnimationEnabled) {
    935             animateToExpansionFraction(1f);
    936         } else {
    937             mCollapsingTextHelper.setExpansionFraction(1f);
    938         }
    939     }
    940 
    941     private void expandHint(boolean animate) {
    942         if (mAnimator != null && mAnimator.isRunning()) {
    943             mAnimator.cancel();
    944         }
    945         if (animate && mHintAnimationEnabled) {
    946             animateToExpansionFraction(0f);
    947         } else {
    948             mCollapsingTextHelper.setExpansionFraction(0f);
    949         }
    950     }
    951 
    952     private void animateToExpansionFraction(final float target) {
    953         if (mCollapsingTextHelper.getExpansionFraction() == target) {
    954             return;
    955         }
    956         if (mAnimator == null) {
    957             mAnimator = ViewUtils.createAnimator();
    958             mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
    959             mAnimator.setDuration(ANIMATION_DURATION);
    960             mAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
    961                 @Override
    962                 public void onAnimationUpdate(ValueAnimatorCompat animator) {
    963                     mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue());
    964                 }
    965             });
    966         }
    967         mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
    968         mAnimator.start();
    969     }
    970 
    971     private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat {
    972         @Override
    973         public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
    974             super.onInitializeAccessibilityEvent(host, event);
    975             event.setClassName(TextInputLayout.class.getSimpleName());
    976         }
    977 
    978         @Override
    979         public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
    980             super.onPopulateAccessibilityEvent(host, event);
    981 
    982             final CharSequence text = mCollapsingTextHelper.getText();
    983             if (!TextUtils.isEmpty(text)) {
    984                 event.getText().add(text);
    985             }
    986         }
    987 
    988         @Override
    989         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
    990             super.onInitializeAccessibilityNodeInfo(host, info);
    991             info.setClassName(TextInputLayout.class.getSimpleName());
    992 
    993             final CharSequence text = mCollapsingTextHelper.getText();
    994             if (!TextUtils.isEmpty(text)) {
    995                 info.setText(text);
    996             }
    997             if (mEditText != null) {
    998                 info.setLabelFor(mEditText);
    999             }
   1000             final CharSequence error = mErrorView != null ? mErrorView.getText() : null;
   1001             if (!TextUtils.isEmpty(error)) {
   1002                 info.setContentInvalid(true);
   1003                 info.setError(error);
   1004             }
   1005         }
   1006     }
   1007 
   1008     private static boolean arrayContains(int[] array, int value) {
   1009         for (int v : array) {
   1010             if (v == value) {
   1011                 return true;
   1012             }
   1013         }
   1014         return false;
   1015     }
   1016 }