自定义一个可以更换边框背景的类似密码输入框的view,如图
我们先来了解一下 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,需要几个主要的属性
- 输入框的数量
- 输入框的宽度
- 输入框之间的分割视图
- 输入框文字颜色
- 输入框文字大小
- 输入框获取焦点时的背景
- 输入框没有焦点时的背景
- 是否密码模式
- 密码模式下的圆点的半径
- 一个容器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>