一個仿支付寶風格的密碼輸入框

這次帶來一個仿支付寶的支付密碼輸入框的控件,自己起了個名字叫AlipayEditText。先來看看效果:


                                                                 


首先告訴大家一個好消息,本次的控件不需要任何資源文件或佈局文件,就一個Java文件,複製粘貼即可用。話不多說,上控件本體:

AliPayEditText.java

public class AliPayEditText extends EditText {
    // 畫方框的畫筆
    private Paint boxPaint;

    // 畫密碼的畫筆(小圓點)
    private Paint passwdPaint;

    // 畫文字的畫筆
    private Paint textPaint;


    // 未輸入文字時的方框線寬
    private int normalLineWidth;

    // 未輸入文字時的方框顏色
    private int normalLineColor;

    // 輸入文字時的方框線寬
    private int editLineWidth;

    // 輸入文字時的方框顏色
    private int editLineColor;

    // 密碼(小圓點)顏色
    private int passwdColor;

    // 密碼(小圓點)半徑
    private int passwdRadius;

    // 輸入的文字大小
    private int textSize;

    // 輸入的文字顏色
    private int textColor;

    // 輸入最大長度
    private int maxLength;

    // 方框間距
    private int margin;

    // 震動時間
    private int vibratePeriod;

    // 震動幅度
    private int vibrateX;

    // 震動重複次數
    private int repeatCount;

    // 文字是否可見,爲false時是密碼模式,方框裏顯示的是小圓點
    private boolean textVisibility = true;

    // 文字是否加粗
    private boolean fakeBoldText = true;


    private int width;
    private int height;
    private String currentText = "";
    private Context context;

    private AnimatorSet animator;
    private int currentRepeatCount;
    private boolean isAnimationShowing;

    // 輸入回調
    private OnInputListener listener;

    public AliPayEditText(Context context) {
        this(context, null);
    }

    public AliPayEditText(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public AliPayEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    /**
     * 初始化方法
     * 初始化參數和默認設置
     *
     * @param context
     */
    private void init(Context context) {
        this.context = context;
        normalLineWidth = dp2px(1);
        normalLineColor = Color.parseColor("#dddddd");
        editLineWidth = dp2px(2);
        editLineColor = Color.parseColor("#000000");
        passwdRadius = dp2px(5);
        passwdColor = Color.parseColor("#000000");
        textSize = sp2px(16);
        textColor = Color.parseColor("#000000");
        maxLength = 6;
        margin = dp2px(10);
        repeatCount = 1;
        currentRepeatCount = 1;
        vibrateX = dp2px(10);
        vibratePeriod = 80;
        setFocusable(true);
        setBackgroundDrawable(null);
        setCursorVisible(false);
        setSingleLine();
        setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
        setTextColor(Color.parseColor("#00000000"));
        setHintTextColor(Color.parseColor("#00000000"));

        boxPaint = new Paint();
        boxPaint.setColor(normalLineColor);
        boxPaint.setStrokeWidth(normalLineWidth);
        passwdPaint = new Paint();
        passwdPaint.setColor(passwdColor);
        passwdPaint.setStrokeWidth(passwdRadius);
        textPaint = new Paint();
        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
        textPaint.setFakeBoldText(fakeBoldText);
        animator = new AnimatorSet();
    }

    /**
     * 很重要
     * 在佈局讀取完畢時重新計算輸入框的寬高
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getMeasuredWidth();
        height = (width - (maxLength - 1) * margin - Math.max(normalLineWidth, editLineWidth)) / maxLength;
        getLayoutParams().height = height + Math.max(normalLineWidth, editLineWidth);
    }

    /**
     * 初始化和每次輸入文字時重繪視圖
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawBox(canvas);
        drawText(canvas);
    }

    /**
     * 繪製方框
     *
     * @param canvas
     */
    private void drawBox(Canvas canvas) {
        // 循環繪製方框,個數與文字最大輸入數maxLength相同
        for (int i = 0; i < maxLength; i++) {
            boxPaint.setColor(i < currentText.length() ? editLineColor : normalLineColor);
            boxPaint.setStrokeWidth(i < currentText.length() ? editLineWidth : normalLineWidth);

            // 繪製方框左邊的線條
            canvas.drawLine(i * (height + margin) + (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, 0, i * (height + margin) + (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, height, boxPaint);
            // 繪製方框右邊的線條
            canvas.drawLine(height + i * (height + margin) - (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, 0, height + i * (height + margin) - (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, height, boxPaint);
            // 繪製方框上邊的線條
            canvas.drawLine(i * (height + margin), (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, height + i * (height + margin), (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, boxPaint);
            // 繪製方框下邊的線條
            canvas.drawLine(i * (height + margin), height - (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, height + i * (height + margin), height - (i < currentText.length() ? editLineWidth : normalLineWidth) / 2, boxPaint);
        }
    }

    /**
     * 繪製文字
     *
     * @param canvas
     */
    private void drawText(Canvas canvas) {
        for (int i = 0; i < currentText.length(); i++) {
            if (textVisibility) {
                // 非密碼模式畫文字
                float deltaX = textPaint.measureText(String.valueOf(currentText.charAt(i))) / 2;
                canvas.drawText(String.valueOf(currentText.charAt(i)), i * (height + margin) + height / 2 - deltaX, height / 2 + deltaX, textPaint);
            } else {
                // 密碼文字畫小圓點
                canvas.drawCircle(i * (height + margin) + height / 2, height / 2, passwdRadius, passwdPaint);
            }
        }
    }

    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        currentText = text.toString();
        invalidate();
        if (currentText.length() == maxLength) {
            if (!isAnimationShowing && listener != null) {
                listener.onFinish(currentText);
            }
        } else {
            if (!isAnimationShowing && listener != null) {
                listener.onInput(currentText, currentText.length());
            }
        }
    }

    /**
     * 震動
     * 在一些情況(比如密碼輸入錯誤)時讓整個輸入框震動
     */
    public void vibrate() {
        currentRepeatCount = 0;
        isAnimationShowing = true;
        ObjectAnimator leftAnimator = ObjectAnimator.ofFloat(this, "TranslationX", 0, -vibrateX);
        ObjectAnimator rightAnimator = ObjectAnimator.ofFloat(this, "TranslationX", -vibrateX, vibrateX);
        ObjectAnimator backAnimator = ObjectAnimator.ofFloat(this, "TranslationX", vibrateX, 0);
        leftAnimator.setDuration(vibratePeriod / 4);
        rightAnimator.setDuration(vibratePeriod / 2);
        backAnimator.setDuration(vibratePeriod / 4);
        leftAnimator.setInterpolator(new LinearInterpolator());
        rightAnimator.setInterpolator(new LinearInterpolator());
        backAnimator.setInterpolator(new LinearInterpolator());
        backAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (currentRepeatCount < repeatCount) {
                    animator.start();
                    currentRepeatCount++;
                } else {
                    isAnimationShowing = false;
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                isAnimationShowing = false;
            }
        });
        animator.play(rightAnimator).after(leftAnimator).before(backAnimator);
        animator.start();
    }

    /**
     * 設置未輸入文字時方框線寬
     *
     * @param normalLineWidth 線寬值,單位dp
     */
    public void setNormalLineWidth(int normalLineWidth) {
        this.normalLineWidth = dp2px(normalLineWidth);
        invalidate();
    }

    /**
     * 設置未輸入文字時方框顏色
     *
     * @param normalLineColor 顏色值
     */
    public void setNormalLineColor(int normalLineColor) {
        this.normalLineColor = normalLineColor;
        invalidate();
    }

    /**
     * 設置輸入文字時方框線寬
     *
     * @param editLineWidth 線寬值,單位dp
     */
    public void setEditLineWidth(int editLineWidth) {
        this.editLineWidth = dp2px(editLineWidth);
        invalidate();
    }

    /**
     * 設置輸入文字時方框顏色
     *
     * @param editLineColor 顏色值
     */
    public void setEditLineColor(int editLineColor) {
        this.editLineColor = editLineColor;
        invalidate();
    }

    /**
     * 設置密碼(小圓點)顏色
     *
     * @param passwdColor 顏色值
     */
    public void setPasswdColor(int passwdColor) {
        this.passwdColor = passwdColor;
        passwdPaint.setColor(passwdColor);
        invalidate();
    }

    /**
     * 設置密碼(小圓點)半徑
     *
     * @param passwdRadius 半徑值,單位dp
     */
    public void setPasswdRadius(int passwdRadius) {
        this.passwdRadius = dp2px(passwdRadius);
        passwdPaint.setStrokeWidth(passwdRadius);
        invalidate();
    }

    /**
     * 設置文字大小
     * 之所以方法名加個Draw是因爲setTextSize在TextView裏有同名方法
     *
     * @param textSize 大小值,單位sp
     */
    public void setDrawTextSize(int textSize) {
        this.textSize = sp2px(textSize);
        textPaint.setTextSize(this.textSize);
        invalidate();
    }

    /**
     * 設置文字顏色
     * 之所以方法名加個Draw是因爲setTextColor在TextView裏有同名方法
     *
     * @param textColor 顏色值
     */
    public void setDrawTextColor(int textColor) {
        this.textColor = textColor;
        textPaint.setColor(textColor);
        invalidate();
    }

    /**
     * 設置文字最大長度
     * 如果在佈局文件中設置了maxLength屬性,務必再設置一遍此方法,否則方框數默認爲6個
     *
     * @param maxLength 長度值,整數
     */
    public void setMaxLength(int maxLength) {
        this.maxLength = maxLength;
        setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
        requestLayout();
    }

    /**
     * 設置方框間距
     *
     * @param margin 間距值,單位dp
     */
    public void setMargin(int margin) {
        this.margin = dp2px(margin);
        requestLayout();
    }

    /**
     * 設置震動時間
     *
     * @param vibratePeriod 震動時間,單位毫秒
     */
    public void setVibratePeriod(int vibratePeriod) {
        this.vibratePeriod = vibratePeriod;
    }

    /**
     * 設置文字是否可見
     * 設置爲false時爲密碼模式,輸入顯示小圓點
     *
     * @param textVisibility
     */
    public void setTextVisibility(boolean textVisibility) {
        this.textVisibility = textVisibility;
        invalidate();
    }

    /**
     * 設置文字是否加粗,密碼模式下無效
     *
     * @param fakeBoldText
     */
    public void setFakeBoldText(boolean fakeBoldText) {
        this.fakeBoldText = fakeBoldText;
        textPaint.setFakeBoldText(fakeBoldText);
    }

    /**
     * 設置震動幅度,即震動偏離原始位置的最大距離
     *
     * @param vibrateX 震動幅度,單位dp
     */
    public void setVibrateX(int vibrateX) {
        this.vibrateX = dp2px(vibrateX);
    }

    /**
     * 設置震動重複次數
     * 左右擺動一個來回視爲重複一次
     *
     * @param repeatCount 重複次數,整數
     */
    public void setRepeatCount(int repeatCount) {
        this.repeatCount = repeatCount;
    }

    /**
     * 輸入回調接口
     */
    public interface OnInputListener {
        /**
         * 輸入事件發生且長度未達到最大長度時回調
         *
         * @param text          當前文字
         * @param currentLength 當前文字長度
         */
        void onInput(String text, int currentLength);

        /**
         * 輸入長度達到最大長度時回調
         *
         * @param text 當前文字
         */
        void onFinish(String text);
    }

    /**
     * 設置輸入完成時的回調接口
     *
     * @param listener
     */
    public void setOnInputListener(OnInputListener listener) {
        this.listener = listener;
    }

    /**
     * 重置輸入框
     * 調用後文字清空
     */
    public void reset() {
        setText("");
        invalidate();
    }

    private int dp2px(float dipValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    private int sp2px(float spValue) {
        float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}


簡單說一下寫控件的思路:既然是個輸入框自然要繼承自EditText,爲了實現顯示效果我們可以把EditText的背景設爲null,遊標隱藏掉,這樣EditText的本體就相當於看不見了;然後在這上面覆蓋一層視圖,手動畫上方框和文字來“偷樑換柱”顯示效果就達到了,具體實現方法大家可以看一看源碼的註釋。


然後說一說用法。其實用法非常的傻瓜式,把把控件本體複製到你項目裏任意的文件夾(最要項目有個專門的文件夾來裝這些自定義控件),然後在佈局文件裏就可以直接引用了:

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <com.min.alipayedittext.AliPayEditText
        android:id="@+id/edittext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp" />

</LinearLayout>

然後MainActivity裏什麼都不用寫:

MainActivity.java:

public class MainActivity extends Activity {
    private AliPayEditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initView();
    }

    private void initData() {

    }

    private void initView() {
        editText = (AliPayEditText) findViewById(R.id.edittext);
    }
}

運行一下,就能看到開頭展示的那個效果了。


當然如果什麼都不寫,輸入的時候是沒有交互的(比如在密碼輸入完成的時候給一個回調,你就可以判斷密碼輸的對不對)。添加幾行代碼:

private void initView() {
    ...

    editText.setOnInputListener(new AliPayEditText.OnInputListener() {
        @Override
        public void onInput(String text, int currentLength) {
            editText.setEditLineColor(Color.parseColor("#000000"));
        }

        @Override
        public void onFinish(String text) {
            if (text.equals("ILoveU")) {
                Toast.makeText(MainActivity.this, "哈哈", Toast.LENGTH_SHORT).show();
            } else {
                editText.setEditLineColor(Color.parseColor("#ff4500"));
                editText.vibrate();
            }
        }
    });
    
    ...
}

再次運行:

                                                                


是不是就有模有樣了?當然控件還支持更多的屬性設置,源碼裏註釋都寫得很清楚,大家可以自己試一試,這裏簡單做幾個示範,添加代碼:

private void initView() {
    ...
    
    editText.setMaxLength(8);
    editText.setTextVisibility(false);
    editText.setMargin(-1);

    ...
}

看一下效果:


至於EditText本身是一些屬性,比如inputType什麼的,自己在佈局文件裏設置一下就好了。

 

最後附上源碼地址:點擊打開鏈接

 

這次的內容就到這裏,我們下次再見。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章