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