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 內容大?。艽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" />

GitHub鏈接:
https://github.com/yixiaolunhui/AndroidXY

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容