大神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);
}
}