Android开发自定义短信验证码实现过程详解
作者:龙之音
效果图
简介
基本上只要需要登录的APP,都会有验证码输入,所以说是比较常用的控件,而且花样也是很多的,这里列出来4种样式,分别是:
- 表格类型
- 方块类型
- 横线类型
- 圈圈类型
其实还有很多其他的样式,但是这四种是我遇到最多的样式,所以特地拿来实现下,网上有很多类似的轮子,实现方式也是蛮多的,比如说:
- 组合控件(线性布局添加子View)
- 自定义ViewGrop
- 自定义View
- ...
自己看了些网络上实现的方案,参考了一些比较好的方式,这里先来分析下这个控件有哪些功能,再决定实现方案。
功能分析
- 1、默认状态样式展示
- 2、支持设置最大数量
- 3、支持4种类型样式
- 4、点击控件,弹出键盘,获取焦点,显示焦点样式。焦点失去,展示默认样式。
- 5、输入数据,焦点移动到下一个位置,删除数据,焦点也跟随移动
通过功能4让我第一想到的就是EditText控件,那么怎么做呢?大家都知道EditText有自己的样式和操作,如果我们可以屏蔽无用的样式和功能,留下我们需要的不就可以了吗。
[图片上传失败...(image-888083-1663139918159)]
EditText
- 点击EditText可以弹出键盘(需要的),并获取焦点(需要的),显示光标(不需要的)
- 长按EditText会显示复制,粘贴等操作(不需要的)
- 输入数据,内容默认显示(不需要的)
上面对EditText基本使用时出现的样式和操作,有的是需要的,有的是不需要的,我们可以对不需要的进行屏蔽,来代码走起。
代码实现
1、创建CodeEditText
继承AppCompatEditText,并屏蔽一些功能。
class CodeEditText @JvmOverloads constructor(context: Context, var attrs: AttributeSet, var defStyleAttr: Int = 0) : AppCompatEditText(context, attrs, defStyleAttr) { init { initSetting() } private fun initSetting() { //内容默认显示(不需要的)- 文字设置透明 setTextColor(Color.TRANSPARENT) //触摸获取焦点 isFocusableInTouchMode = true //不显示光标 isCursorVisible = false //屏蔽长按操作 setOnLongClickListener { true } } }
2、创建自定义配置参数
这里根据样式,列举一些参数,如果需要其他参数可以自行添加
<declare-styleable name="CodeEditText"> <!--code模式--> <attr name="code_mode" format="enum"> <!--文字--> <enum name="text" value="0" /> <!--TODO 拓展--> </attr> <!--code样式--> <attr name="code_style" format="enum"> <!--表格--> <enum name="form" value="0" /> <!--方块--> <enum name="rectangle" value="1" /> <!--横线--> <enum name="line" value="2" /> <!--圆形--> <enum name="circle" value="3" /> <!--TODO 拓展--> </attr> <!--code背景色--> <attr name="code_bg_color" format="color" /> <!--边框宽度--> <attr name="code_border_width" format="dimension" /> <!--边框默认颜色--> <attr name="code_border_color" format="color" /> <!--边框选中颜色--> <attr name="code_border_select_color" format="color" /> <!--边框圆角--> <attr name="code_border_radius" format="dimension" /> <!--code 内容颜色(密码或文字)--> <attr name="code_content_color" format="color" /> <!--code 内容大小(密码或文字)--> <attr name="code_content_size" format="dimension" /> <!--code 单个宽度--> <attr name="code_item_width" format="dimension" /> <!--code Item之间的间隙--> <attr name="code_item_space" format="dimension" /> </declare-styleable>
3、获取自定义配置参数
这里获取参数,有的参数默认给了默认值。
private fun initAttrs() { val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.CodeEditText) codeMode = obtainStyledAttributes.getInt(R.styleable.CodeEditText_code_mode, 0) codeStyle = obtainStyledAttributes.getInt(R.styleable.CodeEditText_code_style, 0) borderWidth = obtainStyledAttributes.getDimension(R.styleable.CodeEditText_code_border_width, DensityUtil.dip2px(context, 1.0f)) borderColor = obtainStyledAttributes.getColor(R.styleable.CodeEditText_code_border_color, Color.GRAY) borderSelectColor = obtainStyledAttributes.getColor(R.styleable.CodeEditText_code_border_select_color, Color.GRAY) borderRadius = obtainStyledAttributes.getDimension(R.styleable.CodeEditText_code_border_radius, 0f) codeBgColor = obtainStyledAttributes.getColor(R.styleable.CodeEditText_code_bg_color, Color.WHITE) codeItemWidth = obtainStyledAttributes.getDimension(R.styleable.CodeEditText_code_item_width, -1f).toInt() codeItemSpace = obtainStyledAttributes.getDimension(R.styleable.CodeEditText_code_item_space, DensityUtil.dip2px(context, 16f)) if (codeStyle == 0) codeItemSpace = 0f codeContentColor = obtainStyledAttributes.getColor(R.styleable.CodeEditText_code_content_color, Color.GRAY) codeContentSize = obtainStyledAttributes.getDimension(R.styleable.CodeEditText_code_content_size, DensityUtil.dip2px(context, 16f)) obtainStyledAttributes.recycle() }
4、重写 onDraw 方法
override fun onDraw(canvas: Canvas) { super.onDraw(canvas) //当前索引(待输入的光标位置) currentIndex = text?.length ?: 0 //Item宽度(这里判断如果设置了宽度并且合理就使用当前设置的宽度,否则平均计算) codeItemWidth = if (codeItemWidth != -1 && (codeItemWidth * maxLength + codeItemSpace * (maxLength - 1)) <= measuredWidth) { codeItemWidth } else { ((measuredWidth - codeItemSpace * (maxLength - 1)) / maxLength).toInt() } //计算左右间距大小 space = ((measuredWidth - codeItemWidth * maxLength - codeItemSpace * (maxLength - 1)) / 2).toInt() //绘制Code样式 when (codeStyle) { //表格 0 -> { drawFormCode(canvas) } //方块 1 -> { drawRectangleCode(canvas) } //横线 2 -> { drawLineCode(canvas) } //圆形 3 -> { drawCircleCode(canvas) } } //绘制文字 drawContentText(canvas) }
在onDraw方法中主要是根据设置的codeStyle样式,绘制不同的样子。在绘制之前,主要做了三个操作。
- 对当前焦点索引currentIndex的计算
- 单个验证码宽度codeItemWidth的计算
- 第一个验证码距离左边的间距space的计算
对当前焦点索引currentIndex的计算
这里巧妙的使用了获取当前EditText数据的长度作为当前索引值,比如说,开始没有输入数据,获取长度为0,则当前焦点应该在0索引位置上,当输入一个数据时,数据长度为1,则焦点变为1,焦点相当于移动到了索引1的位置上,删除数据同理,这样就达到了上面分析的 ”功能5“的效果。
单个验证码宽度codeItemWidth的计算
这里因为有4中样式,有的是表格一体展示,有的是分开展示,比如方块、横线、圈圈,这三种中间是有空隙的,这个空隙大小我们做了配置参数code_item_space,对于这个参数,表格样式是不需要的,所以不管你设置了还是没有设置,在表格样式中是无效的。所以这里做了统一计算。
第一个验证码距离左边的间距space的计算
因为需要绘制,所以需要起始点,那么起点应该是:(控件总宽度-所有验证码的宽度-所有验证码之前的空隙)/2 .
5、绘制表格样式
/** * 表格code */ private fun drawFormCode(canvas: Canvas) { //绘制表格边框 defaultDrawable.setBounds(space, 0, measuredWidth - space, measuredHeight) defaultBitmap = CodeHelper.drawableToBitmap(defaultDrawable, measuredWidth - 2 * space, measuredHeight) canvas.drawBitmap(defaultBitmap!!, space.toFloat(), 0f, mLinePaint) //绘制表格中间分割线 for (i in 1 until maxLength) { val startX = space + codeItemWidth * i + codeItemSpace * i val startY = 0f val stopY = measuredHeight canvas.drawLine(startX, startY, startX, stopY.toFloat(), mLinePaint) } //绘制当前位置边框 for (i in 0 until maxLength) { if (currentIndex != -1 && currentIndex == i && isCodeFocused) { when (i) { 0 -> { val radii = floatArrayOf(borderRadius, borderRadius, 0f, 0f, 0f, 0f, borderRadius, borderRadius) currentDrawable.cornerRadii = radii currentBitmap = CodeHelper.drawableToBitmap(currentDrawable, (codeItemWidth + borderWidth / 2).toInt(), measuredHeight) } maxLength - 1 -> { val radii = floatArrayOf(0f, 0f, borderRadius, borderRadius, borderRadius, borderRadius, 0f, 0f) currentDrawable.cornerRadii = radii currentBitmap = CodeHelper.drawableToBitmap(currentDrawable, (codeItemWidth + borderWidth / 2 + codeItemSpace).toInt(), measuredHeight) } else -> { val radii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f) currentDrawable.cornerRadii = radii currentBitmap = CodeHelper.drawableToBitmap(currentDrawable, (codeItemWidth + borderWidth).toInt(), measuredHeight) } } val left = if (i == 0) (space + codeItemWidth * i) else ((space + codeItemWidth * i + codeItemSpace * i) - borderWidth / 2) canvas.drawBitmap(currentBitmap!!, left.toFloat(), 0f, mLinePaint) } } }
6、绘制方块样式
/** * 方块 code */ private fun drawRectangleCode(canvas: Canvas) { defaultDrawable.cornerRadius = borderRadius defaultBitmap = CodeHelper.drawableToBitmap(defaultDrawable, codeItemWidth, measuredHeight) currentDrawable.cornerRadius = borderRadius currentBitmap = CodeHelper.drawableToBitmap(currentDrawable, codeItemWidth, measuredHeight) for (i in 0 until maxLength) { val left = if (i == 0) { space + i * codeItemWidth } else { space + i * codeItemWidth + codeItemSpace * i } //当前光标样式 if (currentIndex != -1 && currentIndex == i && isCodeFocused) { canvas.drawBitmap(currentBitmap!!, left.toFloat(), 0f, mLinePaint) } //默认样式 else { canvas.drawBitmap(defaultBitmap!!, left.toFloat(), 0f, mLinePaint) } } }
7、绘制横线样式
/** * 横线 code */ private fun drawLineCode(canvas: Canvas) { for (i in 0 until maxLength) { //当前选中状态 if (currentIndex == i && isCodeFocused) { mLinePaint.color = borderSelectColor } //默认状态 else { mLinePaint.color = borderColor } val startX: Float = space + codeItemWidth * i + codeItemSpace * i val startY: Float = measuredHeight - borderWidth val stopX: Float = startX + codeItemWidth val stopY: Float = startY canvas.drawLine(startX, startY, stopX, stopY, mLinePaint) } }
8、绘制圈圈样式
/** * 圆形 code */ private fun drawCircleCode(canvas: Canvas) { for (i in 0 until maxLength) { //当前绘制的圆圈的左x轴坐标 var left: Float = if (i == 0) { (space + i * codeItemWidth).toFloat() } else { space + i * codeItemWidth + codeItemSpace * i } //圆心坐标 val cx: Float = left + codeItemWidth / 2f val cy: Float = measuredHeight / 2f //圆形半径 val radius: Float = codeItemWidth / 5f //默认样式 if (i >= currentIndex) { canvas.drawCircle(cx, cy, radius, mLinePaint.apply { style = Paint.Style.FILL }) } } }
9、绘制输入数据展示
/** * 绘制内容 */ private fun drawContentText(canvas: Canvas) { val textStr = text.toString() for (i in 0 until maxLength) { if (textStr.isNotEmpty() && i < textStr.length) { when (codeMode) { //文字 0 -> { val code: String = textStr[i].toString() val textWidth: Float = mTextPaint.measureText(code) val textHeight: Float = CodeHelper.getTextHeight(code, mTextPaint) val x: Float = space + codeItemWidth * i + codeItemSpace * i + (codeItemWidth - textWidth) / 2 val y: Float = (measuredHeight + textHeight) / 2f canvas.drawText(code, x, y, mTextPaint) } //TODO 拓展 } } } }
上面就是对四种样式的绘制,主要考察的API如下:
- canvas.drawBitmap()
- canvas.drawLine()
- canvas.drawCircle()
- canvas.drawText()
主要对这四个API的使用数据上的计算,相对比较的简单,其中有个点击获取焦点以及失去焦点更新样式方式:
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { super.onFocusChanged(focused, direction, previouslyFocusedRect) isCodeFocused = focused invalidate() }
通过isCodeFocused字段来控制。
10、控件使用
<com.yxlh.androidxy.demo.ui.codeet.widget.CodeEditText android:layout_width="match_parent" android:layout_height="50dp" android:layout_marginLeft="20dp" android:layout_marginTop="40dp" android:layout_marginRight="20dp" android:inputType="number" android:maxLength="4" app:code_border_color="@android:color/darker_gray" app:code_border_radius="5dp" app:code_border_select_color="@color/design_default_color_primary" app:code_border_width="2dp" app:code_content_color="@color/purple_500" app:code_content_size="35dp" app:code_item_width="50dp" app:code_mode="text" app:code_bg_color="#E1E1E1" app:code_style="rectangle" />
GitHub链接:https://github.com/yixiaolunhui/AndroidXY
以上就是Android开发自定义短信验证码实现过程详解的详细内容,更多关于Android自定义短信验证码的资料请关注脚本之家其它相关文章!