效果圖
簡介
基本上只要需要登錄的APP寡夹,都會(huì)有驗(yàn)證碼輸入暂刘,所以說是比較常用的控件,而且花樣也是很多的勺美,這里列出來4種樣式,分別是:
- 表格類型
- 方塊類型
- 橫線類型
- 圈圈類型
其實(shí)還有很多其他的樣式碑韵,但是這四種是我遇到最多的樣式赡茸,所以特地拿來實(shí)現(xiàn)下,網(wǎng)上有很多類似的輪子祝闻,實(shí)現(xiàn)方式也是蠻多的占卧,比如說:
- 組合控件(線性布局添加子View)
- 自定義ViewGrop
- 自定義View
- ...
自己看了些網(wǎng)絡(luò)上實(shí)現(xiàn)的方案,參考了一些比較好的方式联喘,這里先來分析下這個(gè)控件有哪些功能华蜒,再?zèng)Q定實(shí)現(xiàn)方案。
功能分析
- 1豁遭、默認(rèn)狀態(tài)樣式展示
- 2叭喜、支持設(shè)置最大數(shù)量
- 3、支持4種類型樣式
- 4蓖谢、點(diǎn)擊控件捂蕴,彈出鍵盤,獲取焦點(diǎn)蜈抓,顯示焦點(diǎn)樣式启绰。焦點(diǎn)失去,展示默認(rèn)樣式沟使。
- 5委可、輸入數(shù)據(jù),焦點(diǎn)移動(dòng)到下一個(gè)位置腊嗡,刪除數(shù)據(jù)着倾,焦點(diǎn)也跟隨移動(dòng)
通過功能4讓我第一想到的就是EditText控件,那么怎么做呢燕少?大家都知道EditText有自己的樣式和操作卡者,如果我們可以屏蔽無用的樣式和功能,留下我們需要的不就可以了嗎客们。
[圖片上傳失敗...(image-888083-1663139918159)]
EditText
- 點(diǎn)擊EditText可以彈出鍵盤(需要的)崇决,并獲取焦點(diǎn)(需要的)材诽,顯示光標(biāo)(不需要的)
- 長按EditText會(huì)顯示復(fù)制,粘貼等操作(不需要的)
- 輸入數(shù)據(jù)恒傻,內(nèi)容默認(rèn)顯示(不需要的)
上面對(duì)EditText基本使用時(shí)出現(xiàn)的樣式和操作脸侥,有的是需要的,有的是不需要的盈厘,我們可以對(duì)不需要的進(jìn)行屏蔽睁枕,來代碼走起。
代碼實(shí)現(xiàn)
1沸手、創(chuàng)建CodeEditText
繼承AppCompatEditText外遇,并屏蔽一些功能。
class CodeEditText @JvmOverloads constructor(context: Context, var attrs: AttributeSet, var defStyleAttr: Int = 0) :
AppCompatEditText(context, attrs, defStyleAttr) {
init {
initSetting()
}
private fun initSetting() {
//內(nèi)容默認(rèn)顯示(不需要的)- 文字設(shè)置透明
setTextColor(Color.TRANSPARENT)
//觸摸獲取焦點(diǎn)
isFocusableInTouchMode = true
//不顯示光標(biāo)
isCursorVisible = false
//屏蔽長按操作
setOnLongClickListener { true }
}
}
2契吉、創(chuàng)建自定義配置參數(shù)
這里根據(jù)樣式跳仿,列舉一些參數(shù),如果需要其他參數(shù)可以自行添加
<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" />
<!--邊框?qū)挾?->
<attr name="code_border_width" format="dimension" />
<!--邊框默認(rèn)顏色-->
<attr name="code_border_color" format="color" />
<!--邊框選中顏色-->
<attr name="code_border_select_color" format="color" />
<!--邊框圓角-->
<attr name="code_border_radius" format="dimension" />
<!--code 內(nèi)容顏色(密碼或文字)-->
<attr name="code_content_color" format="color" />
<!--code 內(nèi)容大姓ひ(密碼或文字)-->
<attr name="code_content_size" format="dimension" />
<!--code 單個(gè)寬度-->
<attr name="code_item_width" format="dimension" />
<!--code Item之間的間隙-->
<attr name="code_item_space" format="dimension" />
</declare-styleable>
3塔嬉、獲取自定義配置參數(shù)
這里獲取參數(shù)玩徊,有的參數(shù)默認(rèn)給了默認(rèn)值租悄。
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)
//當(dāng)前索引(待輸入的光標(biāo)位置)
currentIndex = text?.length ?: 0
//Item寬度(這里判斷如果設(shè)置了寬度并且合理就使用當(dāng)前設(shè)置的寬度恩袱,否則平均計(jì)算)
codeItemWidth = if (codeItemWidth != -1 && (codeItemWidth * maxLength + codeItemSpace * (maxLength - 1)) <= measuredWidth) {
codeItemWidth
} else {
((measuredWidth - codeItemSpace * (maxLength - 1)) / maxLength).toInt()
}
//計(jì)算左右間距大小
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方法中主要是根據(jù)設(shè)置的codeStyle樣式泣棋,繪制不同的樣子。在繪制之前畔塔,主要做了三個(gè)操作潭辈。
- 對(duì)當(dāng)前焦點(diǎn)索引currentIndex的計(jì)算
- 單個(gè)驗(yàn)證碼寬度codeItemWidth的計(jì)算
- 第一個(gè)驗(yàn)證碼距離左邊的間距space的計(jì)算
對(duì)當(dāng)前焦點(diǎn)索引currentIndex的計(jì)算
這里巧妙的使用了獲取當(dāng)前EditText數(shù)據(jù)的長度作為當(dāng)前索引值,比如說澈吨,開始沒有輸入數(shù)據(jù)把敢,獲取長度為0,則當(dāng)前焦點(diǎn)應(yīng)該在0索引位置上谅辣,當(dāng)輸入一個(gè)數(shù)據(jù)時(shí)修赞,數(shù)據(jù)長度為1,則焦點(diǎn)變?yōu)?桑阶,焦點(diǎn)相當(dāng)于移動(dòng)到了索引1的位置上柏副,刪除數(shù)據(jù)同理,這樣就達(dá)到了上面分析的 ”功能5“的效果蚣录。
單個(gè)驗(yàn)證碼寬度codeItemWidth的計(jì)算
這里因?yàn)橛?中樣式割择,有的是表格一體展示,有的是分開展示萎河,比如方塊荔泳、橫線蕉饼、圈圈,這三種中間是有空隙的玛歌,這個(gè)空隙大小我們做了配置參數(shù)code_item_space椎椰,對(duì)于這個(gè)參數(shù),表格樣式是不需要的沾鳄,所以不管你設(shè)置了還是沒有設(shè)置慨飘,在表格樣式中是無效的。所以這里做了統(tǒng)一計(jì)算译荞。
第一個(gè)驗(yàn)證碼距離左邊的間距space的計(jì)算
因?yàn)樾枰L制瓤的,所以需要起始點(diǎn),那么起點(diǎn)應(yīng)該是:(控件總寬度-所有驗(yàn)證碼的寬度-所有驗(yàn)證碼之前的空隙)/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)
}
//繪制當(dāng)前位置邊框
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
}
//當(dāng)前光標(biāo)樣式
if (currentIndex != -1 && currentIndex == i && isCodeFocused) {
canvas.drawBitmap(currentBitmap!!, left.toFloat(), 0f, mLinePaint)
}
//默認(rèn)樣式
else {
canvas.drawBitmap(defaultBitmap!!, left.toFloat(), 0f, mLinePaint)
}
}
}
7、繪制橫線樣式
/**
* 橫線 code
*/
private fun drawLineCode(canvas: Canvas) {
for (i in 0 until maxLength) {
//當(dāng)前選中狀態(tài)
if (currentIndex == i && isCodeFocused) {
mLinePaint.color = borderSelectColor
}
//默認(rèn)狀態(tài)
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) {
//當(dāng)前繪制的圓圈的左x軸坐標(biāo)
var left: Float = if (i == 0) {
(space + i * codeItemWidth).toFloat()
} else {
space + i * codeItemWidth + codeItemSpace * i
}
//圓心坐標(biāo)
val cx: Float = left + codeItemWidth / 2f
val cy: Float = measuredHeight / 2f
//圓形半徑
val radius: Float = codeItemWidth / 5f
//默認(rèn)樣式
if (i >= currentIndex) {
canvas.drawCircle(cx, cy, radius, mLinePaint.apply { style = Paint.Style.FILL })
}
}
}
10稽坤、繪制輸入數(shù)據(jù)展示
/**
* 繪制內(nèi)容
*/
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 拓展
}
}
}
}
上面就是對(duì)四種樣式的繪制,主要考察的API如下:
- canvas.drawBitmap()
- canvas.drawLine()
- canvas.drawCircle()
- canvas.drawText()
主要對(duì)這四個(gè)API的使用數(shù)據(jù)上的計(jì)算糯俗,相對(duì)比較的簡單尿褪,其中有個(gè)點(diǎn)擊獲取焦點(diǎn)以及失去焦點(diǎn)更新樣式方式:
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(focused, direction, previouslyFocusedRect)
isCodeFocused = focused
invalidate()
}
通過isCodeFocused字段來控制。
11得湘、控件使用
<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" />