Android開(kāi)發(fā)自定義短信驗(yàn)證碼實(shí)現(xiàn)過(guò)程詳解
效果圖
簡(jiǎn)介
基本上只要需要登錄的APP,都會(huì)有驗(yàn)證碼輸入,所以說(shuō)是比較常用的控件,而且花樣也是很多的,這里列出來(lái)4種樣式,分別是:
- 表格類(lèi)型
- 方塊類(lèi)型
- 橫線(xiàn)類(lèi)型
- 圈圈類(lèi)型
其實(shí)還有很多其他的樣式,但是這四種是我遇到最多的樣式,所以特地拿來(lái)實(shí)現(xiàn)下,網(wǎng)上有很多類(lèi)似的輪子,實(shí)現(xiàn)方式也是蠻多的,比如說(shuō):
- 組合控件(線(xiàn)性布局添加子View)
- 自定義ViewGrop
- 自定義View
- ...
自己看了些網(wǎng)絡(luò)上實(shí)現(xiàn)的方案,參考了一些比較好的方式,這里先來(lái)分析下這個(gè)控件有哪些功能,再?zèng)Q定實(shí)現(xiàn)方案。
功能分析
- 1、默認(rèn)狀態(tài)樣式展示
- 2、支持設(shè)置最大數(shù)量
- 3、支持4種類(lèi)型樣式
- 4、點(diǎn)擊控件,彈出鍵盤(pán),獲取焦點(diǎn),顯示焦點(diǎn)樣式。焦點(diǎn)失去,展示默認(rèn)樣式。
- 5、輸入數(shù)據(jù),焦點(diǎn)移動(dòng)到下一個(gè)位置,刪除數(shù)據(jù),焦點(diǎn)也跟隨移動(dòng)
通過(guò)功能4讓我第一想到的就是EditText控件,那么怎么做呢?大家都知道EditText有自己的樣式和操作,如果我們可以屏蔽無(wú)用的樣式和功能,留下我們需要的不就可以了嗎。
[圖片上傳失敗...(image-888083-1663139918159)]
EditText
- 點(diǎn)擊EditText可以彈出鍵盤(pán)(需要的),并獲取焦點(diǎn)(需要的),顯示光標(biāo)(不需要的)
- 長(zhǎng)按EditText會(huì)顯示復(fù)制,粘貼等操作(不需要的)
- 輸入數(shù)據(jù),內(nèi)容默認(rèn)顯示(不需要的)
上面對(duì)EditText基本使用時(shí)出現(xiàn)的樣式和操作,有的是需要的,有的是不需要的,我們可以對(duì)不需要的進(jìn)行屏蔽,來(lái)代碼走起。
代碼實(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 //屏蔽長(zhǎng)按操作 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" /> <!--橫線(xiàn)--> <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)容大?。艽a或文字)--> <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、重寫(xiě) 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) } //橫線(xiàn) 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ù)的長(zhǎng)度作為當(dāng)前索引值,比如說(shuō),開(kāi)始沒(méi)有輸入數(shù)據(jù),獲取長(zhǎng)度為0,則當(dāng)前焦點(diǎn)應(yīng)該在0索引位置上,當(dāng)輸入一個(gè)數(shù)據(jù)時(shí),數(shù)據(jù)長(zhǎng)度為1,則焦點(diǎn)變?yōu)?,焦點(diǎn)相當(dāng)于移動(dòng)到了索引1的位置上,刪除數(shù)據(jù)同理,這樣就達(dá)到了上面分析的 ”功能5“的效果。
單個(gè)驗(yàn)證碼寬度codeItemWidth的計(jì)算
這里因?yàn)橛?中樣式,有的是表格一體展示,有的是分開(kāi)展示,比如方塊、橫線(xiàn)、圈圈,這三種中間是有空隙的,這個(gè)空隙大小我們做了配置參數(shù)code_item_space,對(duì)于這個(gè)參數(shù),表格樣式是不需要的,所以不管你設(shè)置了還是沒(méi)有設(shè)置,在表格樣式中是無(wú)效的。所以這里做了統(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) //繪制表格中間分割線(xiàn) 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、繪制橫線(xiàn)樣式
/** * 橫線(xiàn) 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 }) } } }
9、繪制輸入數(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ì)比較的簡(jiǎn)單,其中有個(gè)點(diǎn)擊獲取焦點(diǎn)以及失去焦點(diǎn)更新樣式方式:
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { super.onFocusChanged(focused, direction, previouslyFocusedRect) isCodeFocused = focused invalidate() }
通過(guò)isCodeFocused字段來(lái)控制。
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開(kāi)發(fā)自定義短信驗(yàn)證碼實(shí)現(xiàn)過(guò)程詳解的詳細(xì)內(nèi)容,更多關(guān)于Android自定義短信驗(yàn)證碼的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android使用WebView加載播放視頻流及實(shí)現(xiàn)相關(guān)功能
這篇文章主要講解在 Android 應(yīng)用中使用 WebView 加載播放視頻流,包括 WebView 配置、媒體控制器、權(quán)限、安全性、用戶(hù)體驗(yàn)等方面,介紹了實(shí)現(xiàn)相關(guān)功能的代碼示例,需要的朋友可以參考下2025-01-01Android 邊播邊緩存的實(shí)現(xiàn)(MP4 未加密m3u8)
這篇文章主要介紹了Android 邊播邊緩存的實(shí)現(xiàn)(MP4 未加密m3u8),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11配置一個(gè)好用的Android模擬器讓你不再對(duì)模擬器那么失望
默認(rèn)情況下的Android模擬器窗口占據(jù)了屏幕巨大的空間,而且毫無(wú)緣由的放著一個(gè)屏幕鍵盤(pán),如果你沒(méi)親自用過(guò)模擬器的話(huà),還有一個(gè)不易發(fā)現(xiàn)的問(wèn)題:幾乎是慢到不能用2013-01-01Android BottomSheet效果的兩種實(shí)現(xiàn)方式
這篇文章主要介紹了Android BottomSheet效果的兩種實(shí)現(xiàn)方式,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08Android實(shí)現(xiàn)EventBus登錄界面與傳值(粘性事件)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)EventBus登錄界面與傳值,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11android 實(shí)現(xiàn)ScrollView自動(dòng)滾動(dòng)的實(shí)例代碼
這篇文章主要介紹了android 實(shí)現(xiàn)ScrollView自動(dòng)滾動(dòng)的實(shí)例代碼,有需要的朋友可以參考一下2014-01-01Android識(shí)別預(yù)裝的第三方App方法實(shí)例
這篇文章主要給大家介紹了關(guān)于Android如何識(shí)別預(yù)裝的第三方App的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01