大神GitHub地址:https://github.com/alphamu/PinEntryEditText
所有大神的注释都写的很少,整理一下属性如下:
<declare-styleable name="PinEntryEditText">
<!-- 字符动画 -->
<attr name="pinAnimationType" format="enum">
<enum name="popIn" value="0" />
<enum name="fromBottom" value="1" />
<enum name="none" value="-1" />
</attr>
<!-- 输入后密码字符 -->
<attr name="pinCharacterMask" format="string" />
<!-- 单元格默认文字 -->
<attr name="pinRepeatedHint" format="string" />
<!-- 底部线条尺寸 -->
<attr name="pinLineStroke" format="dimension" />
<!-- 底部线条选中时尺寸 -->
<attr name="pinLineStrokeSelected" format="dimension" />
<!-- 每个框之间的间隔 -->
<attr name="pinCharacterSpacing" format="dimension" />
<!-- 文字距离底部距离 -->
<attr name="pinTextBottomPadding" format="dimension" />
<!-- 底部线条颜色 -->
<attr name="pinLineColors" format="color" />
<!-- 背景样式 -->
<attr name="pinBackgroundDrawable" format="reference" />
<!-- 是否正方形 -->
<attr name="pinBackgroundIsSquare" format="boolean" />
</declare-styleable>
当设置背景样式之后,底部线条就会自动隐藏;不设置背景样式,则底部线条会自动显示。
分析一下代码(在原有的代码之上,我根据需求做了修改,然后省略了许多不重要的代码,不能直接复制粘贴使用):
public class PinPasswordEditText extends AppCompatEditText {
private static final String XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android";
private static final int DEFAULT_TEXT_LENGTH = 6; // 默认长度
public static final String DEFAULT_MASK = "\u25CF";
protected String mMask = null; // 输入后密码字符
protected StringBuilder mMaskChars = null;
protected String mSingleCharHint = null; // 未输入时默认文字
protected int mAnimatedType = 0; // 框中文字出现的动画
protected float mLineStroke = 1; // 未设置背景时,显示的底部线条
protected float mLineStrokeSelected = 1; //2dp by default
protected float mSpace = 8; // 每个框之间的间隔
protected float mTextBottomPadding = 8; // 文字距离底部距离
protected float mCharSize; // 每个单元格的尺寸
protected float mNumChars = DEFAULT_TEXT_LENGTH;
protected int mMaxLength = DEFAULT_TEXT_LENGTH;
protected RectF[] mLineCoords; // 所有单元格的底部分割线位置
protected float[] mCharBottom;
protected Paint mCharPaint;
protected Paint mLastCharPaint;
protected Paint mSingleCharPaint;
protected Drawable mPinBackground; // 背景样式
protected Rect mTextHeight = new Rect();
protected boolean mIsDigitSquare = false; // 是否正方形
protected int defaultColor = Color.parseColor("#6B767E");
protected OnClickListener mClickListener;
protected OnPinEnteredListener mOnPinEnteredListener = null;
protected Paint mLinesPaint;
protected boolean mAnimate = false;
protected boolean mHasError = false;
protected ColorStateList mOriginalTextColors; // 使输入框不同的点击状态显示不同的颜色
public PinPasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 获取自定义属性值
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PinEntryEditText, 0, 0);
try {
TypedValue outValue = new TypedValue();
ta.getValue(R.styleable.PinEntryEditText_pinAnimationType, outValue);
mAnimatedType = outValue.data;
mMask = ta.getString(R.styleable.PinEntryEditText_pinCharacterMask);
mSingleCharHint = ta.getString(R.styleable.PinEntryEditText_pinRepeatedHint);
mLineStroke = ta.getDimension(R.styleable.PinEntryEditText_pinLineStroke, mLineStroke);
mLineStrokeSelected = ta.getDimension(R.styleable.PinEntryEditText_pinLineStrokeSelected, mLineStrokeSelected);
mSpace = ta.getDimension(R.styleable.PinEntryEditText_pinCharacterSpacing, mSpace);
mTextBottomPadding = ta.getDimension(R.styleable.PinEntryEditText_pinTextBottomPadding, mTextBottomPadding);
mIsDigitSquare = ta.getBoolean(R.styleable.PinEntryEditText_pinBackgroundIsSquare, mIsDigitSquare);
mPinBackground = ta.getDrawable(R.styleable.PinEntryEditText_pinBackgroundDrawable);
ColorStateList colors = ta.getColorStateList(R.styleable.PinEntryEditText_pinLineColors);
if (colors != null) {
mColorStates = colors;
}
} finally {
ta.recycle();
}
// 获取系统的属性值
mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", DEFAULT_TEXT_LENGTH);
mNumChars = mMaxLength;
// 获取输入框文字高度
getPaint().getTextBounds("|", 0, 1, mTextHeight);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mIsDigitSquare) {
// 正方形
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
// If we want a square or circle pin box, we might be able
// to figure out the dimensions outselves
// if width and height are set to wrap_content or match_parent
if (widthMode == MeasureSpec.EXACTLY) {
// 如果宽度确定,高度就是宽度减去空格之后除以数量
measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
measuredHeight = (int) ((measuredWidth - (mNumChars - 1 * mSpace)) / mNumChars);
} else if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
measuredWidth = (int) ((measuredHeight * mNumChars) + (mSpace * mNumChars - 1));
} else if (widthMode == MeasureSpec.AT_MOST) {
measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
measuredHeight = (int) ((measuredWidth - (mNumChars - 1 * mSpace)) / mNumChars);
} else if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
measuredWidth = (int) ((measuredHeight * mNumChars) + (mSpace * mNumChars - 1));
} else {
measuredWidth = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
measuredHeight = (int) ((measuredWidth - (mNumChars - 1 * mSpace)) / mNumChars);
}
setMeasuredDimension(
resolveSizeAndState(measuredWidth, widthMeasureSpec, 1), resolveSizeAndState(measuredHeight, heightMeasureSpec, 0));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mOriginalTextColors = getTextColors();
int availableWidth = getWidth() - ViewCompat.getPaddingEnd(this) - ViewCompat.getPaddingStart(this);
if (mSpace < 0) {
// 计算每个单元格的尺寸
mCharSize = (availableWidth / (mNumChars * 2 - 1));
} else {
mCharSize = (availableWidth - (mSpace * (mNumChars - 1))) / mNumChars;
}
mLineCoords = new RectF[(int) mNumChars];
mCharBottom = new float[(int) mNumChars];
int startX;
int bottom = getHeight() - getPaddingBottom();
int rtlFlag;
final boolean isLayoutRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
if (isLayoutRtl) {
rtlFlag = -1;
startX = (int) (getWidth() - ViewCompat.getPaddingStart(this) - mCharSize);
} else {
rtlFlag = 1;
startX = ViewCompat.getPaddingStart(this);
}
for (int i = 0; i < mNumChars; i++) {
// 底部线条的位置信息
mLineCoords[i] = new RectF(startX, bottom, startX + mCharSize, bottom);
if (mPinBackground != null) {
if (mIsDigitSquare) {
mLineCoords[i].top = getPaddingTop();
mLineCoords[i].right = startX + mLineCoords[i].width();
} else {
mLineCoords[i].top -= mTextHeight.height() + mTextBottomPadding * 2;
}
}
if (mSpace < 0) {
startX += rtlFlag * mCharSize * 2;
} else {
startX += rtlFlag * (mCharSize + mSpace);
}
mCharBottom[i] = mLineCoords[i].bottom - mTextBottomPadding;
}
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
CharSequence text = getFullText();
int textLength = text.length(); // 输入框中文字长度
float[] textWidths = new float[textLength];
getPaint().getTextWidths(text, 0, textLength, textWidths);
float hintWidth = 0;
if (mSingleCharHint != null) {
float[] hintWidths = new float[mSingleCharHint.length()];
getPaint().getTextWidths(mSingleCharHint, hintWidths);
for (float i : hintWidths) {
hintWidth += i;
}
}
for (int i = 0; i < mNumChars; i++) {
//If a background for the pin characters is specified, it should be behind the characters.
if (mPinBackground != null) {
// 绘制每一个单元格的背景
updateDrawableState(i < textLength, i == textLength);
mPinBackground.setBounds((int) mLineCoords[i].left, (int) mLineCoords[i].top, (int) mLineCoords[i].right, (int) mLineCoords[i].bottom);
mPinBackground.draw(canvas);
}
float middle = mLineCoords[i].left + mCharSize / 2;
if (textLength > i) {
// 输入文字
if (!mAnimate || i != textLength - 1) {
// 不是输入的最后一位
canvas.drawText(text, i, i + 1, middle - textWidths[i] / 2, mCharBottom[i], mCharPaint);
} else {
canvas.drawText(text, i, i + 1, middle - textWidths[i] / 2, mCharBottom[i], mLastCharPaint);
}
} else if (mSingleCharHint != null) {
// 默认文字
canvas.drawText(mSingleCharHint, middle - hintWidth / 2, mCharBottom[i], mSingleCharPaint);
}
//The lines should be in front of the text (because that's how I want it).
if (mPinBackground == null) {
updateColorForLines(i <= textLength);
canvas.drawLine(mLineCoords[i].left, mLineCoords[i].top, mLineCoords[i].right, mLineCoords[i].bottom, mLinesPaint);
}
}
}
@Override
protected void onTextChanged(CharSequence text, final int start, int lengthBefore, final int lengthAfter) {
setError(false);
if (mLineCoords == null || !mAnimate) {
if (mOnPinEnteredListener != null && text.length() == mMaxLength) {
mOnPinEnteredListener.onPinEntered(text);
}
return;
}
if (mAnimatedType == -1) {
// 没有动画
invalidate();
return;
}
if (lengthAfter > lengthBefore) {
// 动画
if (mAnimatedType == 0) {
animatePopIn();
} else {
animateBottomUp(text, start);
}
}
}
private void animatePopIn() {
ValueAnimator va = ValueAnimator.ofFloat(1, getPaint().getTextSize());
va.setDuration(200);
va.setInterpolator(new OvershootInterpolator());
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLastCharPaint.setTextSize((Float) animation.getAnimatedValue());
PinPasswordEditText.this.invalidate();
}
});
if (getText().length() == mMaxLength && mOnPinEnteredListener != null) {
va.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mOnPinEnteredListener.onPinEntered(getText());
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
va.start();
}
private void animateBottomUp(CharSequence text, final int start) {
mCharBottom[start] = mLineCoords[start].bottom - mTextBottomPadding;
ValueAnimator animUp = ValueAnimator.ofFloat(mCharBottom[start] + getPaint().getTextSize(), mCharBottom[start]);
animUp.setDuration(300);
animUp.setInterpolator(new OvershootInterpolator());
animUp.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float value = (Float) animation.getAnimatedValue();
mCharBottom[start] = value;
PinPasswordEditText.this.invalidate();
}
});
mLastCharPaint.setAlpha(255);
ValueAnimator animAlpha = ValueAnimator.ofInt(0, 255);
animAlpha.setDuration(300);
animAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Integer value = (Integer) animation.getAnimatedValue();
mLastCharPaint.setAlpha(value);
}
});
AnimatorSet set = new AnimatorSet();
if (text.length() == mMaxLength && mOnPinEnteredListener != null) {
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mOnPinEnteredListener.onPinEntered(getText());
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
set.playTogether(animUp, animAlpha);
set.start();
}
public void setOnPinEnteredListener(OnPinEnteredListener l) {
mOnPinEnteredListener = l;
}
public interface OnPinEnteredListener {
// 输入完成回调
void onPinEntered(CharSequence str);
}
}