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 965 SavedState(Parcelable superState) { 966 super(superState); 967 } 968 969 SavedState(Parcel source, ClassLoader loader) { 970 super(source, loader); 971 error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); 972 973 } 974 975 @Override 976 public void writeToParcel(Parcel dest, int flags) { 977 super.writeToParcel(dest, flags); 978 TextUtils.writeToParcel(error, dest, flags); 979 } 980 981 @Override 982 public String toString() { 983 return "TextInputLayout.SavedState{" 984 + Integer.toHexString(System.identityHashCode(this)) 985 + " error=" + error + "}"; 986 } 987 988 public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() { 989 @Override 990 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 991 return new SavedState(in, loader); 992 } 993 994 @Override 995 public SavedState createFromParcel(Parcel in) { 996 return new SavedState(in, null); 997 } 998 999 @Override 1000 public SavedState[] newArray(int size) { 1001 return new SavedState[size]; 1002 } 1003 }; 1004 } 1005 1006 @Override 1007 public Parcelable onSaveInstanceState() { 1008 Parcelable superState = super.onSaveInstanceState(); 1009 SavedState ss = new SavedState(superState); 1010 if (mErrorShown) { 1011 ss.error = getError(); 1012 } 1013 return ss; 1014 } 1015 1016 @Override 1017 protected void onRestoreInstanceState(Parcelable state) { 1018 if (!(state instanceof SavedState)) { 1019 super.onRestoreInstanceState(state); 1020 return; 1021 } 1022 SavedState ss = (SavedState) state; 1023 super.onRestoreInstanceState(ss.getSuperState()); 1024 setError(ss.error); 1025 requestLayout(); 1026 } 1027 1028 @Override 1029 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 1030 mRestoringSavedState = true; 1031 super.dispatchRestoreInstanceState(container); 1032 mRestoringSavedState = false; 1033 } 1034 1035 /** 1036 * Returns the error message that was set to be displayed with 1037 * {@link #setError(CharSequence)}, or <code>null</code> if no error was set 1038 * or if error displaying is not enabled. 1039 * 1040 * @see #setError(CharSequence) 1041 */ 1042 @Nullable 1043 public CharSequence getError() { 1044 return mErrorEnabled ? mError : null; 1045 } 1046 1047 /** 1048 * Returns whether any hint state changes, due to being focused or non-empty text, are 1049 * animated. 1050 * 1051 * @see #setHintAnimationEnabled(boolean) 1052 * 1053 * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled 1054 */ 1055 public boolean isHintAnimationEnabled() { 1056 return mHintAnimationEnabled; 1057 } 1058 1059 /** 1060 * Set whether any hint state changes, due to being focused or non-empty text, are 1061 * animated. 1062 * 1063 * @see #isHintAnimationEnabled() 1064 * 1065 * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled 1066 */ 1067 public void setHintAnimationEnabled(boolean enabled) { 1068 mHintAnimationEnabled = enabled; 1069 } 1070 1071 @Override 1072 public void draw(Canvas canvas) { 1073 super.draw(canvas); 1074 1075 if (mHintEnabled) { 1076 mCollapsingTextHelper.draw(canvas); 1077 } 1078 } 1079 1080 @Override 1081 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1082 updatePasswordToggleView(); 1083 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1084 } 1085 1086 private void updatePasswordToggleView() { 1087 if (mEditText == null) { 1088 // If there is no EditText, there is nothing to update 1089 return; 1090 } 1091 1092 if (shouldShowPasswordIcon()) { 1093 if (mPasswordToggleView == null) { 1094 mPasswordToggleView = (CheckableImageButton) LayoutInflater.from(getContext()) 1095 .inflate(R.layout.design_text_input_password_icon, mInputFrame, false); 1096 mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable); 1097 mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc); 1098 mInputFrame.addView(mPasswordToggleView); 1099 1100 mPasswordToggleView.setOnClickListener(new View.OnClickListener() { 1101 @Override 1102 public void onClick(View view) { 1103 passwordVisibilityToggleRequested(); 1104 } 1105 }); 1106 } 1107 1108 if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) { 1109 // We should make sure that the EditText has the same min-height as the password 1110 // toggle view. This ensure focus works properly, and there is no visual jump 1111 // if the password toggle is enabled/disabled. 1112 mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView)); 1113 } 1114 1115 mPasswordToggleView.setVisibility(VISIBLE); 1116 mPasswordToggleView.setChecked(mPasswordToggledVisible); 1117 1118 // We need to add a dummy drawable as the end compound drawable so that the text is 1119 // indented and doesn't display below the toggle view 1120 if (mPasswordToggleDummyDrawable == null) { 1121 mPasswordToggleDummyDrawable = new ColorDrawable(); 1122 } 1123 mPasswordToggleDummyDrawable.setBounds(0, 0, mPasswordToggleView.getMeasuredWidth(), 1); 1124 1125 final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText); 1126 // Store the user defined end compound drawable so that we can restore it later 1127 if (compounds[2] != mPasswordToggleDummyDrawable) { 1128 mOriginalEditTextEndDrawable = compounds[2]; 1129 } 1130 TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], compounds[1], 1131 mPasswordToggleDummyDrawable, compounds[3]); 1132 1133 // Copy over the EditText's padding so that we match 1134 mPasswordToggleView.setPadding(mEditText.getPaddingLeft(), 1135 mEditText.getPaddingTop(), mEditText.getPaddingRight(), 1136 mEditText.getPaddingBottom()); 1137 } else { 1138 if (mPasswordToggleView != null && mPasswordToggleView.getVisibility() == VISIBLE) { 1139 mPasswordToggleView.setVisibility(View.GONE); 1140 } 1141 1142 if (mPasswordToggleDummyDrawable != null) { 1143 // Make sure that we remove the dummy end compound drawable if it exists, and then 1144 // clear it 1145 final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText); 1146 if (compounds[2] == mPasswordToggleDummyDrawable) { 1147 TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], 1148 compounds[1], mOriginalEditTextEndDrawable, compounds[3]); 1149 mPasswordToggleDummyDrawable = null; 1150 } 1151 } 1152 } 1153 } 1154 1155 /** 1156 * Set the icon to use for the password visibility toggle button. 1157 * 1158 * <p>If you use an icon you should also set a description for its action 1159 * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}. 1160 * This is used for accessibility.</p> 1161 * 1162 * @param resId resource id of the drawable to set, or 0 to clear the icon 1163 * 1164 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable 1165 */ 1166 public void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) { 1167 setPasswordVisibilityToggleDrawable(resId != 0 1168 ? AppCompatResources.getDrawable(getContext(), resId) 1169 : null); 1170 } 1171 1172 /** 1173 * Set the icon to use for the password visibility toggle button. 1174 * 1175 * <p>If you use an icon you should also set a description for its action 1176 * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}. 1177 * This is used for accessibility.</p> 1178 * 1179 * @param icon Drawable to set, may be null to clear the icon 1180 * 1181 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable 1182 */ 1183 public void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) { 1184 mPasswordToggleDrawable = icon; 1185 if (mPasswordToggleView != null) { 1186 mPasswordToggleView.setImageDrawable(icon); 1187 } 1188 } 1189 1190 /** 1191 * Set a content description for the navigation button if one is present. 1192 * 1193 * <p>The content description will be read via screen readers or other accessibility 1194 * systems to explain the action of the password visibility toggle.</p> 1195 * 1196 * @param resId Resource ID of a content description string to set, 1197 * or 0 to clear the description 1198 * 1199 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription 1200 */ 1201 public void setPasswordVisibilityToggleContentDescription(@StringRes int resId) { 1202 setPasswordVisibilityToggleContentDescription( 1203 resId != 0 ? getResources().getText(resId) : null); 1204 } 1205 1206 /** 1207 * Set a content description for the navigation button if one is present. 1208 * 1209 * <p>The content description will be read via screen readers or other accessibility 1210 * systems to explain the action of the password visibility toggle.</p> 1211 * 1212 * @param description Content description to set, or null to clear the content description 1213 * 1214 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription 1215 */ 1216 public void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) { 1217 mPasswordToggleContentDesc = description; 1218 if (mPasswordToggleView != null) { 1219 mPasswordToggleView.setContentDescription(description); 1220 } 1221 } 1222 1223 /** 1224 * Returns the icon currently used for the password visibility toggle button. 1225 * 1226 * @see #setPasswordVisibilityToggleDrawable(Drawable) 1227 * 1228 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable 1229 */ 1230 @Nullable 1231 public Drawable getPasswordVisibilityToggleDrawable() { 1232 return mPasswordToggleDrawable; 1233 } 1234 1235 /** 1236 * Returns the currently configured content description for the password visibility 1237 * toggle button. 1238 * 1239 * <p>This will be used to describe the navigation action to users through mechanisms 1240 * such as screen readers.</p> 1241 */ 1242 @Nullable 1243 public CharSequence getPasswordVisibilityToggleContentDescription() { 1244 return mPasswordToggleContentDesc; 1245 } 1246 1247 /** 1248 * Returns whether the password visibility toggle functionality is currently enabled. 1249 * 1250 * @see #setPasswordVisibilityToggleEnabled(boolean) 1251 */ 1252 public boolean isPasswordVisibilityToggleEnabled() { 1253 return mPasswordToggleEnabled; 1254 } 1255 1256 /** 1257 * Returns whether the password visibility toggle functionality is enabled or not. 1258 * 1259 * <p>When enabled, a button is placed at the end of the EditText which enables the user 1260 * to switch between the field's input being visibly disguised or not.</p> 1261 * 1262 * @param enabled true to enable the functionality 1263 * 1264 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleEnabled 1265 */ 1266 public void setPasswordVisibilityToggleEnabled(final boolean enabled) { 1267 if (mPasswordToggleEnabled != enabled) { 1268 mPasswordToggleEnabled = enabled; 1269 1270 if (!enabled && mPasswordToggledVisible && mEditText != null) { 1271 // If the toggle is no longer enabled, but we remove the PasswordTransformation 1272 // to make the password visible, add it back 1273 mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); 1274 } 1275 1276 // Reset the visibility tracking flag 1277 mPasswordToggledVisible = false; 1278 1279 updatePasswordToggleView(); 1280 } 1281 } 1282 1283 /** 1284 * Applies a tint to the the password visibility toggle drawable. Does not modify the current 1285 * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 1286 * 1287 * <p>Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will 1288 * automatically mutate the drawable and apply the specified tint and tint mode using 1289 * {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.</p> 1290 * 1291 * @param tintList the tint to apply, may be null to clear tint 1292 * 1293 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTint 1294 */ 1295 public void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) { 1296 mPasswordToggleTintList = tintList; 1297 mHasPasswordToggleTintList = true; 1298 applyPasswordToggleTint(); 1299 } 1300 1301 /** 1302 * Specifies the blending mode used to apply the tint specified by 1303 * {@link #setPasswordVisibilityToggleTintList(ColorStateList)} to the password 1304 * visibility toggle drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.</p> 1305 * 1306 * @param mode the blending mode used to apply the tint, may be null to clear tint 1307 * 1308 * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTintMode 1309 */ 1310 public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) { 1311 mPasswordToggleTintMode = mode; 1312 mHasPasswordToggleTintMode = true; 1313 applyPasswordToggleTint(); 1314 } 1315 1316 void passwordVisibilityToggleRequested() { 1317 if (mPasswordToggleEnabled) { 1318 // Store the current cursor position 1319 final int selection = mEditText.getSelectionEnd(); 1320 1321 if (hasPasswordTransformation()) { 1322 mEditText.setTransformationMethod(null); 1323 mPasswordToggledVisible = true; 1324 } else { 1325 mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); 1326 mPasswordToggledVisible = false; 1327 } 1328 1329 mPasswordToggleView.setChecked(mPasswordToggledVisible); 1330 1331 // And restore the cursor position 1332 mEditText.setSelection(selection); 1333 } 1334 } 1335 1336 private boolean hasPasswordTransformation() { 1337 return mEditText != null 1338 && mEditText.getTransformationMethod() instanceof PasswordTransformationMethod; 1339 } 1340 1341 private boolean shouldShowPasswordIcon() { 1342 return mPasswordToggleEnabled && (hasPasswordTransformation() || mPasswordToggledVisible); 1343 } 1344 1345 private void applyPasswordToggleTint() { 1346 if (mPasswordToggleDrawable != null 1347 && (mHasPasswordToggleTintList || mHasPasswordToggleTintMode)) { 1348 mPasswordToggleDrawable = DrawableCompat.wrap(mPasswordToggleDrawable).mutate(); 1349 1350 if (mHasPasswordToggleTintList) { 1351 DrawableCompat.setTintList(mPasswordToggleDrawable, mPasswordToggleTintList); 1352 } 1353 if (mHasPasswordToggleTintMode) { 1354 DrawableCompat.setTintMode(mPasswordToggleDrawable, mPasswordToggleTintMode); 1355 } 1356 1357 if (mPasswordToggleView != null 1358 && mPasswordToggleView.getDrawable() != mPasswordToggleDrawable) { 1359 mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable); 1360 } 1361 } 1362 } 1363 1364 @Override 1365 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1366 super.onLayout(changed, left, top, right, bottom); 1367 1368 if (mHintEnabled && mEditText != null) { 1369 final Rect rect = mTmpRect; 1370 ViewGroupUtils.getDescendantRect(this, mEditText, rect); 1371 1372 final int l = rect.left + mEditText.getCompoundPaddingLeft(); 1373 final int r = rect.right - mEditText.getCompoundPaddingRight(); 1374 1375 mCollapsingTextHelper.setExpandedBounds( 1376 l, rect.top + mEditText.getCompoundPaddingTop(), 1377 r, rect.bottom - mEditText.getCompoundPaddingBottom()); 1378 1379 // Set the collapsed bounds to be the the full height (minus padding) to match the 1380 // EditText's editable area 1381 mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(), 1382 r, bottom - top - getPaddingBottom()); 1383 1384 mCollapsingTextHelper.recalculate(); 1385 } 1386 } 1387 1388 private void collapseHint(boolean animate) { 1389 if (mAnimator != null && mAnimator.isRunning()) { 1390 mAnimator.cancel(); 1391 } 1392 if (animate && mHintAnimationEnabled) { 1393 animateToExpansionFraction(1f); 1394 } else { 1395 mCollapsingTextHelper.setExpansionFraction(1f); 1396 } 1397 mHintExpanded = false; 1398 } 1399 1400 @Override 1401 protected void drawableStateChanged() { 1402 if (mInDrawableStateChanged) { 1403 // Some of the calls below will update the drawable state of child views. Since we're 1404 // using addStatesFromChildren we can get into infinite recursion, hence we'll just 1405 // exit in this instance 1406 return; 1407 } 1408 1409 mInDrawableStateChanged = true; 1410 1411 super.drawableStateChanged(); 1412 1413 final int[] state = getDrawableState(); 1414 boolean changed = false; 1415 1416 // Drawable state has changed so see if we need to update the label 1417 updateLabelState(ViewCompat.isLaidOut(this) && isEnabled()); 1418 1419 updateEditTextBackground(); 1420 1421 if (mCollapsingTextHelper != null) { 1422 changed |= mCollapsingTextHelper.setState(state); 1423 } 1424 1425 if (changed) { 1426 invalidate(); 1427 } 1428 1429 mInDrawableStateChanged = false; 1430 } 1431 1432 private void expandHint(boolean animate) { 1433 if (mAnimator != null && mAnimator.isRunning()) { 1434 mAnimator.cancel(); 1435 } 1436 if (animate && mHintAnimationEnabled) { 1437 animateToExpansionFraction(0f); 1438 } else { 1439 mCollapsingTextHelper.setExpansionFraction(0f); 1440 } 1441 mHintExpanded = true; 1442 } 1443 1444 @VisibleForTesting 1445 void animateToExpansionFraction(final float target) { 1446 if (mCollapsingTextHelper.getExpansionFraction() == target) { 1447 return; 1448 } 1449 if (mAnimator == null) { 1450 mAnimator = new ValueAnimator(); 1451 mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); 1452 mAnimator.setDuration(ANIMATION_DURATION); 1453 mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1454 @Override 1455 public void onAnimationUpdate(ValueAnimator animator) { 1456 mCollapsingTextHelper.setExpansionFraction((float) animator.getAnimatedValue()); 1457 } 1458 }); 1459 } 1460 mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target); 1461 mAnimator.start(); 1462 } 1463 1464 @VisibleForTesting 1465 final boolean isHintExpanded() { 1466 return mHintExpanded; 1467 } 1468 1469 private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat { 1470 TextInputAccessibilityDelegate() { 1471 } 1472 1473 @Override 1474 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 1475 super.onInitializeAccessibilityEvent(host, event); 1476 event.setClassName(TextInputLayout.class.getSimpleName()); 1477 } 1478 1479 @Override 1480 public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 1481 super.onPopulateAccessibilityEvent(host, event); 1482 1483 final CharSequence text = mCollapsingTextHelper.getText(); 1484 if (!TextUtils.isEmpty(text)) { 1485 event.getText().add(text); 1486 } 1487 } 1488 1489 @Override 1490 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 1491 super.onInitializeAccessibilityNodeInfo(host, info); 1492 info.setClassName(TextInputLayout.class.getSimpleName()); 1493 1494 final CharSequence text = mCollapsingTextHelper.getText(); 1495 if (!TextUtils.isEmpty(text)) { 1496 info.setText(text); 1497 } 1498 if (mEditText != null) { 1499 info.setLabelFor(mEditText); 1500 } 1501 final CharSequence error = mErrorView != null ? mErrorView.getText() : null; 1502 if (!TextUtils.isEmpty(error)) { 1503 info.setContentInvalid(true); 1504 info.setError(error); 1505 } 1506 } 1507 } 1508 1509 private static boolean arrayContains(int[] array, int value) { 1510 for (int v : array) { 1511 if (v == value) { 1512 return true; 1513 } 1514 } 1515 return false; 1516 } 1517} 1518