一个仿支付宝风格的密码输入框

这次带来一个仿支付宝的支付密码输入框的控件,自己起了个名字叫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什么的,自己在布局文件里设置一下就好了。

 

最后附上源码地址:点击打开链接

 

这次的内容就到这里,我们下次再见。





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