Android-自定義短信驗(yàn)證碼

效果圖

簡介

基本上只要需要登錄的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" />

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杖玲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子淘正,更是在濱河造成了極大的恐慌摆马,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸿吆,死亡現(xiàn)場離奇詭異囤采,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)惩淳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門蕉毯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人黎泣,你說我怎么就攤上這事恕刘。” “怎么了抒倚?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵褐着,是天一觀的道長。 經(jīng)常有香客問我托呕,道長含蓉,這世上最難降的妖魔是什么频敛? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮馅扣,結(jié)果婚禮上斟赚,老公的妹妹穿的比我還像新娘。我一直安慰自己差油,他們只是感情好拗军,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蓄喇,像睡著了一般发侵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妆偏,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天刃鳄,我揣著相機(jī)與錄音,去河邊找鬼钱骂。 笑死叔锐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的见秽。 我是一名探鬼主播愉烙,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼张吉!你這毒婦竟也來了齿梁?” 一聲冷哼從身側(cè)響起催植,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤肮蛹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后创南,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伦忠,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年稿辙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昆码。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡邻储,死狀恐怖赋咽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吨娜,我是刑警寧澤脓匿,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站宦赠,受9級(jí)特大地震影響陪毡,放射性物質(zhì)發(fā)生泄漏米母。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一毡琉、第九天 我趴在偏房一處隱蔽的房頂上張望铁瞒。 院中可真熱鬧,春花似錦桅滋、人聲如沸慧耍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至笋鄙,卻和暖如春师枣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背萧落。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工践美, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人找岖。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓陨倡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親许布。 傳聞我的和親對(duì)象是個(gè)殘疾皇子兴革,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容