最近项目有个需求,需要有一个弹窗验证手机号的中间四位,大概长这样:
就是一个简单的校验手机号的控件,原以为项目中以前应该也有做过,会有现成的可以用。
结果找了半天,没有~,好吧,再想想之前自己体验过的类似的控件,好像实现起来也比较简单,那就自己做一个吧。
先做个简单的版本,能完成需求就行,这也是这一篇的主要内容。
首先分析一下布局,上面一个TextView,下面四个文字框,底部的两个Button先不考虑。
主要是这四个文字框要怎么实现?
我们想要实现的效果是用户每输入一个数字,就会依次填充在对应的框框里,如果用户键入软键盘的删除(退格)键,那么框框中的数字就会依次删除。
如下图:
这四个文字框,我想到的有这么几种思路:
-
重写一个类,继承自View,然后重写它的onDraw()方法。
实际上就是自己实现一个View,并自己计算它的布局,包括方框的位置,它们之间的间隔,还得计算数字绘制的位置。要实现一次删除的效果,还得监听软键盘的删除事件,并且每次删除都要触发一次View的重绘事件(重新画一遍边框和数字),这个思路比较复杂,但可以实现。
-
重写一个View,继承自TextView,同样尝试重写它的onDraw()过程,尝试定制TextView的文本绘制效果,让每个数字绘制时都自带一个边框。(再没仔细研究TextView的文本绘制过程之前,我觉得这是可行的,但研究后,我发现TextView的布局绘制过程因为太复杂,已经委托给其它类来辅助绘制,这个思路,比第一种还要复杂)
-
第三种,封装一个Layout,继承自ViewGroup,那四个方框,就对应四个EditText,作为ViewGroup的子控件。这个思路实际上是把我们的绘制过程委托给EditText了,我们只需要负责监听软键盘的输入和删除事件,当用户输入时,每输入一个数字,让EditText的光标移到下一个,删除的话,就让光标移动到上一个EditText。
毫无疑问,第三种是思路是最简单的。
接下来就按第三个思路来实现这个控件。
先把基本的布局准备好,比较简单,这里就直接放图了:
就是上面一个TextView,下面一个LinearLayout,包着四个EditText
EditText的设置还是要放一下的,大概就是设置,EditText的背景图,文字对齐方向,文字的最大长度。
<EditText
android:id="@+id/ed_1"
android:layout_width="46dp"
android:layout_height="46dp"
android:textAlignment="center"
android:inputType="number"
android:maxLength="1"
android:textCursorDrawable="@color/transparent"
android:layout_marginRight="20dp"
android:background="@drawable/tel_number_check_border"/>
可以看到,我们限定了EditText的inputType = number,这样用户就只能够输入数字了。
接下来创建一个类,继承自ViewGroup,给类个名字,就叫它:TelNumCheckerView,继承自FrameLayout。
public class TelNumCheckerView extends FrameLayout {}
默认实现FrameLayout的三个构造方法,并在构造方法中初始化布局:
private void init(Context context) {
// 初始化布局文件
LayoutInflater.from(context).inflate(R.layout.tel_num_unbind_check,
this, true);
// 获取引用
mTvTitleText = findViewById(R.id.text_number);
mEdTexts[0] = findViewById(R.id.ed_1);
mEdTexts[1] = findViewById(R.id.ed_2);
mEdTexts[2] = findViewById(R.id.ed_3);
mEdTexts[3] = findViewById(R.id.ed_4);
initEdits();
}
因为我们现在只是简单的想要实现需求,所以这里就直接初始化4个EditText:
private EditText[] mEdTexts = new EditText[4];
这里我们想要实现一个效果,在弹窗的时候,自动弹出软键盘,并让第一个EditText获取焦点,这样用户就可以很方便的输入数字了。
所以在initEdits()
中设置:
mEdTexts[0].requestFocus(); // 让第一个EditText获取焦点
// 弹出软键盘
KeyBoardUtil.showKeyBoardInMills(mEdTexts[0], getContext(), 400);
先来看看目前实现的效果:
可以看到,因为我们并没有处理当一个框输入完数字后,让光标自动传递到下一个方框内,并且在EditText的配置文件中,设置了maxLength = 1,所以输入完一个数字后,就无法再输入了,光标也不会自动传递到下一个框。
接下来就要处理这个问题。
解决这个问题,我们需要准备能够监听用户的输入事件,让用户每输入一个文字,就让光标转移到下一个方框内,也就是调用下一个EditText的requestFocus()
。
那如何能够监听用户的输入事件呢?这里也有两种思路:
-
给EditText设置addTextChangedListener(),这个可以监听EditText的文字变化事件,当我们监听到文本变化时,就解注册当前editText的listener,然后调用下一个editText.requestFocus(),并设置textChanged监听。
代码如下:
mEdTexts[curIdx].removeTextChangedListener(textChangedListener); mEdTexts[curIdx + 1].requestFocus(); mEdTexts[curIdx + 1].addTextChangedListener(textChangedListener);
textChangedListener本身无法监听用户的删除事件,不过可以通过其它途径来实现,只是比较复杂。
-
给EditText设置setOnKeyListener(),这个能够监听用户软键盘的键入事件,移动光标的思路和第一种是一样的,只不过onKeyListener,可以监听用户输入的删除(退格)事件,当我们想要实现删除数字,依次退格的需求时比较方便。
我们选第二种,上面也分析过了,第二种实现监听退格事件比较简单,也就是给EditText设置setOnKeyListener()。
那么首先创建一个OnKeyListener,关键在于实现onKey()方法
private final OnKeyListener mKeyListener = new OnKeyListener() {
// 当前所在光标所在EditText索引
private int mIdx = 0;
// 删除时,用于记录光标的移动方向
private final int DIRECTION_LEFT = -1;
private final int DIRECTION_RIGHT = 1;
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
}
return false;
}
};
我们只需要关注用户按下的事件,也就是KeyEvent.ACTION_DOWN。
用户按下后,可能按下的是数字,也可能按下退格KeyEvent.KEYCODE_DEL,所以我们还要区分处理。
当用户按下的是删除键,先看看处理的逻辑
// 退格
if (keyCode == KeyEvent.KEYCODE_DEL) {
int textLength = mEdTexts[mIdx].getText().toString().length();
// 在删除前已经没有内容,需要退格
if (textLength == 0) {
// 如果此时光标就在第一格,直接拦截,放弃此次点击
if (mIdx == 0) return true;
else {
mIdx -= 1;
EditText curEdit = mEdTexts[mIdx];
Editable curText = curEdit.getText();
curEdit.setText(curText.subSequence(0, curText.length() - 1));
// 左移光标
moveCursor(mIdx, DIRECTION_LEFT);
}
} else {
// 仍然有内容,走正常逻辑
return false;
}
}
首先,获取当前光标所在的EditText文本长度,如果用户按下删除键,此时文本没有内容了,就要触发退格的事件,把光标移到上一格,并删除掉上一格的一个字符。否则,不作特殊处理,即直接return false,直接让editText自然删除。
当用户按下普通的数字:
else {
// 已经到了尾格,不需要移动光标,放弃不处理
if (mIdx + 1 == mEdTexts.length) {
return false;
}
// 不是尾格,需要移动光标
mIdx += 1;
// 右移光标
moveCursor(mIdx, DIRECTION_RIGHT);
return false;
}
整体逻辑还是比较简单的,来看看moveCursor()的做了些啥:
private void moveCursor(int toIdx, int direction) {
// 解注册上一个editText
mEdTexts[toIdx - direction].setOnKeyListener(null);
// 让其他EditText不可点击,防止光标索引错乱
disEnableOther(toIdx);
mEdTexts[toIdx].requestFocus();
mEdTexts[toIdx].setOnKeyListener(this);
}
就是解注册上一个EditText,然后给当前editText设置监听,同时获取焦点。
看看我们目前实现的效果:
这时候就差最后一个步骤了,就是当我们用户输入完四个数字时,要触发一次回调来检验用户输入的是否正确。
可以定义一个接口:
public interface onKeyFinishListener {
void onKeyRight();
void onKeyError();
}
然后在用户输入到最后一格的时候,加一层判断:用户输对了,就调用onKeyRight(),否则调用onKeyError()。比较简单就不贴代码了。
那么本篇到这里,我们这个控件要实现的效果就都已经实现,这个控件已经能够简单的满足需求了。
有同学感兴趣的话,也可以再进行优化下,比如:
让EditText的数量可以灵活配置,也就是可以在代码上动态设置EditText的数量,这种优化的场景是,有可能需要检验号码中间的5位或3位,这样文字框可以动态配置的话,就可以适应这种场景,不过这类场景比较少哈哈。
兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
拜托拜托,谢谢各位同学!