前两天在掘金上看到了一个验证码输入框,然后自己实现了一下,以前都是继承的 View,这次继承了 ViewGroup,也算是尝试了一点不同的东西。先看看最终效果:
事实上就是用将输入的密码用几个文本框来显示而已,要打造这样一个东西我刚开始也是一头雾水,不急,直接写不会,我们可以采取曲线救国的方法。下面我来说说我的思路。
1 准备工作
光看图上效果没有什么头绪的话,但是我相信下面这个布局大家肯定都会写:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.qinshou.passwordedittext.MainActivity">
<RelativeLayout
android:layout_width="0dp"
android:layout_height="80dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="@drawable/bg_inputing" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="@drawable/bg_inputed" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="@drawable/bg_inputed" />
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="@drawable/bg_inputed" />
</LinearLayout>
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
android:cursorVisible="false" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
这样布局写出来的效果是这样:
这就是我们要的效果吧,所以下面的工作就简单了,就是把这个布局给封装到一个自定义控件中去。这里面有好多 View,所以我们需要继承的是一个 ViewGroup,根据上面的布局,选择继承 RelativeLayout。
2 写PasswordEditText布局
我们需要在一个 RelativeLayout 中添加 1 个横向的 LinearLayout 用来装 4 个 用来显示的 EditText,为什么用 EditText 不用 TextView 是因为 EditText 可以密文显示。然后还需要一个 EditText 来弹出软键盘用来输入。
public class PasswordEditText extends RelativeLayout {
private Context mContext;
private EditText mEditText;
private List<EditText> editTexts; //选用 EditText 而不用 TextView 是因为 EditText 可以密文显示
public PasswordEditText(Context context) {
this(context, null);
}
public PasswordEditText(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initView();
}
/**
* Description:添加控件,密码框和不可见的输入框
* Date:2017/8/18
*/
private void initView() {
//新建一个容器
LinearLayout mLinearLayout = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
mLinearLayout.setLayoutParams(linearLayoutParams);
mLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
//根据 count 添加一定数量的密码框
editTexts = new ArrayList<EditText>();
LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams(getScreenWidth(mContext) / 4 - dip2px(mContext, 20), getScreenWidth(mContext) / count - dip2px(mContext, 20));
textViewParams.setMargins(dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10));
for (int i = 0; i < 4; i++) {
EditText mEditText = new EditText(mContext);
mEditText.setLayoutParams(textViewParams);
mEditText.setBackgroundResource(R.drawable.bg_inputed); //设置背景
mEditText.setGravity(Gravity.CENTER); //设置文本显示位置
mEditText.setTextSize(24); //设置文本大小
mEditText.setFocusable(false); //设置无法获得焦点
editTexts.add(mEditText);
mLinearLayout.addView(mEditText);
}
editTexts.get(0).setBackgroundResource(R.drawable.bg_inputing);
//添加不可见的 EditText
LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
mEditText = new EditText(mContext);
mEditText.setLayoutParams(editTextParams);
mEditText.setCursorVisible(false); //设置输入游标不可见
mEditText.setBackgroundResource(0); //设置透明背景,让下划线不可见
mEditText.setAlpha(0.0f); //设置为全透明,让输入的内容不可见
mEditText.setInputType(InputType.TYPE_CLASS_NUMBER); //设置只能输入数字
mEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(count)}); //限制输入长度
addView(mLinearLayout);
addView(mEditText);
}
/**
* Description:获取屏幕宽度
* Date:2017/8/18
*/
private int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return wm.getDefaultDisplay().getWidth();
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
}
现在的效果就跟刚才一样了:
3 添加输入监听器
光有布局还不行,我们要让它可以输入,让输入的内容显示到对应的 4 个密码框中,这里监听的肯定是输入的那个 EditText,为其添加 TextWatcher,这里介绍一下 TextWatcher 这个监听器。
TextWatch 有 3 个回调方法:
public void beforeTextChanged(CharSequence s, int start, int count, int after):这是监听内容改变前,s 是内容改变前的文本,start 是内容改变操作后输入光标所在位置,count 删除内容时是删除字符的个数,增加内容时为 0,after 增加内容时是增加字符的个数,删除内容时为 0。
public void onTextChanged(CharSequence s, final int start, int before, int count):监听内容改变后,s 是内容改变后的文本,start 是内容改变操作后光标的位置,count 增加内容时是增加字符的个数,删除内容时为 0,after 删除内容时是删除字符的个数,增加内容时为 0。
public void afterTextChanged(Editable s):监听内容改变后,s 为内容改变后的文本。
我们主要的操作就是在 onTextChanged() 中,增加第一个密码,显示到第一个密码框中,增加第二个,显示到第二个上,删除时也是从后往前依次删除。
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
/**
*
* @param s 输入后的文本
* @param start 输入的位置
* @param before 输入的位置上之前的字符数
* @param count 输入的位置上新输入的字符数
*/
@Override
public void onTextChanged(CharSequence s, final int start, int before, int count) {
if (before == 0 && count == 1) {
//为对应显示框设置对应显示内容
editTexts.get(start).setText(s.subSequence(start, start + 1));
//修改输入了内容的密码框的背景
editTexts.get(start).setBackgroundResource(R.drawable.bg_inputed);
//如果还有下一个密码框,将其背景设置为待输入的背景
if (start + 1 < editTexts.size()) {
editTexts.get(start + 1).setBackgroundResource(R.drawable.bg_inputing);
} else {
//输入完成后关闭软键盘
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}
} else if (before == 1 && count == 0) {
//清除退格位置对应显示框的内容
editTexts.get(start).setText("");
//将其退格的位置设置为明文显示
editTexts.get(start).setTransformationMethod(HideReturnsTransformationMethod.getInstance());
//设置退格位置的背景
for (EditText editText : editTexts) {
editText.setBackgroundResource(R.drawable.bg_inputed);
}
editTexts.get(start).setBackgroundResource(R.drawable.bg_inputing);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
现在效果已经初具成效了:
4 密文显示
然后我们需要在输入后一段时间变为密文显示,这里我选择了 0.5s,同时,在下一个密码框有输入时,如果上一个密码框还没有变为密文显示的话则立即将其设置为密文显示,这里同样是在 onTextChanged() 方法中操作:
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
/**
*
* @param s 输入后的文本
* @param start 输入的位置
* @param before 输入的位置上之前的字符数
* @param count 输入的位置上新输入的字符数
*/
@Override
public void onTextChanged(CharSequence s, final int start, int before, int count) {
if (before == 0 && count == 1) {
//为对应显示框设置对应显示内容
editTexts.get(start).setText(s.subSequence(start, start + 1));
//修改输入了内容的密码框的背景
editTexts.get(start).setBackgroundResource(R.drawable.bg_inputed);
//如果还有下一个密码框,将其背景设置为待输入的背景
if (start + 1 < editTexts.size()) {
editTexts.get(start + 1).setBackgroundResource(R.drawable.bg_inputing);
} else {
//输入完成后关闭软键盘
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}
//如果需要密文显示,则 0.5s 后设置为密文显示
editTexts.get(start).postDelayed(new Runnable() {
@Override
public void run() {
editTexts.get(start).setTransformationMethod(PasswordTransformationMethod.getInstance());
}
}, 500);
//如果上一个显示框还不是密文显示的话,立即将其设置为密文显示,前提是需要密文显示
if (start > 0 && editTexts.get(start - 1).getTransformationMethod() instanceof HideReturnsTransformationMethod) {
editTexts.get(start - 1).setTransformationMethod(PasswordTransformationMethod.getInstance());
}
} else if (before == 1 && count == 0) {
//清除退格位置对应显示框的内容
editTexts.get(start).setText("");
//将其退格的位置设置为明文显示
editTexts.get(start).setTransformationMethod(HideReturnsTransformationMethod.getInstance());
//设置退格位置的背景
for (EditText editText : editTexts) {
editText.setBackgroundResource(R.drawable.bg_inputed);
}
editTexts.get(start).setBackgroundResource(R.drawable.bg_inputing);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
效果如下:
5 添加自定义属性
至此,我已经实现了我想要的效果了,为了让它有更好的适应性,我们可以为其添加一些自定义属性,然后动态获取这些属性来更好的自定义这个控件,毕竟老去修改源码还是挺麻烦的:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PasswordEditText">
<!-- 密码框数量 -->
<attr name="count" format="integer" />
<!-- 密码字体大小 -->
<attr name="passwordSize" format="integer" />
<!-- 是否显示输入的密码 -->
<attr name="showPassword" format="boolean" />
<!-- 下一个待输入的密码框的背景 -->
<attr name="bgInputing" format="reference" />
<!-- 已经输入了密码框的背景 -->
<attr name="bgInputed" format="reference" />
</declare-styleable>
</resources>
如何获取自定义属性这个网上很多教程,我也不赘述如何设置,具体的可以看看最后的完整源码。
6 总结
这是第一次写继承一个 ViewGroup,自定义控件这个东西,可以说是一直都得学,但永远也学不完,因为需求总是在变的,但是万变不离其宗,多写几个控件,我相信再复杂的控件,我们在加以思考后也能实现。最近找到了新工作,真的感觉自己很幸运,公司不错,同事也很好,希望自己能够快速融入其中然后帮着做事,实现自己的价值。新公司不算是很忙,但是感觉自己很多不会的,在闲暇时间我要继续提高自己,对得起自己的工作。
7 源码
package com.qinshou.passwordedittext;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.HideReturnsTransformationMethod;
import android.text.method.PasswordTransformationMethod;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import java.util.ArrayList;
import java.util.List;
/**
* Description:密码输入框,也可明文显示,用作输入验证码
* Created by 禽兽先生
* Created on 2017/8/17
*/
public class PasswordEditText extends RelativeLayout {
private Context mContext;
private EditText mEditText;
private List<EditText> editTexts; //选用 EditText 而不用 TextView 是因为 EditText 可以密文显示
private int count = 4; //密码框数量
private int passwordSize = 24; //密码文本大小
private boolean showPassword = true; //密码是否密文显示,true 为一直明文显示,false 为 0.5s 后密文显示
private int bgInputing = R.drawable.bg_inputing; //待输入的密码框的背景
private int bgInputed = R.drawable.bg_inputed; //非待输入的密码框的背景
private onCompletionListener mOnCompletionListener;
public PasswordEditText(Context context) {
this(context, null);
}
public PasswordEditText(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initAttr(attrs, defStyleAttr);
initView();
addListener();
}
/**
* Description:初始化自定义属性
* Date:2017/8/19
*/
private void initAttr(AttributeSet attrs, int defStyleAttr) {
TypedArray mTypeArray = mContext.getTheme().obtainStyledAttributes(attrs, R.styleable.PasswordEditText, defStyleAttr, 0);
count = mTypeArray.getInt(R.styleable.PasswordEditText_count, 4);
passwordSize = mTypeArray.getInt(R.styleable.PasswordEditText_passwordSize, 24);
showPassword = mTypeArray.getBoolean(R.styleable.PasswordEditText_showPassword, true);
bgInputing = mTypeArray.getResourceId(R.styleable.PasswordEditText_bgInputing, R.drawable.bg_inputing);
bgInputed = mTypeArray.getResourceId(R.styleable.PasswordEditText_bgInputed, R.drawable.bg_inputed);
}
/**
* Description:添加控件,密码框和不可见的输入框
* Date:2017/8/18
*/
private void initView() {
//新建一个容器
LinearLayout mLinearLayout = new LinearLayout(mContext);
LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
mLinearLayout.setLayoutParams(linearLayoutParams);
mLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
//根据 count 添加一定数量的密码框
editTexts = new ArrayList<EditText>();
LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams(getScreenWidth(mContext) / count - dip2px(mContext, 20), getScreenWidth(mContext) / count - dip2px(mContext, 20));
textViewParams.setMargins(dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10));
for (int i = 0; i < count; i++) {
EditText mEditText = new EditText(mContext);
mEditText.setLayoutParams(textViewParams);
mEditText.setBackgroundResource(R.drawable.bg_inputed); //设置背景
mEditText.setGravity(Gravity.CENTER); //设置文本显示位置
mEditText.setTextSize(passwordSize); //设置文本大小
mEditText.setFocusable(false); //设置无法获得焦点
editTexts.add(mEditText);
mLinearLayout.addView(mEditText);
}
editTexts.get(0).setBackgroundResource(bgInputing);
//添加不可见的 EditText
LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
mEditText = new EditText(mContext);
mEditText.setLayoutParams(editTextParams);
mEditText.setCursorVisible(false); //设置输入游标不可见
mEditText.setBackgroundResource(0); //设置透明背景,让下划线不可见
mEditText.setAlpha(0.0f); //设置为全透明,让输入的内容不可见
mEditText.setInputType(InputType.TYPE_CLASS_NUMBER); //设置只能输入数字
mEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(count)}); //限制输入长度
addView(mLinearLayout);
addView(mEditText);
}
/**
* Description:为输入框添加监听器
* Date:2017/8/18
*/
private void addListener() {
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
/**
*
* @param s 输入后的文本
* @param start 输入的位置
* @param before 输入的位置上之前的字符数
* @param count 输入的位置上新输入的字符数
*/
@Override
public void onTextChanged(CharSequence s, final int start, int before, int count) {
if (before == 0 && count == 1) {
//为对应显示框设置对应显示内容
editTexts.get(start).setText(s.subSequence(start, start + 1));
//修改输入了内容的密码框的背景
editTexts.get(start).setBackgroundResource(bgInputed);
//如果还有下一个密码框,将其背景设置为待输入的背景
if (start + 1 < editTexts.size()) {
editTexts.get(start + 1).setBackgroundResource(bgInputing);
} else {
//输入完成后关闭软键盘
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
//如果添加了监听器,则回调
if (mOnCompletionListener != null) {
mOnCompletionListener.onCompletion(s.toString());
}
}
//如果需要密文显示,则 0.5s 后设置为密文显示
if (!showPassword) {
editTexts.get(start).postDelayed(new Runnable() {
@Override
public void run() {
editTexts.get(start).setTransformationMethod(PasswordTransformationMethod.getInstance());
}
}, 500);
}
//如果上一个显示框还不是密文显示的话,立即将其设置为密文显示,前提是需要密文显示
if (!showPassword && start > 0 && editTexts.get(start - 1).getTransformationMethod() instanceof HideReturnsTransformationMethod) {
editTexts.get(start - 1).setTransformationMethod(PasswordTransformationMethod.getInstance());
}
} else if (before == 1 && count == 0) {
//清除退格位置对应显示框的内容
editTexts.get(start).setText("");
//将其退格的位置设置为明文显示
editTexts.get(start).setTransformationMethod(HideReturnsTransformationMethod.getInstance());
//设置退格位置的背景
for (EditText editText : editTexts) {
editText.setBackgroundResource(bgInputed);
}
editTexts.get(start).setBackgroundResource(bgInputing);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
public void setOnCompleteListener(onCompletionListener onCompletionListener) {
this.mOnCompletionListener = onCompletionListener;
}
/**
* Description:获取屏幕宽度
* Date:2017/8/18
*/
private int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return wm.getDefaultDisplay().getWidth();
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
public interface onCompletionListener {
void onCompletion(String code);
}
}
这个小东西已经传到 Github 上了,传送门