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

采用继承自 EditText 实现自定义 View 的方式。
实现步骤:
- 绘制外边框(直角或圆角)
- 绘制密码之间的分割线(竖线)
- 绘制实心圆代替输入的字符
- 对输入的字符进行监听,便于扩展处理
- 实现一些常用的外部接口方法调用
具体实现
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)
}
}
网友评论