Android自定义View-仿滴滴自定义验证码输入框
之前公司有过需求,要求做一个类似滴滴打车输入验证码的页面,长这样:emmmmmm,好像截了iOS的图,不要在意这些细节。来分析一下这个验证码部分,实现这样一个自定义View,首先,要区分单个验证码选中状态和未选中状态,并且光标悬停在选中的验证码中心,其次, 每次输入文字后需要依次显示在每个单独的验证码容器中,还有诸如自定义验证码选中状态、清空输入验证码等等。受到博文Android 自定...
之前公司有过需求,要求做一个类似滴滴打车输入验证码的页面,长这样:
emmmmmm,好像截了iOS的图,不要在意这些细节。
来分析一下这个验证码部分,实现这样一个自定义View,首先,要区分单个验证码选中状态和未选中状态,并且光标悬停在选中的验证码中心,其次, 每次输入文字后需要依次显示在每个单独的验证码容器中,还有诸如自定义验证码选中状态、清空输入验证码等等。受到博文Android 自定义View之带密码模式的正方形验证码输入框的启发,我决定使用继承RelativeLayout的方式来完成这个自定义View。
实现思路
笔者这里决定继承RelativeLayout来实现自定义验证码的功能。显示验证码可以使用几个TextView,这里需要将TextView统一管理,所以还需要一个TextView数组。有光标,那自然而然的就想到了EditText,可以使用一个透明背景的EditText。几个验证码可以使用TextView以一定的规则进行排列,通过监听EditText的输入,拦截到输入字符,并将字符传递给TextView数组,并将EditText置为空,同时重设TextView选中状态,移动EditText光标。笔者这里采用给EditText设置paddingLeft的方式来实现光标的移动,当然,需要经过一些计算。OK,大概思路就是这样了,具体的代码我们继续看下面的。
具体实现
首先,我们需要一个类来继承RelativeLayout,并且给这个类分配上一些自定义属性,在attrs.xml文件中定义以下属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 自定义验证码-->
<declare-styleable name="VerificationCodeView">
<!--输入框的数量-->
<attr name="vcv_code_number" format="integer" />
<!--单个验证码的宽度-->
<attr name="vcv_code_width" format="dimension|reference" />
<!--输入框文字颜色-->
<attr name="vcv_code_color" format="color|reference" />
<!--输入框文字大小-->
<attr name="vcv_code_size" format="dimension|reference" />
<!--输入框获取焦点时边框-->
<attr name="vcv_code_bg_focus" format="reference" />
<!--输入框没有焦点时边框-->
<attr name="vcv_code_bg_normal" format="reference" />
</declare-styleable>
</resources>
具体属性都再代码中注释出来了,接着,在Java类中取出这些属性:
/**
* @author 小米Xylitol
* @email xiaomi987@hotmail.com
* @desc Android验证码View
* @date 2018-05-09 10:25
*/
public class VerificationCodeView extends RelativeLayout {
//当前验证码View用来展示验证码的TextView数组,数组个数由codeNum决定
private TextView[] textViews;
//用来输入验证码的EditText输入框,输入框会跟随输入个数移动,显示或者隐藏光标等
private WiseEditText editText;
//当前验证码View展示验证码个数
private int codeNum;
//每个TextView的宽度
private float codeWidth;
//字体颜色
private int textColor;
//字体大小
private int textSize;
//每个单独验证码的背景
private Drawable textDrawable;
//验证码选中时的背景
private Drawable textFocusedDrawable;
//验证码之间间隔
private float dividerWidth = 0;
//对EditText输入进行监听
private TextWatcher watcher;
//监听删除键和enter键
private OnKeyListener onKeyListener;
//当前选中的TextView位置,即光标所在位置
private int currentFocusPosition = 0;
public VerificationCodeView(Context context) {
this(context,null);
}
public VerificationCodeView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public VerificationCodeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs,defStyleAttr);
}
/**
* 初始化View
* @param context
* @param attrs
* @param defStyleAttr
*/
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.VerificationCodeView,defStyleAttr,0);
codeNum = array.getInteger(R.styleable.VerificationCodeView_vcv_code_number,4);
codeWidth = array.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_code_width, (int) dip2px(50,context));
textColor = array.getColor(R.styleable.VerificationCodeView_vcv_code_color,getResources().getColor(R.color.text_border_focused));
textSize = array.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_code_size,getResources().getDimensionPixelOffset(R.dimen.text_size));
textDrawable = array.getDrawable(R.styleable.VerificationCodeView_vcv_code_bg_normal);
textFocusedDrawable = array.getDrawable(R.styleable.VerificationCodeView_vcv_code_bg_focus);
array.recycle();
//若未设置选中和未选中状态,设置默认
if(textDrawable == null) {
textDrawable = getResources().getDrawable(R.drawable.bg_text_normal);
}
if(textFocusedDrawable == null) {
textFocusedDrawable = getResources().getDrawable(R.drawable.bg_text_focused);
}
initView(context);
initListener();
resetCursorPosition();
}
}
可以看到我们这里依次取出了验证码字数、单个验证码宽度(这里默认是正方形)、字体颜色、字体大小、获取焦点背景、失去焦点背景等,并设置了默认值。最后别忘了调用array.recycler()
释放资源。
接下来就是对TextView数组textViews
以及editText
的初始化工作了,在方法initView
中:
/**
* 初始化各View并加入当前View中
*/
private void initView(Context context) {
//初始化TextView数组
textViews = new TextView[codeNum];
for (int i = 0;i < codeNum;i++) {
//循环加入数组中,设置TextView字体大小和颜色,并将TextView依次加入LinearLayout
TextView textView = new TextView(context);
textView.setWidth((int) codeWidth);
textView.setHeight((int) codeWidth);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(textColor);
textView.setTextSize(textSize);
textViews[i] = textView;
this.addView(textView);
RelativeLayout.LayoutParams params = (LayoutParams) textView.getLayoutParams();
params.addRule(CENTER_VERTICAL);
}
//初始化EditText,设置背景色为透明,获取焦点,设置光标颜色,设置输入类型等
editText = new WiseEditText(context);
editText.setBackgroundColor(Color.TRANSPARENT);
editText.requestFocus();
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
setCursorRes(R.drawable.cursor);
addView(editText);
}
这里做了一个循环,将TextView添加进数组中,并将TextView添加进RelativeLayout中,这里注意将TextView设置为纵向居中。EditText的设置比较简单,将其背景设置为透明,并将传入的光标通过反射设置给EditText,设置其输入类型(我这里偷个懒直接设置了TYPE_CLASS_NUMBER
),最后将EditText加入RelativeLayout中。
这一步完成之后我们可以想象,现在的View是几个TextView和EditText堆叠在一起,下一步我们给它们各自调整位置,实现TextView居中,EditText充满父控件。由于我们需要TextView分散居中,需要获取到当前控件的宽度,那么代码就需要在onMeasure之后来执行,笔者这里选择在onLayout中执行设置的代码,在这之前需要考虑在当前控件是wrap_content时重设其高度:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST) {
//若高度是wrap_content,则设置为50dp
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) dip2px(80, getContext()), MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//获取到宽高,可以对TextView进行摆放
layoutTextView();
super.onLayout(changed, l, t, r, b);
}
/**
* onMeasure后获取到测量的控件宽度,计算出每个Code之间的间隔
*/
private void layoutTextView() {
//获取控件剩余宽度
float availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
if(codeNum > 1) {
//计算每个Code之间的间距
dividerWidth = (availableWidth - codeWidth * codeNum) / (codeNum - 1);
for(int i = 1;i < codeNum;i++) {
float leftMargin = codeWidth * i + dividerWidth * i;
RelativeLayout.LayoutParams params1 = (RelativeLayout.LayoutParams) textViews[i].getLayoutParams();
params1.leftMargin = (int) leftMargin;
}
}
//设置EditText宽度从第一个Code左侧到最后一个Code右侧,设置高度为Code高度
//设置EditText为纵向居中
editText.setWidth((int) availableWidth);
editText.setHeight((int) codeWidth);
RelativeLayout.LayoutParams params = (LayoutParams) editText.getLayoutParams();
params.addRule(CENTER_VERTICAL);
}
这里的layoutTextView
方法就是一个摆放TextView的过程,通过getMeasuredWidth
获取当前View的宽度,减去左侧右侧内边距,获得子View可用的宽度。算出各个TextView之间的间距,然后逐个设置左边距即可。
EditText就很简单,设置宽度占满父控件,并且垂直居中即可。
接下来是设置EditText的相关监听:
/**
* 监听EditText输入字符,监听键盘删除键
*/
private void initListener() {
watcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String content = s.toString();
if(!TextUtils.isEmpty(content)) {
if(content.length() == 1) {
setText(content);
}
editText.setText("");
}
}
};
editText.addTextChangedListener(watcher);
onKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
deleteCode();
return true;
}
return false;
}
};
editText.setSoftKeyListener(onKeyListener);
}
/**
* 删除键按下
*/
private void deleteCode() {
if(currentFocusPosition == 0) {
//当前光标位置在0,直接返回
return;
} else {
//当前光标不为0,当前光标位置-1,将当前光标位置TextView置为"",重设光标位置
currentFocusPosition--;
textViews[currentFocusPosition].setText("");
resetCursorPosition();
}
}
/**
* 拦截到EditText输入字符,发送给该方法进行处理
* @param s
*/
private void setText(String s) {
if(currentFocusPosition >= codeNum) {
//光标已经隐藏,直接返回
return;
}
//设置字符给当前光标位置的TextView,光标位置后移,重设光标状态
textViews[currentFocusPosition].setText(s);
currentFocusPosition++;
resetCursorPosition();
}
这里由于原生EditText在监听软键盘删除按钮时有一些缺陷,某些情况下会出现监听不到的现象。我使用了一个自定义的EditText,具体代码可以下载代码查看,我这里就不多做赘述。监听到删除按钮被按下后,将当前光标所在位置向前移动一位,将移动后位置的TextView设置为空,并且重设光标和选中状态。
监听EditText变化的思路很简单,内容有变化,则取出内容,设置给TextView,并且移动光标位置,将EditText置为空。注意这里editText.setText("")
一定要放在非空判断里执行,不然会死循环。在setText方法中与删除逻辑大同小异,先判断光标是否已经隐藏(是否已经全部输入),若未全部输入,将字符设置给当前光标所在位置的TextView,将光标位置后移,并且重设光标和选中状态。
我们来看下重设光标和选中状态的代码:
/**
* 重设EditText的光标位置,以及选中TextView的边框颜色
*/
private void resetCursorPosition() {
for(int i = 0;i < codeNum;i++) {
TextView textView = textViews[i];
if(i == currentFocusPosition) {
textView.setBackgroundDrawable(textFocusedDrawable);
} else {
textView.setBackgroundDrawable(textDrawable);
}
}
if(codeNum > 1) {
if(currentFocusPosition < codeNum) {
//字数小于总数,设置EditText的leftPadding,造成光标移动的错觉
editText.setCursorVisible(true);
float leftPadding = codeWidth / 2 + currentFocusPosition * codeWidth + currentFocusPosition * dividerWidth;
editText.setPadding((int) leftPadding, 0, 0, 0);
} else {
//字数大于总数,隐藏光标
editText.setCursorVisible(false);
}
}
}
简单来讲就是遍历数组,将对应的选中TextView设置为选中状态,其他则设置为未选中状态。在验证码字数大于1(这不废话嘛)的情况下,隐藏或者展示光标,并根据间距设置EditText的leftPadding值,逻辑很简单,看代码即可。
到这里我们的整个逻辑就已经实现了。看下效果图
看起来还是蛮像那么回事儿的。
使用
完成了主体逻辑后,我们给自定义View来添加几个公共方法:
/**
* 暴露公共方法,设置光标颜色
* @param drawableRes
*/
public void setCursorRes(@DrawableRes int drawableRes) {
try {
java.lang.reflect.Field f = TextView.class.getDeclaredField("mCursorDrawableRes");
f.setAccessible(true);
f.set(editText, drawableRes);
} catch (Exception e) {
}
}
/**
* 获取输入的验证码
* @return
*/
public String getContent() {
StringBuilder builder = new StringBuilder();
for(TextView tv : textViews) {
builder.append(tv.getText());
}
return builder.toString();
}
/**
* 判断是否验证码输入完毕
* @return
*/
public boolean isFinish() {
for(TextView tv : textViews) {
if(TextUtils.isEmpty(tv.getText())) {
return false;
}
}
return true;
}
/**
* 清除已输入验证码
*/
public void clear() {
for(TextView tv : textViews) {
tv.setText("");
}
currentFocusPosition = 0;
resetCursorPosition();
}
在xml中,我们可以这样使用该自定义View:
<com.xylitolz.androidverificationcode.view.VerificationCodeView
android:id="@+id/view_verification"
android:layout_marginTop="20dp"
android:layout_width="240dp"
android:layout_height="wrap_content"
app:vcv_code_size="16sp"
app:vcv_code_bg_focus="@drawable/bg_text_focused"
app:vcv_code_bg_normal="@drawable/bg_text_normal"
app:vcv_code_color="@color/text_border_focused"
app:vcv_code_number="4"
app:vcv_code_width="50dp"/>
接下来是在Activity中的使用演示:
OK,以上就是本次博客的全部内容了,如果您对本文有任何疑问或者文章有错误或者遗漏,请在评论留言告诉我,不胜感激~
本文代码地址github,欢迎fork~
我的个人博客,欢迎访问~
enjoy~
更多推荐
所有评论(0)