1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.design.widget; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.TypedArray; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.Typeface; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.StyleRes; 28 import android.support.design.R; 29 import android.support.v4.view.AccessibilityDelegateCompat; 30 import android.support.v4.view.GravityCompat; 31 import android.support.v4.view.ViewCompat; 32 import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; 33 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 34 import android.support.v7.internal.widget.TintManager; 35 import android.text.Editable; 36 import android.text.TextUtils; 37 import android.text.TextWatcher; 38 import android.util.AttributeSet; 39 import android.util.TypedValue; 40 import android.view.Gravity; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.animation.AccelerateInterpolator; 45 import android.widget.EditText; 46 import android.widget.LinearLayout; 47 import android.widget.TextView; 48 49 /** 50 * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label 51 * when the hint is hidden due to the user inputting text. 52 * 53 * Also supports showing an error via {@link #setErrorEnabled(boolean)} and 54 * {@link #setError(CharSequence)}. 55 */ 56 public class TextInputLayout extends LinearLayout { 57 58 private static final int ANIMATION_DURATION = 200; 59 60 private EditText mEditText; 61 private CharSequence mHint; 62 63 private Paint mTmpPaint; 64 65 private boolean mErrorEnabled; 66 private TextView mErrorView; 67 private int mErrorTextAppearance; 68 69 private ColorStateList mDefaultTextColor; 70 private ColorStateList mFocusedTextColor; 71 72 private final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this); 73 74 private boolean mHintAnimationEnabled; 75 private ValueAnimatorCompat mAnimator; 76 TextInputLayout(Context context)77 public TextInputLayout(Context context) { 78 this(context, null); 79 } 80 TextInputLayout(Context context, AttributeSet attrs)81 public TextInputLayout(Context context, AttributeSet attrs) { 82 this(context, attrs, 0); 83 } 84 TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr)85 public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) { 86 // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10 87 super(context, attrs); 88 89 setOrientation(VERTICAL); 90 setWillNotDraw(false); 91 setAddStatesFromChildren(true); 92 93 mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 94 mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator()); 95 mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START); 96 97 final TypedArray a = context.obtainStyledAttributes(attrs, 98 R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout); 99 mHint = a.getText(R.styleable.TextInputLayout_android_hint); 100 mHintAnimationEnabled = a.getBoolean( 101 R.styleable.TextInputLayout_hintAnimationEnabled, true); 102 103 if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) { 104 mDefaultTextColor = mFocusedTextColor = 105 a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint); 106 } 107 108 final int hintAppearance = a.getResourceId( 109 R.styleable.TextInputLayout_hintTextAppearance, -1); 110 if (hintAppearance != -1) { 111 setHintTextAppearance( 112 a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0)); 113 } 114 115 mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0); 116 final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false); 117 a.recycle(); 118 119 setErrorEnabled(errorEnabled); 120 121 if (ViewCompat.getImportantForAccessibility(this) 122 == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 123 // Make sure we're important for accessibility if we haven't been explicitly not 124 ViewCompat.setImportantForAccessibility(this, 125 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 126 } 127 128 ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate()); 129 } 130 131 @Override addView(View child, int index, ViewGroup.LayoutParams params)132 public void addView(View child, int index, ViewGroup.LayoutParams params) { 133 if (child instanceof EditText) { 134 setEditText((EditText) child); 135 super.addView(child, 0, updateEditTextMargin(params)); 136 } else { 137 // Carry on adding the View... 138 super.addView(child, index, params); 139 } 140 } 141 142 /** 143 * Set the typeface to use for the both the expanded and floating hint. 144 * 145 * @param typeface typeface to use, or {@code null} to use the default. 146 */ setTypeface(@ullable Typeface typeface)147 public void setTypeface(@Nullable Typeface typeface) { 148 mCollapsingTextHelper.setTypeface(typeface); 149 } 150 setEditText(EditText editText)151 private void setEditText(EditText editText) { 152 // If we already have an EditText, throw an exception 153 if (mEditText != null) { 154 throw new IllegalArgumentException("We already have an EditText, can only have one"); 155 } 156 mEditText = editText; 157 158 // Use the EditText's typeface, and it's text size for our expanded text 159 mCollapsingTextHelper.setTypeface(mEditText.getTypeface()); 160 mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize()); 161 mCollapsingTextHelper.setExpandedTextGravity(mEditText.getGravity()); 162 163 // Add a TextWatcher so that we know when the text input has changed 164 mEditText.addTextChangedListener(new TextWatcher() { 165 @Override 166 public void afterTextChanged(Editable s) { 167 updateLabelVisibility(true); 168 } 169 170 @Override 171 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 172 173 @Override 174 public void onTextChanged(CharSequence s, int start, int before, int count) {} 175 }); 176 177 // Use the EditText's hint colors if we don't have one set 178 if (mDefaultTextColor == null) { 179 mDefaultTextColor = mEditText.getHintTextColors(); 180 } 181 182 // If we do not have a valid hint, try and retrieve it from the EditText 183 if (TextUtils.isEmpty(mHint)) { 184 setHint(mEditText.getHint()); 185 // Clear the EditText's hint as we will display it ourselves 186 mEditText.setHint(null); 187 } 188 189 if (mErrorView != null) { 190 // Add some start/end padding to the error so that it matches the EditText 191 ViewCompat.setPaddingRelative(mErrorView, ViewCompat.getPaddingStart(mEditText), 192 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom()); 193 } 194 195 // Update the label visibility with no animation 196 updateLabelVisibility(false); 197 } 198 updateEditTextMargin(ViewGroup.LayoutParams lp)199 private LayoutParams updateEditTextMargin(ViewGroup.LayoutParams lp) { 200 // Create/update the LayoutParams so that we can add enough top margin 201 // to the EditText so make room for the label 202 LayoutParams llp = lp instanceof LayoutParams ? (LayoutParams) lp : new LayoutParams(lp); 203 204 if (mTmpPaint == null) { 205 mTmpPaint = new Paint(); 206 } 207 mTmpPaint.setTypeface(mCollapsingTextHelper.getTypeface()); 208 mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize()); 209 llp.topMargin = (int) -mTmpPaint.ascent(); 210 211 return llp; 212 } 213 updateLabelVisibility(boolean animate)214 private void updateLabelVisibility(boolean animate) { 215 boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText()); 216 boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused); 217 218 if (mDefaultTextColor != null && mFocusedTextColor != null) { 219 mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor.getDefaultColor()); 220 mCollapsingTextHelper.setCollapsedTextColor(isFocused 221 ? mFocusedTextColor.getDefaultColor() 222 : mDefaultTextColor.getDefaultColor()); 223 } 224 225 if (hasText || isFocused) { 226 // We should be showing the label so do so if it isn't already 227 collapseHint(animate); 228 } else { 229 // We should not be showing the label so hide it 230 expandHint(animate); 231 } 232 } 233 234 /** 235 * Returns the {@link android.widget.EditText} used for text input. 236 */ 237 @Nullable getEditText()238 public EditText getEditText() { 239 return mEditText; 240 } 241 242 /** 243 * Set the hint to be displayed in the floating label 244 * 245 * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint 246 */ setHint(@ullable CharSequence hint)247 public void setHint(@Nullable CharSequence hint) { 248 mHint = hint; 249 mCollapsingTextHelper.setText(hint); 250 251 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 252 } 253 254 /** 255 * Returns the hint which is displayed in the floating label. 256 * 257 * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint 258 */ 259 @Nullable getHint()260 public CharSequence getHint() { 261 return mHint; 262 } 263 264 /** 265 * Sets the hint text color, size, style from the specified TextAppearance resource. 266 * 267 * @attr ref android.support.design.R.styleable#TextInputLayout_hintTextAppearance 268 */ setHintTextAppearance(@tyleRes int resId)269 public void setHintTextAppearance(@StyleRes int resId) { 270 mCollapsingTextHelper.setCollapsedTextAppearance(resId); 271 mFocusedTextColor = ColorStateList.valueOf(mCollapsingTextHelper.getCollapsedTextColor()); 272 273 if (mEditText != null) { 274 updateLabelVisibility(false); 275 276 // Text size might have changed so update the top margin 277 LayoutParams lp = updateEditTextMargin(mEditText.getLayoutParams()); 278 mEditText.setLayoutParams(lp); 279 mEditText.requestLayout(); 280 } 281 } 282 283 /** 284 * Whether the error functionality is enabled or not in this layout. Enabling this 285 * functionality before setting an error message via {@link #setError(CharSequence)}, will mean 286 * that this layout will not change size when an error is displayed. 287 * 288 * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled 289 */ setErrorEnabled(boolean enabled)290 public void setErrorEnabled(boolean enabled) { 291 if (mErrorEnabled != enabled) { 292 if (mErrorView != null) { 293 ViewCompat.animate(mErrorView).cancel(); 294 } 295 296 if (enabled) { 297 mErrorView = new TextView(getContext()); 298 mErrorView.setTextAppearance(getContext(), mErrorTextAppearance); 299 mErrorView.setVisibility(INVISIBLE); 300 addView(mErrorView); 301 302 if (mEditText != null) { 303 // Add some start/end padding to the error so that it matches the EditText 304 ViewCompat.setPaddingRelative(mErrorView, ViewCompat.getPaddingStart(mEditText), 305 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom()); 306 } 307 } else { 308 removeView(mErrorView); 309 mErrorView = null; 310 } 311 mErrorEnabled = enabled; 312 } 313 } 314 315 /** 316 * Returns whether the error functionality is enabled or not in this layout. 317 * 318 * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled 319 * 320 * @see #setErrorEnabled(boolean) 321 */ isErrorEnabled()322 public boolean isErrorEnabled() { 323 return mErrorEnabled; 324 } 325 326 /** 327 * Sets an error message that will be displayed below our {@link EditText}. If the 328 * {@code error} is {@code null}, the error message will be cleared. 329 * <p> 330 * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then 331 * it will be automatically enabled if {@code error} is not empty. 332 * 333 * @param error Error message to display, or null to clear 334 * 335 * @see #getError() 336 */ setError(@ullable CharSequence error)337 public void setError(@Nullable CharSequence error) { 338 if (!mErrorEnabled) { 339 if (TextUtils.isEmpty(error)) { 340 // If error isn't enabled, and the error is empty, just return 341 return; 342 } 343 // Else, we'll assume that they want to enable the error functionality 344 setErrorEnabled(true); 345 } 346 347 if (!TextUtils.isEmpty(error)) { 348 ViewCompat.setAlpha(mErrorView, 0f); 349 mErrorView.setText(error); 350 ViewCompat.animate(mErrorView) 351 .alpha(1f) 352 .setDuration(ANIMATION_DURATION) 353 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 354 .setListener(new ViewPropertyAnimatorListenerAdapter() { 355 @Override 356 public void onAnimationStart(View view) { 357 view.setVisibility(VISIBLE); 358 } 359 }) 360 .start(); 361 362 // Set the EditText's background tint to the error color 363 ViewCompat.setBackgroundTintList(mEditText, 364 ColorStateList.valueOf(mErrorView.getCurrentTextColor())); 365 } else { 366 if (mErrorView.getVisibility() == VISIBLE) { 367 ViewCompat.animate(mErrorView) 368 .alpha(0f) 369 .setDuration(ANIMATION_DURATION) 370 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 371 .setListener(new ViewPropertyAnimatorListenerAdapter() { 372 @Override 373 public void onAnimationEnd(View view) { 374 view.setVisibility(INVISIBLE); 375 } 376 }).start(); 377 378 // Restore the 'original' tint, using colorControlNormal and colorControlActivated 379 final TintManager tintManager = TintManager.get(getContext()); 380 ViewCompat.setBackgroundTintList(mEditText, 381 tintManager.getTintList(R.drawable.abc_edit_text_material)); 382 } 383 } 384 385 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 386 } 387 388 /** 389 * Returns the error message that was set to be displayed with 390 * {@link #setError(CharSequence)}, or <code>null</code> if no error was set 391 * or if error displaying is not enabled. 392 * 393 * @see #setError(CharSequence) 394 */ 395 @Nullable getError()396 public CharSequence getError() { 397 if (mErrorEnabled && mErrorView != null && mErrorView.getVisibility() == VISIBLE) { 398 return mErrorView.getText(); 399 } 400 return null; 401 } 402 403 /** 404 * Returns whether any hint state changes, due to being focused or non-empty text, are 405 * animated. 406 * 407 * @see #setHintAnimationEnabled(boolean) 408 * 409 * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled 410 */ isHintAnimationEnabled()411 public boolean isHintAnimationEnabled() { 412 return mHintAnimationEnabled; 413 } 414 415 /** 416 * Set whether any hint state changes, due to being focused or non-empty text, are 417 * animated. 418 * 419 * @see #isHintAnimationEnabled() 420 * 421 * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled 422 */ setHintAnimationEnabled(boolean enabled)423 public void setHintAnimationEnabled(boolean enabled) { 424 mHintAnimationEnabled = enabled; 425 } 426 427 @Override draw(Canvas canvas)428 public void draw(Canvas canvas) { 429 super.draw(canvas); 430 mCollapsingTextHelper.draw(canvas); 431 } 432 433 @Override onLayout(boolean changed, int left, int top, int right, int bottom)434 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 435 super.onLayout(changed, left, top, right, bottom); 436 437 if (mEditText != null) { 438 final int l = mEditText.getLeft() + mEditText.getCompoundPaddingLeft(); 439 final int r = mEditText.getRight() - mEditText.getCompoundPaddingRight(); 440 441 mCollapsingTextHelper.setExpandedBounds(l, 442 mEditText.getTop() + mEditText.getCompoundPaddingTop(), 443 r, mEditText.getBottom() - mEditText.getCompoundPaddingBottom()); 444 445 // Set the collapsed bounds to be the the full height (minus padding) to match the 446 // EditText's editable area 447 mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(), 448 r, bottom - top - getPaddingBottom()); 449 450 mCollapsingTextHelper.recalculate(); 451 } 452 } 453 454 @Override refreshDrawableState()455 public void refreshDrawableState() { 456 super.refreshDrawableState(); 457 // Drawable state has changed so see if we need to update the label 458 updateLabelVisibility(ViewCompat.isLaidOut(this)); 459 } 460 collapseHint(boolean animate)461 private void collapseHint(boolean animate) { 462 if (mAnimator != null && mAnimator.isRunning()) { 463 mAnimator.cancel(); 464 } 465 if (animate && mHintAnimationEnabled) { 466 animateToExpansionFraction(1f); 467 } else { 468 mCollapsingTextHelper.setExpansionFraction(1f); 469 } 470 } 471 expandHint(boolean animate)472 private void expandHint(boolean animate) { 473 if (mAnimator != null && mAnimator.isRunning()) { 474 mAnimator.cancel(); 475 } 476 if (animate && mHintAnimationEnabled) { 477 animateToExpansionFraction(0f); 478 } else { 479 mCollapsingTextHelper.setExpansionFraction(0f); 480 } 481 } 482 animateToExpansionFraction(final float target)483 private void animateToExpansionFraction(final float target) { 484 if (mCollapsingTextHelper.getExpansionFraction() == target) { 485 return; 486 } 487 if (mAnimator == null) { 488 mAnimator = ViewUtils.createAnimator(); 489 mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); 490 mAnimator.setDuration(ANIMATION_DURATION); 491 mAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 492 @Override 493 public void onAnimationUpdate(ValueAnimatorCompat animator) { 494 mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue()); 495 } 496 }); 497 } 498 mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target); 499 mAnimator.start(); 500 } 501 getThemeAttrColor(int attr)502 private int getThemeAttrColor(int attr) { 503 TypedValue tv = new TypedValue(); 504 if (getContext().getTheme().resolveAttribute(attr, tv, true)) { 505 return tv.data; 506 } else { 507 return Color.MAGENTA; 508 } 509 } 510 511 private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat { 512 @Override onInitializeAccessibilityEvent(View host, AccessibilityEvent event)513 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 514 super.onInitializeAccessibilityEvent(host, event); 515 event.setClassName(TextInputLayout.class.getSimpleName()); 516 } 517 518 @Override onPopulateAccessibilityEvent(View host, AccessibilityEvent event)519 public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 520 super.onPopulateAccessibilityEvent(host, event); 521 522 final CharSequence text = mCollapsingTextHelper.getText(); 523 if (!TextUtils.isEmpty(text)) { 524 event.getText().add(text); 525 } 526 } 527 528 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)529 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 530 super.onInitializeAccessibilityNodeInfo(host, info); 531 info.setClassName(TextInputLayout.class.getSimpleName()); 532 533 final CharSequence text = mCollapsingTextHelper.getText(); 534 if (!TextUtils.isEmpty(text)) { 535 info.setText(text); 536 } 537 if (mEditText != null) { 538 info.setLabelFor(mEditText); 539 } 540 final CharSequence error = mErrorView != null ? mErrorView.getText() : null; 541 if (!TextUtils.isEmpty(error)) { 542 info.setContentInvalid(true); 543 info.setError(error); 544 } 545 } 546 } 547 arrayContains(int[] array, int value)548 private static boolean arrayContains(int[] array, int value) { 549 for (int v : array) { 550 if (v == value) { 551 return true; 552 } 553 } 554 return false; 555 } 556 }