- 文章目录
- 效果图
- TextInputLayout的使用
- 源码解析
- TextInputLayout继承自LinearLayout,配合EditText使用,可以实现MD风格效果
- 使用前添加依赖
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
- 在xml文件中,EditText作为子空间放到TextInputLayout中
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/common_toolbar" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp"
android:text="登录"
android:textColor="@color/colorPrimary"
android:textSize="38sp" />
<android.support.design.widget.TextInputLayout
android:id="@+id/til_username"
android:layout_width="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_height="wrap_content"
android:layout_marginTop="30dp">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="手机号"
android:textColor="@color/Black" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/til_password"
android:layout_width="match_parent"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_height="wrap_content">
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:textColor="@color/Black" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="登录"
android:textAllCaps="false" />
</LinearLayout>
- 当EidtText获得焦点后提示信息往上移动
tilUserName.setHint("手机号");
tilPassword.setHint("密码");
- 设置EditText输入文字最大值并监听
tilUserName.setCounterEnabled(true);
tilUserName.setCounterMaxLength(11);
- 解决报错:java.lang.UnsupportedOperationException: Failed to resolve attribute at index:需要自己在主题中设置
//在Theme主题的style中添加该item
<item name="textColorError">@color/design_textinput_error_color_light</item>
- 当输入的信息不正确时,给出错误提示(例如手机格式不正确)
tilUserName.setError("手机号最多11位");
//不显示错误信息
tilUserName.setErrorEnabled(false);
- Activity代码
public class TextInputLayoutActivity extends BaseActivity {
/**
* 正则表达式:验证密码
*/
public static final String REGEX_PASSWORD = "^[a-zA-Z0-9]{6,16}$";
/**
* 正则表达式:验证手机号
*/
public static final String REGEX_MOBILE = "^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_input_layout);
initToolBar();
final TextInputLayout tilUserName = (TextInputLayout) findViewById(R.id.til_username);
final TextInputLayout tilPassword = (TextInputLayout) findViewById(R.id.til_password);
Button btnLogin = (Button) findViewById(R.id.btn_login);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String phone = tilUserName.getEditText().getText().toString().trim();
String password = tilPassword.getEditText().getText().toString().trim();
boolean pass = true;
tilUserName.setErrorEnabled(false);
tilPassword.setErrorEnabled(false);
if (!isMobile(phone)) {//输入的不是手机号
tilUserName.setError("请输入正确的手机号");
pass = false;
}
if (!isPassword(password)) {
tilPassword.setError("密码至少6位");
pass = false;
}
if (!pass) {
return;
}
doLogin();
}
});
//设置提示信息
tilUserName.setHint("手机号");
tilUserName.setCounterEnabled(true);
tilUserName.setCounterMaxLength(11);
tilPassword.setHint("密码");
tilUserName.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s.toString().length() > 11) {
tilUserName.setError("手机号最多11位");
} else {
tilUserName.setErrorEnabled(false);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
private void doLogin() {
Toast.makeText(TextInputLayoutActivity.this, "登录验证通过", android.widget.Toast.LENGTH_SHORT).show();
}
public static boolean isMobile(String mobile) {
return Pattern.matches(REGEX_MOBILE, mobile);
}
public static boolean isPassword(String password) {
return Pattern.matches(REGEX_PASSWORD, password);
}
}
- 源码解析
- 构造函数:该类继承自LinearLayout,orientation为垂直
public class TextInputLayout extends LinearLayout {
public TextInputLayout(Context context) {
this(context, null);
}
public TextInputLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
// Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10
super(context, attrs);
ThemeUtils.checkAppCompatTheme(context);
setOrientation(VERTICAL);
setWillNotDraw(false);
setAddStatesFromChildren(true);
mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
//获取xml设置的属性信息
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
setHint(a.getText(R.styleable.TextInputLayout_android_hint));
mHintAnimationEnabled = a.getBoolean(
R.styleable.TextInputLayout_hintAnimationEnabled, true);
if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) {
mDefaultTextColor = mFocusedTextColor =
a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint);
}
final int hintAppearance = a.getResourceId(
R.styleable.TextInputLayout_hintTextAppearance, -1);
if (hintAppearance != -1) {
setHintTextAppearance(
a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0));
}
mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
final boolean counterEnabled = a.getBoolean(
R.styleable.TextInputLayout_counterEnabled, false);
setCounterMaxLength(
a.getInt(R.styleable.TextInputLayout_counterMaxLength, INVALID_MAX_LENGTH));
mCounterTextAppearance = a.getResourceId(
R.styleable.TextInputLayout_counterTextAppearance, 0);
mCounterOverflowTextAppearance = a.getResourceId(
R.styleable.TextInputLayout_counterOverflowTextAppearance, 0);
a.recycle();
setErrorEnabled(errorEnabled);
setCounterEnabled(counterEnabled);
if (ViewCompat.getImportantForAccessibility(this)
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
// Make sure we're important for accessibility if we haven't been explicitly not
ViewCompat.setImportantForAccessibility(this,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate());
}
- }
- 构造函数中主要处理
- 设置错误提示信息setErrorEnabled()
public void setErrorEnabled(boolean enabled) {
- //巧妙的写法,判断如果和错误提示状态和之前是一样的,不用处理->学习
if (mErrorEnabled != enabled) {
if (mErrorView != null) {
ViewCompat.animate(mErrorView).cancel();
}
if (enabled) {
/
/进入到这个条件中,表示之前之前状态是没有该错误提示空间,需要创建并添加mErrorView = new TextView(getContext());
try {
mErrorView.setTextAppearance(getContext(), mErrorTextAppearance);//在xml文件中设置错误信息样式
} catch (Exception e) {
// Probably caused by our theme not extending from Theme.Design*. Instead
// we manually set something appropriate
//使用系统的样式
mErrorView.setTextAppearance(getContext(),
android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
mErrorView.setTextColor(ContextCompat.getColor(
getContext(), R.color.design_textinput_error_color_light));
}
mErrorView.setVisibility(INVISIBLE);//先隐藏
ViewCompat.setAccessibilityLiveRegion(mErrorView,
ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
addIndicator(mErrorView, 0);//添加到Indicator中
} else {//false的话,需要将该错误信息的背景和对象引用置空,并刷新界面
mErrorShown = false;
updateEditTextBackground();
removeIndicator(mErrorView);
mErrorView = null;
}
mErrorEnabled = enabled;
}
}
在看下addIndicator()方法private void addIndicator(TextView indicator, int index) {
if (mIndicatorArea == null) {
//发现,mIndicatorArea也是一个LinearLayout,且方向为横向摆放,EidtText长度信息也是放在该控件中
mIndicatorArea = new LinearLayout(getContext());
mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL);
//将该控件mIndicatorArea添加至TextInputLayout中
addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
// Add a flexible spacer in the middle so that the left/right views stay pinned
final Space spacer = new Space(getContext());
final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f);
mIndicatorArea.addView(spacer, spacerLp);
if (mEditText != null) {
adjustIndicatorPadding();
}
}
mIndicatorArea.setVisibility(View.VISIBLE);
//添加
mIndicatorArea.addView(indicator, index);
mIndicatorsAdded++;
}
- 设置EditText长度提示信息setCounterEnable(),该方法和setErrorEnabled()方法类似
public void setCounterEnabled(boolean enabled) {
if (mCounterEnabled != enabled) {
if (enabled) {
mCounterView = new TextView(getContext());//新建
mCounterView.setMaxLines(1);
try {
mCounterView.setTextAppearance(getContext(), mCounterTextAppearance);
} catch (Exception e) {
// Probably caused by our theme not extending from Theme.Design*. Instead
// we manually set something appropriate
mCounterView.setTextAppearance(getContext(),
android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
mCounterView.setTextColor(ContextCompat.getColor(
getContext(), R.color.design_textinput_error_color_light));
}
addIndicator(mCounterView, -1);//添加
if (mEditText == null) {
updateCounter(0);
} else {
updateCounter(mEditText.getText().length());
}
} else {
removeIndicator(mCounterView);
mCounterView = null;
}
mCounterEnabled = enabled;
}
}
- ViewGroup的三个主流方法中处理逻辑
- draw()
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mHintEnabled) {
mCollapsingTextHelper.draw(canvas);
}
}
mHintEnabled默认为true:mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
- onLayout()
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mHintEnabled && mEditText != null) {
final int l = mEditText.getLeft() + mEditText.getCompoundPaddingLeft();
final int r = mEditText.getRight() - mEditText.getCompoundPaddingRight();
mCollapsingTextHelper.setExpandedBounds(l,
mEditText.getTop() + mEditText.getCompoundPaddingTop(),
r, mEditText.getBottom() - mEditText.getCompoundPaddingBottom());
// Set the collapsed bounds to be the the full height (minus padding) to match the
// EditText's editable area
mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(),
r, bottom - top - getPaddingBottom());
mCollapsingTextHelper.recalculate();
}
}
- 问题一:TextInputLayout和EditText是如何关联的?
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (child instanceof EditText) {
setEditText((EditText) child);
super.addView(child, 0, updateEditTextMargin(params));
} else {
// Carry on adding the View...
super.addView(child, index, params);
}
}
关键方法为addView(child),该方法继承自ViewGroup,会将内部的EditText添加至TextInputLayout中
- 再看setEditText()方法
private void setEditText(EditText editText) {
// If we already have an EditText, throw an exception
if (mEditText != null) {
throw new IllegalArgumentException("We already have an EditText, can only have one");
}
if (!(editText instanceof TextInputEditText)) {
Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that"
+ " class instead.");
}
mEditText = editText;
// Use the EditText's typeface, and it's text size for our expanded text
mCollapsingTextHelper.setTypefaces(mEditText.getTypeface());
mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize());
final int editTextGravity = mEditText.getGravity();
mCollapsingTextHelper.setCollapsedTextGravity(
Gravity.TOP | (editTextGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK));
mCollapsingTextHelper.setExpandedTextGravity(editTextGravity);
// Add a TextWatcher so that we know when the text input has changed
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
updateLabelState(true);
if (mCounterEnabled) {
updateCounter(s.length());
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
});
// Use the EditText's hint colors if we don't have one set
if (mDefaultTextColor == null) {
mDefaultTextColor = mEditText.getHintTextColors();
}
// If we do not have a valid hint, try and retrieve it from the EditText, if enabled
if (mHintEnabled && TextUtils.isEmpty(mHint)) {
setHint(mEditText.getHint());
// Clear the EditText's hint as we will display it ourselves
mEditText.setHint(null);
}
if (mCounterView != null) {
updateCounter(mEditText.getText().length());
}
if (mIndicatorArea != null) {
adjustIndicatorPadding();
}
// Update the label visibility with no animation
updateLabelState(false);
}
- 该方法里主要处理
- 监听EditText输入:在监听回调中,可以获取到EditText输入文本的长度,并且展示
private void updateCounter(int length) {
boolean wasCounterOverflowed = mCounterOverflowed;
if (mCounterMaxLength == INVALID_MAX_LENGTH) {
mCounterView.setText(String.valueOf(length));
mCounterOverflowed = false;
} else {
mCounterOverflowed = length > mCounterMaxLength;
if (wasCounterOverflowed != mCounterOverflowed) {
mCounterView.setTextAppearance(getContext(), mCounterOverflowed ?
mCounterOverflowTextAppearance : mCounterTextAppearance);
}
mCounterView.setText(getContext().getString(R.string.character_counter_pattern,
length, mCounterMaxLength));
}
if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) {
updateLabelState(false);
updateEditTextBackground();
}
}
- EidtText控件中hint隐藏文字在获取到焦点的时候,所做的网上动画效果:调用的方法就是updateLabelState()
private void updateLabelState(boolean animate) {
final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());//EditText文本是否为空
final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);//EditText是否获取到焦点
final boolean isErrorShowing = !TextUtils.isEmpty(getError());
if (mDefaultTextColor != null) {
mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor.getDefaultColor());
}
if (mCounterOverflowed && mCounterView != null) {
mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getCurrentTextColor());
} else if (isFocused && mFocusedTextColor != null) {
mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor.getDefaultColor());
} else if (mDefaultTextColor != null) {
mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor.getDefaultColor());
}
if (hasText || isFocused || isErrorShowing) {
// We should be showing the label so do so if it isn't already
collapseHint(animate);
} else {
// We should not be showing the label so hide it
expandHint(animate);
}
}
动画//关闭动画,如果有正在运行的动画,先关闭在开启
private void collapseHint(boolean animate) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (animate && mHintAnimationEnabled) {
animateToExpansionFraction(1f);
} else {
mCollapsingTextHelper.setExpansionFraction(1f);
}
}
private void expandHint(boolean animate) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (animate && mHintAnimationEnabled) {
animateToExpansionFraction(0f);
} else {
mCollapsingTextHelper.setExpansionFraction(0f);
}
}
private void animateToExpansionFraction(final float target) {
if (mCollapsingTextHelper.getExpansionFraction() == target) {
return;
}
if (mAnimator == null) {
mAnimator = ViewUtils.createAnimator();
mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
mAnimator.setDuration(ANIMATION_DURATION);
//动画监听
mAnimator.setUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animator) {
mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue());//最后交给了CollapsingTextHelp类去处理
}
});
}
mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
mAnimator.start();//开启动画
}
- CollapsingTextHelp类就是TextInputLayout的辅助类,主要用于展示EditText提示信息,和动画处理
//作为TextInputLayout的全局变量
private final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this);
- 设置提示信息
public void setHint(@Nullable CharSequence hint) {
if (mHintEnabled) {
setHintInternal(hint);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
}
private void setHintInternal(CharSequence hint) {
mHint = hint;
mCollapsingTextHelper.setText(hint);//交给CollapsingTextHelp处理
}
- setError()方法逻辑:内部会调用setErrorEnabled(true);
public void setError(@Nullable final CharSequence error) {
mError = error;
if (!mErrorEnabled) {
if (TextUtils.isEmpty(error)) {
// If error isn't enabled, and the error is empty, just return
return;
}
// Else, we'll assume that they want to enable the error functionality
setErrorEnabled(true);
}
// Only animate if we've been laid out already and we have a different error
final boolean animate = ViewCompat.isLaidOut(this)
&& !TextUtils.equals(mErrorView.getText(), error);
mErrorShown = !TextUtils.isEmpty(error);
// Cancel any on-going animation
ViewCompat.animate(mErrorView).cancel();
if (mErrorShown) {
mErrorView.setText(error);
mErrorView.setVisibility(VISIBLE);
if (animate) {
if (ViewCompat.getAlpha(mErrorView) == 1f) {
// If it's currently 100% show, we'll animate it from 0
ViewCompat.setAlpha(mErrorView, 0f);
}
//动画执行
ViewCompat.animate(mErrorView)
.alpha(1f)
.setDuration(ANIMATION_DURATION)
.setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationStart(View view) {
view.setVisibility(VISIBLE);
}
}).start();
} else {
// Set alpha to 1f, just in case
ViewCompat.setAlpha(mErrorView, 1f);
}
} else {
if (mErrorView.getVisibility() == VISIBLE) {
if (animate) {
ViewCompat.animate(mErrorView)
.alpha(0f)
.setDuration(ANIMATION_DURATION)
.setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationEnd(View view) {
mErrorView.setText(error);
view.setVisibility(INVISIBLE);
}
}).start();
} else {
mErrorView.setText(error);
mErrorView.setVisibility(INVISIBLE);
}
}
}
updateEditTextBackground();
updateLabelState(true);
}
- 16年最后一发