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