Android - 自定义数字验证输入框

自定义一个可以更换边框背景的类似密码输入框的view,如图

0021211.png

我们先来了解一下 TextWatcher

EditText的输入监听 TextWatcher

用于监听文本变化的接口,可以很方便的对显示文本控件和可编辑控件中的文字进行监听和修改。
有三个抽象方法 执行顺序 beforeTextChanged -> onTextChanged -> afterTextChanged
beforeTextChanged(CharSequence s, int start, int count, int after) 调用此方法通知您,在s文本中的以start为开头的count个字符,将要被after个字符替代。
onTextChanged(CharSequence s, int start, int before, int count) 在当前文本中,从start位置开始之后的before个字符已经被count个字符替换。
afterTextChanged(Editable s) 文本已改变为s,可以在该函数中实现对s进行下一步处理
afterTextChanged会判断是否输入完成,如果在该函数中调用自身,会造成递归调用。在你输入完成后执行,我们输入完后处于完成状态,他就监测到完成了就不断的执行,因为我们不操作,是不是一直处于完成状态?所以就处于死循环了。切记在此做操作。

自定义可替换边框的EditText

我们设计一个EditText,需要几个主要的属性

  1. 输入框的数量
  2. 输入框的宽度
  3. 输入框之间的分割视图
  4. 输入框文字颜色
  5. 输入框文字大小
  6. 输入框获取焦点时的背景
  7. 输入框没有焦点时的背景
  8. 是否密码模式
  9. 密码模式下的圆点的半径
  10. 一个容器layout
    根据需要的主要的属性,来先定义一些属性
    // 容器
    private lateinit var containerEt: LinearLayout
    // editText
    private lateinit var et: EditText
    // 输入框数量
    private var mEtNumber: Int = 0
    // 输入框的宽度
    private var mEtWidth: Int = 0
    //输入框分割线
    private var mEtDividerDrawable: Drawable? = null
    //输入框文字颜色
    private var mEtTextColor: Int = 0
    //输入框文字大小
    private var mEtTextSize: Float = 0.toFloat()
    //输入框获取焦点时背景
    private var mEtBackgroundDrawableFocus: Drawable? = null
    //输入框没有焦点时背景
    private var mEtBackgroundDrawableNormal: Drawable? = null
    //是否是密码模式
    private var mEtPwd: Boolean = false
    //密码模式时圆的半径
    private var mEtPwdRadius: Float = 0.toFloat()
    //存储TextView的数据 数量由自定义控件的属性传入
    private lateinit var mPwdTextViews: Array<TextView?>

然后我们定义一个容器样式

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <!-- 容器,用来添加每个输入框 -->
    <LinearLayout
            android:id="@+id/container_et"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:showDividers="middle">

    </LinearLayout>
    <!-- 真正EditText -->
    <EditText
            android:id="@+id/et"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/transparent"
            android:inputType="number"
            android:textCursorDrawable="@drawable/cursor_verification_edit_text"
            android:textColor="@android:color/transparent" />
</RelativeLayout>

我们可以通过定义xml属性,在xml中添加视图时指定一些属性

    <!-- 自定义验证码输入框-->
    <declare-styleable name="VerificationEditView">
        <!--输入框的数量-->
        <attr name="icv_et_number" format="integer" />
        <!--输入框的宽度-->
        <attr name="icv_et_width" format="dimension|reference" />
        <!--输入框之间的分割线-->
        <attr name="icv_et_divider_drawable" format="reference" />
        <!--输入框文字颜色-->
        <attr name="icv_et_text_color" format="color|reference" />
        <!--输入框文字大小-->
        <attr name="icv_et_text_size" format="dimension|reference" />
        <!--输入框获取焦点时边框-->
        <attr name="icv_et_bg_focus" format="reference" />
        <!--输入框没有焦点时边框-->
        <attr name="icv_et_bg_normal" format="reference" />
        <!--是否是密码模式-->
        <attr name="icv_et_pwd" format="boolean" />
        <!--密码模式时,圆的半径-->
        <attr name="icv_et_pwd_radius" format="dimension|reference" />
    </declare-styleable>

我们在代码中绑定属性

private var attrs: AttributeSet? = null
...
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerificationEditView, defStyleAttr, 0)
            mEtNumber = typedArray.getInteger(R.styleable.VerificationEditView_icv_et_number, 1)
            mEtWidth = typedArray.getDimensionPixelSize(R.styleable.VerificationEditView_icv_et_width, 42)
            mEtDividerDrawable = typedArray.getDrawable(R.styleable.VerificationEditView_icv_et_divider_drawable)
            mEtTextSize = typedArray.getDimensionPixelSize(R.styleable.VerificationEditView_icv_et_text_size, sp2px(16f, context).toInt()).toFloat()
            mEtTextColor = typedArray.getColor(R.styleable.VerificationEditView_icv_et_text_color, Color.BLACK)
            mEtBackgroundDrawableFocus = typedArray.getDrawable(R.styleable.VerificationEditView_icv_et_bg_focus)
            mEtBackgroundDrawableNormal = typedArray.getDrawable(R.styleable.VerificationEditView_icv_et_bg_normal)
            mEtPwd = typedArray.getBoolean(R.styleable.VerificationEditView_icv_et_pwd, false)
            mEtPwdRadius = typedArray.getDimensionPixelSize(R.styleable.VerificationEditView_icv_et_pwd_radius, 0).toFloat()
            //释放资源
            typedArray.recycle()
        }

重写onMeasure方法,设置默认的高度

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 设置当 高为 warpContent 模式时的默认值 为 50dp
        var mHeightMeasureSpec = heightMeasureSpec

        val heightMode = View.MeasureSpec.getMode(mHeightMeasureSpec)
        if (heightMode == View.MeasureSpec.AT_MOST) {
            mHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(dp2px(50f, context).toInt(), View.MeasureSpec.EXACTLY)
        }

        super.onMeasure(widthMeasureSpec, mHeightMeasureSpec)
    }

在添加输入框之前先把容器初始化

    //初始化存储TextView 的容器
    private fun initEtContainer(mTextViews: Array<TextView?>) {
        for (mTextView in mTextViews) {
            mTextView?.let {
                val params = LinearLayout.LayoutParams(mEtWidth, ViewGroup.LayoutParams.MATCH_PARENT)
                containerEt.addView(mTextView, params)
            }
        }
    }
        // 设置输入框个数
    fun setEtNumber(etNumber: Int) {
        this.mEtNumber = etNumber
        et.removeTextChangedListener(myTextWatcher)
        containerEt.removeAllViews()
        initUI()
    }

        // 设置宽度
    fun setEtItemWidth(width: Int) {
        mEtWidth = width
        // 取出每个view
        for (i in 0 until containerEt.childCount) {
            val childView = containerEt.getChildAt(i)
            val params = childView.layoutParams
            params.width = mEtWidth
            childView.layoutParams = params
        }
    }

指定了输入框数量之后,我们在容器中添加相应数量的TextView,并绑定属性。

    //初始化TextView
    private fun initTextViews(context: Context, number: Int, width: Int, dividerDrawable: Drawable?, textSize: Float, textColor: Int) {
        // 设置 editText 的输入长度
        et.isCursorVisible = false
        et.filters = arrayOf<InputFilter>(InputFilter.LengthFilter(number)) //最大输入长度
        // 设置分割线的宽度
        if (dividerDrawable != null) {
            dividerDrawable.setBounds(0, 0, dividerDrawable.minimumWidth, dividerDrawable.minimumHeight)
            containerEt.dividerDrawable = dividerDrawable
        }
        mPwdTextViews = arrayOfNulls(etNumber)

        for (i in mPwdTextViews.indices) {
            val textView = TextView(context)
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
            textView.setTextColor(textColor)
            if (i == 0) {
                textView.setBackgroundDrawable(mEtBackgroundDrawableFocus)
            } else {
                textView.setBackgroundDrawable(mEtBackgroundDrawableNormal)
            }
            textView.gravity = Gravity.CENTER
            textView.isFocusable = false
            mPwdTextViews[i] = textView
        }
    }

当我们输入或删除文字的时候,容器中的TextView要及时进行更新,所以我们可以通过监听真正的EditText来实现

    interface InputCompleteListener {
        fun inputComplete()
        fun deleteContent()
    }
    // 输入完成 和 删除成功 的监听
    private var inputCompleteListener: InputCompleteListener? = null
    // 设置监听
    fun setInputCompleteListener(inputCompleteListener: InputCompleteListener) {
        this.inputCompleteListener = inputCompleteListener
    }
    private fun setListener() {
        // 监听输入内容
        et.addTextChangedListener(myTextWatcher)

        // 监听删除按键
        et.setOnKeyListener(OnKeyListener { _, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
                onKeyDelete()
                return@OnKeyListener true
            }
            false
        })
    }
    private fun onKeyDelete() {
        for (i in mPwdTextViews.indices.reversed()) {
            val tv = mPwdTextViews[i]
            tv?.let {
                if (it.text.toString().trim() != "") {
                    if (mEtPwd) {
                    }
                    tv.text = ""
                    // 添加删除完成监听
                    if (inputCompleteListener != null) {
                        inputCompleteListener!!.deleteContent()
                    }
                    tv.setBackgroundDrawable(mEtBackgroundDrawableFocus)
                    if (i < mEtNumber - 1) {
                        mPwdTextViews[i + 1]?.setBackgroundDrawable(mEtBackgroundDrawableNormal)
                    }
                    return
                }
            }
        }
    }

监听到更新,要及时的更新TextView

    private fun setText(inputContent: String) {

        for (i in mPwdTextViews.indices) {
            val tv = mPwdTextViews[i]
            tv?.let {
                if (it.text.toString().trim() == "") {
                    if (mEtPwd) {
                    }
                    it.text = inputContent
                    tv.setBackgroundDrawable(mEtBackgroundDrawableNormal)
                    if (i < mEtNumber - 1) {
                        mPwdTextViews[i + 1]?.setBackgroundDrawable(mEtBackgroundDrawableFocus)
                    }
                    if (i == mEtNumber - 1) {
                        // 添加输入完成的监听
                        if (inputCompleteListener != null) {
                            inputCompleteListener!!.inputComplete()
                        }
                    }
                    return
                }
            }
        }
    }

最后,我们用TextWatcher来监听EditText的变化

    private inner class MyTextWatcher : TextWatcher {

        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {

        }

        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {

        }

        override fun afterTextChanged(editable: Editable) {
            val inputStr = editable.toString()
            // 输入字符设置到TextView上
            if (!TextUtils.isEmpty(inputStr)) {
                setText(inputStr)
                et.setText("")
            }
        }
    }

定义完成之后,在xml中使用

 ...
         <com.example.addressselector.view.VerificationEditView
                android:id="@+id/cardEt"
                android:layout_width="match_parent"
                android:layout_height="40dp"
                android:layout_marginTop="30dp"
                android:layout_marginStart="15dp"
                android:layout_marginEnd="15dp"
                app:icv_et_number="10"
                app:icv_et_bg_focus="@drawable/dialog_input_item_bg"
                app:icv_et_bg_normal="@drawable/dialog_input_item_bg"
                app:icv_et_divider_drawable="@drawable/shape_verification_edit_divider"
                app:icv_et_text_color="@color/color_333333"
                app:icv_et_text_size="18dp"
                app:icv_et_width="30dp"
                app:layout_constraintTop_toTopOf="parent" 
                tools:layout_editor_absoluteX="13dp"/>
 ...

文本框之间是用Drawable来填充的,@drawable/shape_verification_edit_divider

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <size android:width="5dp"
          android:height="10dp"/>

    <solid android:color="@color/transparent"/>

</shape>

边框,通过icv_et_bg_focus和icv_et_bg_normal属性来设置

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <stroke
            android:width="1dp"
            android:color="#e6e6e6" />
    <corners android:radius="2dp"/>
</shape>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容