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 * <android.support.design.widget.TextInputLayout 76 * android:layout_width="match_parent" 77 * android:layout_height="wrap_content"> 78 * 79 * <android.support.design.widget.TextInputEditText 80 * android:layout_width="match_parent" 81 * android:layout_height="wrap_content" 82 * android:hint="@string/form_username"/> 83 * 84 * </android.support.design.widget.TextInputLayout> 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 }