美文网首页
微信支付宝的支付密码框的实现

微信支付宝的支付密码框的实现

作者: wuchao226 | 来源:发表于2019-06-19 17:49 被阅读0次

先看实现的支付密码输入框样式:

采用继承自 EditText 实现自定义 View 的方式。

实现步骤:

  1. 绘制外边框(直角或圆角)
  2. 绘制密码之间的分割线(竖线)
  3. 绘制实心圆代替输入的字符
  4. 对输入的字符进行监听,便于扩展处理
  5. 实现一些常用的外部接口方法调用

具体实现

1、绘制外边框

要想绘制边框我们首先要知道 View 的宽高,通过 onSizeChanged 方法去初始化宽高等数据,然后绘制圆角矩形。

 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //View 的宽
        mWidth = w
        //View 的高
        mHeight = h
        //第一个圆心的x坐标
        mStartX = w.toFloat() / mMaxCount.toFloat() / 2
        //第一个圆心的y坐标
        mStartY = h.toFloat() / 2
        //竖直分割线开始的坐标x
        mDivideLineStartX = w.toFloat() / mMaxCount.toFloat()
        //底部分割线的长度
        mBottomLineLength = w / (mMaxCount + 2)
        mRectF.set(0f, 0f, mWidth.toFloat(), mHeight.toFloat())
    }
RectF rectF = new RectF()
rectF.set(0, 0, width, height);
canvas?.drawRoundRect(mRectF, mRectAngle, mRectAngle, mRecBorderPaint!!)
2、绘制密码之间的分割线

既然是分割线肯定是等均分的,假设我们的密码最大输入maxCount=6,那么我们只需画5个分割线就可以了,分割线坐标的计算

计算分割线的起点和终点的坐标
 //循环画出每个密码间的分割线
        for (i in 0 until mMaxCount - 1) {
            canvas?.drawLine((i + 1) * mDivideLineStartX, 0f,
                    (i + 1) * mDivideLineStartX, mHeight.toFloat(),
                    mDivideLinePaint!!)
        }

运行效果:

另一种密码框样式

    /**
     * 画底部显示的分割线
     */
    private fun drawBottomBorder(canvas: Canvas?) {
        for (i in 0 until mMaxCount) {
            val cX = mStartX + i * 2 * mStartX
            canvas?.drawLine(
                cX - mBottomLineLength / 2, mHeight.toFloat(),
                cX + mBottomLineLength / 2, mHeight.toFloat(),
                mBottomLinePaint!!
            )
        }
    }

如下图:

3、绘制实心圆代替输入的字符

这里需要监听 EditView 的输入,重写 onTextChanged 方法获取输入字符的长度,然后计算每个圆圆心的坐标位置

//第一个圆心的x坐标
mStartX = w.toFloat() / mMaxCount.toFloat() / 2
//第一个圆心的y坐标
mStartY = h.toFloat() / 2

    /**
     * 画密码实心圆
     */
    private fun drawPwdCircle(canvas: Canvas?) {
        for (i in 0 until mTextLength) {
            //两个圆心之间的距离是 2 * mStartX,故每个圆心x坐标是 mStartX + i * 2 * mStartX
            canvas?.drawCircle(mStartX + i * 2 * mStartX, mStartY, mRadius, mCirclePaint!!)
        }
    }

    /**
     * 在Text改变过程中触发调用的,在原有的文本text中,从start开始的lengthAfter个字符替换长度为lengthBefore的旧文本
     * @param start 文本开始位置
     * @param lengthBefore 改变之前旧文本减少的数量
     * @param lengthAfter  新文本增加的数量
     */
    override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter)
       
        mTextLength = text?.length ?: 0
       
        invalidate()
    }

运行效果:


从图中可以看出是绘制了相应的实心圆,但是自带的底部线、光标、字符还在(有些手机没有底部线)

解决
首先给 view 设置一个透明的背景色,然后隐藏光标

this.setBackgroundColor(Color.TRANSPARENT)
//隐藏光标
this.isCursorVisible = false

我们发现输入的字符还在,问题出现在 onDraw 方法。只要把 EditView 内部重绘的方法干掉就行了。

    /**
     * 不注释的话在我们重写之前他已经调用了内部方法
     * 去绘制输入的字符了,
     * 我们在重写后虽然我们的方法生效了,
     * 但它的方法也生效了
     */
  override fun onDraw(canvas: Canvas?) {
        //不删除的话会默认绘制输入的文字
        //super.onDraw(canvas)
  }

完美解决,如下图:

4、对输入的字符进行监听,便于扩展处理
 /**
     * 在Text改变过程中触发调用的,在原有的文本text中,从start开始的lengthAfter个字符替换长度为lengthBefore的旧文本
     * @param start 文本开始位置
     * @param lengthBefore 改变之前旧文本减少的数量
     * @param lengthAfter  新文本增加的数量
     */
    override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter)
        this.position = start + lengthAfter
        mTextLength = text?.length ?: 0
        if (mTextLength == mMaxCount) {
            if (mListener != null) {
                if (TextUtils.isEmpty(mComparePassword)) {
                    mListener!!.inputFinished(getPassword())
                } else {
                    if (TextUtils.equals(mComparePassword, getPassword())) {
                        mListener!!.onEqual(getPassword())
                    } else {
                        mListener!!.onDifference(mComparePassword!!, getPassword())
                    }
                }
            }
        }
        invalidate()
    }
5、实现一些常用的外部接口方法调用
 /**
     * 密码比较监听
     */
    interface OnPasswordListener {
        /**
         * 两次密码输入不同
         * @param oldPwd 旧密码
         * @param newPwd 新密码
         */
        fun onDifference(oldPwd: String, newPwd: String)

        /**
         * 密码相同
         */
        fun onEqual(pwd: String)

        /**
         * 密码第一次输入完成,并储存起来给mComparePassword,用来下次输入密码时比较
         */
        fun inputFinished(inputPwd: String)
    }
使用方式

XML 布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:psd="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.wuc.alipay.PayPassWordInputView
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_margin="10dp"
        android:inputType="number"
        psd:maxCount="6"
        psd:psdType="normal"
        psd:rectAngle="4dp"/>

    <com.wuc.alipay.PayPassWordInputView
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_margin="10dp"
        android:layout_marginTop="20dp"
        android:inputType="number"
        psd:maxCount="6"
        psd:psdType="bottomLine"
        psd:rectAngle="4dp"/>
</androidx.appcompat.widget.LinearLayoutCompat>

Activity 中使用:

class PayPsdViewActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_pay_psd_view)
        password.setOnPasswordListener(object : PayPassWordInputView.OnPasswordListener {
            override fun onDifference(oldPwd: String, newPwd: String) {
                //和上次输入的密码不一致  做相应的业务逻辑处理
                toast("两次密码输入不同")
            }

            override fun onEqual(pwd: String) {
                //两次输入密码相同,那就去进行支付
                toast("密码相同")
            }

            override fun inputFinished(inputPwd: String) {
                toast("输入完毕:$inputPwd")
                password.setComparePassword(inputPwd)
            }

        })
    }
}

上述代码中 toast 的使用需要依赖:

ext.anko_version='0.10.8'
// Anko Commons
implementation "org.jetbrains.anko:anko-commons:$anko_version"

PayPassWordInputView 源码实现

//values 目录下的 attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PayPassWordInputView">
        <attr name="maxCount" format="integer"/>
        <attr name="circleColor" format="color"/>
        <attr name="bottomLineColor" format="color"/>
        <attr name="radius" format="dimension"/>
        <attr name="divideLineWidth" format="dimension"/>
        <attr name="divideLineColor" format="color"/>
        <attr name="rectAngle" format="dimension"/>
        <attr name="focusedColor" format="color"/>
        <attr name="psdType" format="enum">
            <enum name="normal" value="0"/>
            <enum name="bottomLine" value="1"/>
        </attr>
    </declare-styleable>
</resources>
/**
 * @desciption: 自定义支付密码输入框
 */
class PayPassWordInputView : EditText {

    /**
     * 第一个圆开始绘制的圆心坐标
     */
    private var mStartX: Float = 0f
    private var mStartY: Float = 0f
    /**
     * 实心圆的半径
     */
    private var mRadius: Float = 10f

    /**
     * View 的宽高
     */
    private var mHeight: Int = 0
    private var mWidth: Int = 0
    /**
     * 当前输入密码位数
     */
    private var mTextLength: Int = 0
    /**
     * 底部分割线的长度
     */
    private var mBottomLineLength: Int = 0
    /**
     * 最大输入位数  默认 6
     */
    private var mMaxCount: Int = 6

    /**
     *  矩形边框的颜色
     */
    private var mRecBorderColor: Int = Color.GRAY
    /**
     * 圆的颜色   默认BLACK
     */
    private var mCircleColor: Int = Color.BLACK
    /**
     * 底部线的颜色   默认GRAY
     */
    private var mBottomLineColor: Int = Color.GRAY

    /**
     * 竖直分割线开始的坐标x
     */
    private var mDivideLineStartX: Float = 0f
    /**
     * 竖直分割线宽度   默认 2
     */
    private var mDivideLineWidth: Float = 2f
    /**
     * 竖直分割线的颜色
     */
    private var mDivideLineColor: Int = Color.GRAY
    /**
     * 获取焦点时的颜色
     */
    private var mFocusedColor = Color.BLUE
    /**
     * 密码矩形框
     */
    private var mRectF = RectF()
    /**
     * 密码输入焦点矩形框
     */
    private var mFocusedRectF = RectF()
    /**
     * 矩形边框的圆角
     */
    private var mRectAngle: Float = 4f

    /**
     * 竖直分割线画笔
     */
    private var mDivideLinePaint: Paint? = null
    /**
     * 输入密码焦点矩形边框的画笔
     */
    private var mFocusedPaint: Paint? = null
    /**
     * 矩形边框的画笔
     */
    private var mRecBorderPaint: Paint? = null
    /**
     * 圆的画笔
     */
    private var mCirclePaint: Paint? = null
    /**
     * 底部线的画笔
     */
    private var mBottomLinePaint: Paint? = null

    /**
     * 需要对比的密码  一般为上次输入的
     */
    private var mComparePassword: String? = null
    /**
     * 当前输入的位置索引
     */
    private var position: Int = 0
    /**
     * 密码框类型
     */
    private var mPsdType = 0
    /**
     * 有矩形边框的密码框类型(类似微信密码框)
     */
    private val psdType_normal = 0
    /**
     * 只有底部分割线的密码框类型
     */
    private val psdType_bottomLine = 1
    /**
     * 密码比较监听
     */
    private var mListener: OnPasswordListener? = null

    constructor(mContext: Context) : this(mContext, null)
    constructor(mContext: Context, attrs: AttributeSet?) : this(mContext, attrs, 0)
    constructor(mContext: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(mContext, attrs, defStyleAttr) {
        init(mContext, attrs)
    }

    private fun init(mContext: Context, attrs: AttributeSet?) {
        getInitAttrs(mContext, attrs)
        initPaint()
        this.setBackgroundColor(Color.TRANSPARENT)
        //隐藏光标
        this.isCursorVisible = false
    }

    /**
     * 获取自定义属性
     */
    private fun getInitAttrs(context: Context, attrs: AttributeSet?) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PayPassWordInputView)
        //最大输入位数  默认 6
        mMaxCount = typedArray.getInt(R.styleable.PayPassWordInputView_maxCount, mMaxCount)
        //圆的颜色   默认BLACK
        mCircleColor = typedArray.getColor(R.styleable.PayPassWordInputView_circleColor, mCircleColor)
        //底部线的颜色   默认GRAY
        mBottomLineColor = typedArray.getColor(R.styleable.PayPassWordInputView_bottomLineColor, mBottomLineColor)
        //实心圆的半径
        mRadius = typedArray.getDimension(R.styleable.PayPassWordInputView_radius, mRadius)

        //竖直分割线宽度   默认 2
        mDivideLineWidth = typedArray.getDimension(R.styleable.PayPassWordInputView_divideLineWidth, mDivideLineWidth)
        //竖直分割线的颜色
        mDivideLineColor = typedArray.getColor(R.styleable.PayPassWordInputView_divideLineColor, mDivideLineColor)
        //密码框类型
        mPsdType = typedArray.getInt(R.styleable.PayPassWordInputView_psdType, mPsdType)
        //矩形边框的圆角
        mRectAngle = typedArray.getDimension(R.styleable.PayPassWordInputView_rectAngle, mRectAngle)
        //获取焦点时的颜色
        mFocusedColor = typedArray.getColor(R.styleable.PayPassWordInputView_focusedColor, mFocusedColor)
        typedArray.recycle()
    }

    private fun initPaint() {
        //圆的画笔
        mCirclePaint = getInitPaint(5f, Paint.Style.FILL, mCircleColor)
        //底部线的画笔
        mBottomLinePaint = getInitPaint(2f, Paint.Style.FILL, mBottomLineColor)
        //竖直分割线画笔
        mDivideLinePaint = getInitPaint(mDivideLineWidth, Paint.Style.FILL, mDivideLineColor)
        //底部分割线的画笔
        mRecBorderPaint = getInitPaint(3f, Paint.Style.STROKE, mRecBorderColor)
        //输入密码焦点矩形边框的画笔
        mFocusedPaint = getInitPaint(3f, Paint.Style.STROKE, mFocusedColor)
    }

    /**
     * 设置画笔
     * @param strokeWidth 画笔宽度
     * @param style 画笔风格
     * @param color 画笔颜色
     */
    private fun getInitPaint(strokeWidth: Float, style: Paint.Style, color: Int): Paint {
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        paint.strokeWidth = strokeWidth
        paint.style = style
        paint.color = color
        paint.isAntiAlias = true
        return paint
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        //View 的宽
        mWidth = w
        //View 的高
        mHeight = h
        //第一个圆心的x坐标
        mStartX = w.toFloat() / mMaxCount.toFloat() / 2
        //第一个圆心的y坐标
        mStartY = h.toFloat() / 2
        //竖直分割线开始的坐标x
        mDivideLineStartX = w.toFloat() / mMaxCount.toFloat()
        //底部分割线的长度
        mBottomLineLength = w / (mMaxCount + 2)
        mRectF.set(0f, 0f, mWidth.toFloat(), mHeight.toFloat())
    }

    /**
     * 画微信支付密码样式
     */
    private fun drawNormalBorder(canvas: Canvas?) {
        canvas?.drawRoundRect(mRectF, mRectAngle, mRectAngle, mRecBorderPaint!!)
        //循环画出每个密码间的分割线
        for (i in 0 until mMaxCount - 1) {
            canvas?.drawLine(
                (i + 1) * mDivideLineStartX, 0f,
                (i + 1) * mDivideLineStartX, mHeight.toFloat(),
                mDivideLinePaint!!
            )
        }
    }

    /**
     * 画密码实心圆
     */
    private fun drawPwdCircle(canvas: Canvas?) {
        for (i in 0 until mTextLength) {
            //两个圆心之间的距离是 2 * mStartX,故每个圆心x坐标是 mStartX + i * 2 * mStartX
            canvas?.drawCircle(mStartX + i * 2 * mStartX, mStartY, mRadius, mCirclePaint!!)
        }

    }

    /**
     * 画底部显示的分割线
     */
    private fun drawBottomBorder(canvas: Canvas?) {
        for (i in 0 until mMaxCount) {
            val cX = mStartX + i * 2 * mStartX
            canvas?.drawLine(
                cX - mBottomLineLength / 2, mHeight.toFloat(),
                cX + mBottomLineLength / 2, mHeight.toFloat(),
                mBottomLinePaint!!
            )
        }
    }

    /**
     * 画矩形框密码框获取焦点时的矩形框
     */
    private fun drawItemFocused(canvas: Canvas?, position: Int) {
        if (position > mMaxCount - 1) {
            return
        }
        mFocusedRectF.set(
            position * mDivideLineStartX, 0f
            , (position + 1) * mDivideLineStartX, mHeight.toFloat()
        )
        canvas?.drawRoundRect(mFocusedRectF, mRectAngle, mRectAngle, mFocusedPaint!!)
    }

    /**
     * 在Text改变过程中触发调用的,在原有的文本text中,从start开始的lengthAfter个字符替换长度为lengthBefore的旧文本
     * @param start 文本开始位置
     * @param lengthBefore 改变之前旧文本减少的数量
     * @param lengthAfter  新文本增加的数量
     */
    override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter)
        this.position = start + lengthAfter
        mTextLength = text?.length ?: 0
        if (mTextLength == mMaxCount) {
            if (mListener != null) {
                if (TextUtils.isEmpty(mComparePassword)) {
                    mListener!!.inputFinished(getPassword())
                } else {
                    if (TextUtils.equals(mComparePassword, getPassword())) {
                        mListener!!.onEqual(getPassword())
                    } else {
                        mListener!!.onDifference(mComparePassword!!, getPassword())
                    }
                }
            }
        }
        invalidate()
    }

    override fun onSelectionChanged(selStart: Int, selEnd: Int) {
        super.onSelectionChanged(selStart, selEnd)
        //保证光标始终在最后
        if (selStart == selEnd) {
            setSelection(text.length)
        }
    }


    override fun onDraw(canvas: Canvas?) {
        //不删除的话会默认绘制输入的文字
        //super.onDraw(canvas)
        when (mPsdType) {
            psdType_normal -> {
                drawNormalBorder(canvas)
                drawItemFocused(canvas, position)
            }
            psdType_bottomLine ->
                drawBottomBorder(canvas)
        }
        drawPwdCircle(canvas)
    }

    /**
     * 获取输入的密码
     */
    fun getPassword(): String {
        return text.toString().trim()
    }

    /**
     * 清空密码
     */
    fun clearPassword() {
        setText("")
    }

    /**
     * 设置密码比较监听器
     */
    fun setOnPasswordListener(listener: OnPasswordListener) {
        mListener = listener
    }

    /**
     * 设置输入完成的密码及密码比较监听器
     */
    fun setComparePassword(comparePassword: String, listener: OnPasswordListener) {
        mComparePassword = comparePassword
        mListener = listener
    }

    /**
     * 设置输入完成的密码
     */
    fun setComparePassword(comparePassword: String) {
        mComparePassword = comparePassword
    }

    /**
     * 密码比较监听
     */
    interface OnPasswordListener {
        /**
         * 两次密码输入不同
         * @param oldPwd 旧密码
         * @param newPwd 新密码
         */
        fun onDifference(oldPwd: String, newPwd: String)

        /**
         * 密码相同
         */
        fun onEqual(pwd: String)

        /**
         * 密码第一次输入完成,并储存起来给mComparePassword,用来下次输入密码时比较
         */
        fun inputFinished(inputPwd: String)
    }
}

相关文章

网友评论

      本文标题:微信支付宝的支付密码框的实现

      本文链接:https://www.haomeiwen.com/subject/ybokqctx.html