Android驗證碼輸入框PinEntryEditText


大神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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章