效果圖
簡介
基本上只要需要登錄的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 內容大?。艽a或文字)-->
<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 })
}
}
}
10、繪制輸入數據展示
/**
* 繪制內容
*/
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字段來控制。
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" />