TextInputLayout.java revision 9fb154338a62edc2c57dc036895199d6f1769400
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.design.widget; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.TypedArray; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Paint; 25import android.os.Handler; 26import android.os.Message; 27import android.support.design.R; 28import android.support.v4.view.AccessibilityDelegateCompat; 29import android.support.v4.view.ViewCompat; 30import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; 31import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 32import android.text.Editable; 33import android.text.TextUtils; 34import android.text.TextWatcher; 35import android.util.AttributeSet; 36import android.util.TypedValue; 37import android.view.Gravity; 38import android.view.View; 39import android.view.ViewGroup; 40import android.view.accessibility.AccessibilityEvent; 41import android.view.animation.AccelerateInterpolator; 42import android.widget.EditText; 43import android.widget.LinearLayout; 44import android.widget.TextView; 45 46/** 47 * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label 48 * when the hint is hidden due to the user inputting text. 49 * 50 * Also supports showing an error via {@link #setErrorEnabled(boolean)} and 51 * {@link #setError(CharSequence)}. 52 */ 53public class TextInputLayout extends LinearLayout { 54 55 private static final int ANIMATION_DURATION = 200; 56 private static final int MSG_UPDATE_LABEL = 0; 57 58 private EditText mEditText; 59 private CharSequence mHint; 60 61 private boolean mErrorEnabled; 62 private TextView mErrorView; 63 private int mErrorTextAppearance; 64 65 private ColorStateList mLabelTextColor; 66 67 private final CollapsingTextHelper mCollapsingTextHelper; 68 private final Handler mHandler; 69 70 private ValueAnimatorCompat mAnimator; 71 72 public TextInputLayout(Context context) { 73 this(context, null); 74 } 75 76 public TextInputLayout(Context context, AttributeSet attrs) { 77 super(context, attrs); 78 79 setOrientation(VERTICAL); 80 setWillNotDraw(false); 81 82 mCollapsingTextHelper = new CollapsingTextHelper(this); 83 mHandler = new Handler(new Handler.Callback() { 84 @Override 85 public boolean handleMessage(Message message) { 86 switch (message.what) { 87 case MSG_UPDATE_LABEL: 88 updateLabelVisibility(true); 89 return true; 90 } 91 return false; 92 } 93 }); 94 95 mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); 96 mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator()); 97 mCollapsingTextHelper.setCollapsedTextVerticalGravity(Gravity.TOP); 98 99 final TypedArray a = context.obtainStyledAttributes(attrs, 100 R.styleable.TextInputLayout, 0, R.style.Widget_Design_TextInputLayout); 101 mHint = a.getText(R.styleable.TextInputLayout_android_hint); 102 103 final int hintAppearance = a.getResourceId( 104 R.styleable.TextInputLayout_hintTextAppearance, -1); 105 if (hintAppearance != -1) { 106 mCollapsingTextHelper.setCollapsedTextAppearance(hintAppearance); 107 } 108 109 mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0); 110 final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false); 111 112 // We create a ColorStateList using the specified text color, combining it with our 113 // theme's textColorHint 114 mLabelTextColor = createLabelTextColorStateList( 115 mCollapsingTextHelper.getCollapsedTextColor()); 116 117 mCollapsingTextHelper.setCollapsedTextColor(mLabelTextColor.getDefaultColor()); 118 mCollapsingTextHelper.setExpandedTextColor(mLabelTextColor.getDefaultColor()); 119 120 a.recycle(); 121 122 if (errorEnabled) { 123 setErrorEnabled(true); 124 } 125 126 if (ViewCompat.getImportantForAccessibility(this) 127 == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 128 // Make sure we're important for accessibility if we haven't been explicitly not 129 ViewCompat.setImportantForAccessibility(this, 130 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 131 } 132 133 ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate()); 134 } 135 136 @Override 137 public void addView(View child, int index, ViewGroup.LayoutParams params) { 138 if (child instanceof EditText) { 139 params = setEditText((EditText) child, params); 140 super.addView(child, 0, params); 141 } else { 142 // Carry on adding the View... 143 super.addView(child, index, params); 144 } 145 } 146 147 private LayoutParams setEditText(EditText editText, ViewGroup.LayoutParams lp) { 148 // If we already have an EditText, throw an exception 149 if (mEditText != null) { 150 throw new IllegalArgumentException("We already have an EditText, can only have one"); 151 } 152 mEditText = editText; 153 154 // Use the EditText's text size for our expanded text 155 mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize()); 156 157 // Add a TextWatcher so that we know when the text input has changed 158 mEditText.addTextChangedListener(new TextWatcher() { 159 @Override 160 public void afterTextChanged(Editable s) { 161 mHandler.sendEmptyMessage(MSG_UPDATE_LABEL); 162 } 163 164 @Override 165 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 166 } 167 168 @Override 169 public void onTextChanged(CharSequence s, int start, int before, int count) { 170 } 171 }); 172 173 // Add focus listener to the EditText so that we can notify the label that it is activated. 174 // Allows the use of a ColorStateList for the text color on the label 175 mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { 176 @Override 177 public void onFocusChange(View view, boolean focused) { 178 mHandler.sendEmptyMessage(MSG_UPDATE_LABEL); 179 } 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 // Create a new FrameLayout.LayoutParams so that we can add enough top margin 199 // to the EditText so make room for the label 200 LayoutParams newLp = new LayoutParams(lp); 201 Paint paint = new Paint(); 202 paint.setTextSize(mCollapsingTextHelper.getExpandedTextSize()); 203 newLp.topMargin = (int) -paint.ascent(); 204 205 return newLp; 206 } 207 208 private void updateLabelVisibility(boolean animate) { 209 boolean hasText = !TextUtils.isEmpty(mEditText.getText()); 210 boolean isFocused = mEditText.isFocused(); 211 212 mCollapsingTextHelper.setCollapsedTextColor(mLabelTextColor.getColorForState( 213 isFocused ? FOCUSED_STATE_SET : EMPTY_STATE_SET, 214 mLabelTextColor.getDefaultColor())); 215 216 if (hasText || isFocused) { 217 // We should be showing the label so do so if it isn't already 218 collapseHint(animate); 219 } else { 220 // We should not be showing the label so hide it 221 expandHint(animate); 222 } 223 } 224 225 /** 226 * @return the {@link android.widget.EditText} text input 227 */ 228 public EditText getEditText() { 229 return mEditText; 230 } 231 232 /** 233 * Set the hint to be displayed in the floating label 234 */ 235 public void setHint(CharSequence hint) { 236 mHint = hint; 237 mCollapsingTextHelper.setText(hint); 238 239 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 240 } 241 242 /** 243 * Whether the error functionality is enabled or not in this layout. Enabling this 244 * functionality before setting an error message via {@link #setError(CharSequence)}, will mean 245 * that this layout will not change size when an error is displayed. 246 */ 247 public void setErrorEnabled(boolean enabled) { 248 if (mErrorEnabled != enabled) { 249 if (enabled) { 250 mErrorView = new TextView(getContext()); 251 mErrorView.setTextAppearance(getContext(), mErrorTextAppearance); 252 mErrorView.setVisibility(INVISIBLE); 253 addView(mErrorView); 254 255 if (mEditText != null) { 256 // Add some start/end padding to the error so that it matches the EditText 257 ViewCompat.setPaddingRelative(mErrorView, ViewCompat.getPaddingStart(mEditText), 258 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom()); 259 } 260 } else { 261 removeView(mErrorView); 262 mErrorView = null; 263 } 264 mErrorEnabled = enabled; 265 } 266 } 267 268 /** 269 * Sets an error message that will be displayed below our {@link EditText}. If the 270 * {@code error} is {@code null}, the error message will be cleared. 271 * <p> 272 * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then 273 * it will be automatically enabled if {@code error} is not empty. 274 * 275 * @param error Error message to display, or null to clear 276 */ 277 public void setError(CharSequence error) { 278 if (!mErrorEnabled) { 279 if (TextUtils.isEmpty(error)) { 280 // If error isn't enabled, and the error is empty, just return 281 return; 282 } 283 // Else, we'll assume that they want to enable the error functionality 284 setErrorEnabled(true); 285 } 286 287 if (!TextUtils.isEmpty(error)) { 288 mErrorView.setText(error); 289 mErrorView.setVisibility(VISIBLE); 290 ViewCompat.setAlpha(mErrorView, 0f); 291 ViewCompat.animate(mErrorView) 292 .alpha(1f) 293 .setDuration(ANIMATION_DURATION) 294 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 295 .setListener(null) 296 .start(); 297 } else { 298 if (mErrorView.getVisibility() == VISIBLE) { 299 ViewCompat.animate(mErrorView) 300 .alpha(0f) 301 .setDuration(ANIMATION_DURATION) 302 .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR) 303 .setListener(new ViewPropertyAnimatorListenerAdapter() { 304 @Override 305 public void onAnimationEnd(View view) { 306 mErrorView.setText(null); 307 mErrorView.setVisibility(INVISIBLE); 308 } 309 }).start(); 310 } 311 } 312 313 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 314 } 315 316 @Override 317 public void draw(Canvas canvas) { 318 super.draw(canvas); 319 mCollapsingTextHelper.draw(canvas); 320 } 321 322 @Override 323 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 324 super.onLayout(changed, left, top, right, bottom); 325 326 mCollapsingTextHelper.onLayout(changed, left, top, right, bottom); 327 328 if (mEditText != null) { 329 final int l = mEditText.getLeft() + mEditText.getPaddingLeft(); 330 final int r = mEditText.getRight() - mEditText.getPaddingRight(); 331 332 mCollapsingTextHelper.setExpandedBounds(l, 333 mEditText.getTop() + mEditText.getPaddingTop(), 334 r, mEditText.getBottom() - mEditText.getPaddingBottom()); 335 336 // Set the collapsed bounds to be the the full height (minus padding) to match the 337 // EditText's editable area 338 mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(), 339 r, bottom - top - getPaddingBottom()); 340 } 341 } 342 343 private void collapseHint(boolean animate) { 344 if (animate) { 345 animateToExpansionFraction(1f); 346 } else { 347 mCollapsingTextHelper.setExpansionFraction(1f); 348 } 349 } 350 351 private void expandHint(boolean animate) { 352 if (animate) { 353 animateToExpansionFraction(0f); 354 } else { 355 mCollapsingTextHelper.setExpansionFraction(0f); 356 } 357 } 358 359 private void animateToExpansionFraction(final float target) { 360 if (mAnimator == null) { 361 mAnimator = ViewUtils.createAnimator(); 362 mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR); 363 mAnimator.setDuration(ANIMATION_DURATION); 364 mAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() { 365 @Override 366 public void onAnimationUpdate(ValueAnimatorCompat animator) { 367 mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue()); 368 } 369 }); 370 } else if (mAnimator.isRunning()) { 371 mAnimator.cancel(); 372 } 373 374 mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target); 375 mAnimator.start(); 376 } 377 378 private ColorStateList createLabelTextColorStateList(int color) { 379 final int[][] states = new int[2][]; 380 final int[] colors = new int[2]; 381 int i = 0; 382 383 // Focused 384 states[i] = FOCUSED_STATE_SET; 385 colors[i] = color; 386 i++; 387 388 states[i] = EMPTY_STATE_SET; 389 colors[i] = getThemeAttrColor(android.R.attr.textColorHint); 390 i++; 391 392 return new ColorStateList(states, colors); 393 } 394 395 private int getThemeAttrColor(int attr) { 396 TypedValue tv = new TypedValue(); 397 if (getContext().getTheme().resolveAttribute(attr, tv, true)) { 398 return tv.data; 399 } else { 400 return Color.MAGENTA; 401 } 402 } 403 404 private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat { 405 @Override 406 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 407 super.onInitializeAccessibilityEvent(host, event); 408 event.setClassName(TextInputLayout.class.getSimpleName()); 409 } 410 411 @Override 412 public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 413 super.onPopulateAccessibilityEvent(host, event); 414 415 final CharSequence text = mCollapsingTextHelper.getText(); 416 if (!TextUtils.isEmpty(text)) { 417 event.getText().add(text); 418 } 419 } 420 421 @Override 422 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 423 super.onInitializeAccessibilityNodeInfo(host, info); 424 info.setClassName(TextInputLayout.class.getSimpleName()); 425 426 final CharSequence text = mCollapsingTextHelper.getText(); 427 if (!TextUtils.isEmpty(text)) { 428 info.setText(text); 429 } 430 if (mEditText != null) { 431 info.setLabelFor(mEditText); 432 } 433 final CharSequence error = mErrorView != null ? mErrorView.getText() : null; 434 if (!TextUtils.isEmpty(error)) { 435 info.setContentInvalid(true); 436 info.setError(error); 437 } 438 } 439 } 440}