编辑框EditText算是Android的一个基础控件了,表面上看,EditText只负责接收用户手工输入的文本;可实际上,要把这看似简单的文本输入做得方便易用,并不是一个简单的事情。因为用户可能希望App会更加智能一些,比如用户希望编辑框提供关键词联想功能,又比如用户希望编辑框能够自我纠错等等;所以,Android从设计之初就努力尝试解决这些问题,先是自带了自动完成编辑框AutoCompleteTextView,后来又在Android5.0以后提供了文本输入布局TextInputLayout。
然而,计划赶不上变化,开发工作中总有一些现有控件无法直接实现的需求,就像支付宝的支付密码输入框,在一排方格区域内输入并显示密文密码,每个密文字符之间又有竖线分隔。为直观理解支付密码输入框的业务需求,下面还是先看看该输入框的最终效果图。
从图中可以看出,这个支付密码输入框由六个方格组成,每个方格输入并显示第几位的密文字符。可是单张静态截图无法准确体现支付密码输入框的具体功能,因此我们再来看看使用该输入框的完整操作流程,相关动图如下所示。
由这张动图可以发现,支付密码输入框至少需要完成以下功能:
1、一开始边框是灰色的,获得焦点后边框变蓝色;
2、输入框一共六个方格,每个方格之间以竖线隔开;
3、每个方格只显示一个密码字符,且字符位于方格中央;
4、密码不显示明文,而是显示密文,比如点号(·)或者星号(*);
5、输完六位密码,应自动触发密码输入完成的事件;
因为支付密码允许一位一位输入,也允许一位一位删除,所以它本质上还是一个编辑框,也就是说,支付密码的输入框必须实现EditText的功能。当然,在界面展现上,需要以横排方格的形式加以显示。于是可以考虑,把支付密码的输入与显示操作分离开来,即密码输入操作仍由EditText处理,而密码显示操作则由自定义的方格布局接管。
对于处理密码输入的EditText来说,需要实现以下几项操作:
1、把默认的下划线背景替换为圆角背景,且支持在获得焦点时高亮显示;
2、屏蔽输入光标,可调用setCursorVisible方法设置为不可见;
3、把输入文字变成不可见,这里建议把文字颜色设为透明,而不是把文字大小设为0,因为若将大小设为0就无法自适应高度;
4、设置输入字符串的长度为6,设置长度操作可调用setFilters方法;
5、添加文本变更监听器,每当密码输入或者删除之时,就通知方格布局更新密文显示;同时还得监控输入字符数是否达到6位,如果达到6位就触发密码完成事件;
对于接管密码显示的方格布局来说,需要实现以下几项操作:
1、建立一个密码文本队列,队列长度为6;
2、每项密码文本控件都是一个TextView,文字居中对齐;
3、往布局上添加TextView队列时,在相邻的TextView之间要添加一条竖线,也就是宽度为1的灰色View;
4、依据转换规则,决定当前显示明文还是密文;如果是密文,则显示哪个密文字符;
5、每当EditText里的文本发生变更之时,相应更新TextView队列的各项文本显示;
上述的改造内容,大部分都有可以直接调用的函数,但有两个功能的实现要特别注意:
首先,对于密文字符,Android默认显示点号(·),可显示星号(*)也很常见,那有没有办法把系统默认的点号替换为星号呢?
这个需求看起来很简单,只要强行给TextView队列调用setText方法即可,然而这不是安全的做法,因为它丢弃了CharSequence中的丰富信息。正确的做法是调用setTransformationMethod方法,给TextView设置转换方式。恰好系统提供了一个字符替换的转换方式类即HideReturnsTransformationMethod,该类的关键代码如下所示:
其次,对于支付密码输入框的焦点获得问题,因为该输入框内部集成了EditText,所以不管是给输入框注册点击事件还是触摸事件,手势焦点都会被内部的EditText所抢占,使得密码输入框反而不会响应点击和触摸事件。详细的事件处理机制限于篇幅不再叙述,这里直接给出具体的解决步骤:
1、重写支付密码输入框布局的onInterceptTouchEvent方法,对所有触摸事件予以拦截,不让触摸事件传递给下级视图,代码如下所示:
下面是支付密码输入框控件的完整代码:
点此查看Android开发笔记的完整目录
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
然而,计划赶不上变化,开发工作中总有一些现有控件无法直接实现的需求,就像支付宝的支付密码输入框,在一排方格区域内输入并显示密文密码,每个密文字符之间又有竖线分隔。为直观理解支付密码输入框的业务需求,下面还是先看看该输入框的最终效果图。
从图中可以看出,这个支付密码输入框由六个方格组成,每个方格输入并显示第几位的密文字符。可是单张静态截图无法准确体现支付密码输入框的具体功能,因此我们再来看看使用该输入框的完整操作流程,相关动图如下所示。
由这张动图可以发现,支付密码输入框至少需要完成以下功能:
1、一开始边框是灰色的,获得焦点后边框变蓝色;
2、输入框一共六个方格,每个方格之间以竖线隔开;
3、每个方格只显示一个密码字符,且字符位于方格中央;
4、密码不显示明文,而是显示密文,比如点号(·)或者星号(*);
5、输完六位密码,应自动触发密码输入完成的事件;
因为支付密码允许一位一位输入,也允许一位一位删除,所以它本质上还是一个编辑框,也就是说,支付密码的输入框必须实现EditText的功能。当然,在界面展现上,需要以横排方格的形式加以显示。于是可以考虑,把支付密码的输入与显示操作分离开来,即密码输入操作仍由EditText处理,而密码显示操作则由自定义的方格布局接管。
对于处理密码输入的EditText来说,需要实现以下几项操作:
1、把默认的下划线背景替换为圆角背景,且支持在获得焦点时高亮显示;
2、屏蔽输入光标,可调用setCursorVisible方法设置为不可见;
3、把输入文字变成不可见,这里建议把文字颜色设为透明,而不是把文字大小设为0,因为若将大小设为0就无法自适应高度;
4、设置输入字符串的长度为6,设置长度操作可调用setFilters方法;
5、添加文本变更监听器,每当密码输入或者删除之时,就通知方格布局更新密文显示;同时还得监控输入字符数是否达到6位,如果达到6位就触发密码完成事件;
对于接管密码显示的方格布局来说,需要实现以下几项操作:
1、建立一个密码文本队列,队列长度为6;
2、每项密码文本控件都是一个TextView,文字居中对齐;
3、往布局上添加TextView队列时,在相邻的TextView之间要添加一条竖线,也就是宽度为1的灰色View;
4、依据转换规则,决定当前显示明文还是密文;如果是密文,则显示哪个密文字符;
5、每当EditText里的文本发生变更之时,相应更新TextView队列的各项文本显示;
上述的改造内容,大部分都有可以直接调用的函数,但有两个功能的实现要特别注意:
首先,对于密文字符,Android默认显示点号(·),可显示星号(*)也很常见,那有没有办法把系统默认的点号替换为星号呢?
这个需求看起来很简单,只要强行给TextView队列调用setText方法即可,然而这不是安全的做法,因为它丢弃了CharSequence中的丰富信息。正确的做法是调用setTransformationMethod方法,给TextView设置转换方式。恰好系统提供了一个字符替换的转换方式类即HideReturnsTransformationMethod,该类的关键代码如下所示:
private static char[] ORIGINAL = new char[] { '\r' }; private static char[] REPLACEMENT = new char[] { '\uFEFF' }; protected char[] getOriginal() { return ORIGINAL; } protected char[] getReplacement() { return REPLACEMENT; }这几行代码的意思是,把回车符('\r')替换为Unicode编码的空格('\uFEFF'),其中getOriginal表示返回需要替换的字符列表,getReplacement表示返回替换后的字符列表。所以,若想把密码文本替换成点号或者星号,即可依样画葫芦,把数字字符('0'到'9')替换为'\u2022'(点号的Unicode编码)或者'\u002A'(星号的Unicode编码)。
其次,对于支付密码输入框的焦点获得问题,因为该输入框内部集成了EditText,所以不管是给输入框注册点击事件还是触摸事件,手势焦点都会被内部的EditText所抢占,使得密码输入框反而不会响应点击和触摸事件。详细的事件处理机制限于篇幅不再叙述,这里直接给出具体的解决步骤:
1、重写支付密码输入框布局的onInterceptTouchEvent方法,对所有触摸事件予以拦截,不让触摸事件传递给下级视图,代码如下所示:
public boolean onInterceptTouchEvent(MotionEvent ev) { return true; }2、给支付密码输入框以及其它编辑框控件注册触摸监听器,并对触摸动作进行处理,在触摸密码输入框时强行使之获得焦点,处理触摸动作的代码如下所示:
public boolean onTouch(View v, MotionEvent event) { if (v.getId() == R.id.et_account) { et_account.setCursorVisible(true); } else if (v.getId() == R.id.ppi_password) { et_account.setCursorVisible(false); et_account.clearFocus(); ppi_password.requestFocus(); } return false; }如此改进之后,本文开头的支付密码输入框也就具备了应有的输入和显示功能。
下面是支付密码输入框控件的完整代码:
public class PayPasswodInput extends RelativeLayout implements TextWatcher { private final static String TAG = "PayPasswodInput"; private Context mContext; private EditText mEditText; // 文本编辑框,实际看不见 private LinearLayout mShowLayout; // 真正显示着的文本区域 private TextView[] mTextViews; // 分隔开的密码框 private int mBorderColor = Color.GRAY; // 边框与分隔线颜色 private int mPasswordColor = Color.BLACK; // 密码文字颜色 private int mPasswordSize = 30; // 密码文字大小 private int mPasswordLength = 6; // 密码长度 private TransformationMethod mPasswordMethod; // 密码的显示方式 private int mSplitWidth; // 分隔线的宽度 public PayPasswodInput(Context context) { this(context, null); } public PayPasswodInput(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PayPasswodInput(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; mBorderColor = mContext.getResources().getColor(R.color.gray); mSplitWidth = Utils.dp2px(mContext, 1); mPasswordMethod = HideReturnsTransformationMethod.getInstance(); } public void setPasswordStyle(int pwd_color, int pwd_size, int pwd_length, boolean pwd_show, int pwd_type) { mPasswordColor = pwd_color; mPasswordSize = pwd_size; mPasswordLength = pwd_length; mPasswordMethod = pwd_show ? HideReturnsTransformationMethod.getInstance() : //明文密码 StarTransformationMethod.getInstance(pwd_type); //密文密码 removeAllViews(); showTextLayout(); } private void showTextLayout() { LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); // 添加看不见的编辑框 mEditText = new EditText(mContext); mEditText.setBackgroundResource(R.drawable.editext_selector); mEditText.setCursorVisible(false); mEditText.setTextSize(mPasswordSize); mEditText.setTextColor(Color.TRANSPARENT); // 设置最大长度 mEditText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(mPasswordLength) }); mEditText.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD | InputType.TYPE_CLASS_NUMBER); mEditText.addTextChangedListener(this); addView(mEditText, layoutParams); // 添加可见的密码框布局 mShowLayout = new LinearLayout(mContext); mShowLayout.setLayoutParams(layoutParams); mShowLayout.setGravity(Gravity.CENTER); mShowLayout.setOrientation(LinearLayout.HORIZONTAL); addView(mShowLayout); // 添加密码文本队列 mTextViews = new TextView[mPasswordLength]; LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams( 0, LayoutParams.WRAP_CONTENT, 1); textParams.gravity = Gravity.CENTER; LinearLayout.LayoutParams splitParams = new LinearLayout.LayoutParams( mSplitWidth, LayoutParams.MATCH_PARENT); for (int i = 0; i < mTextViews.length; i++) { TextView textView = new TextView(mContext); textView.setLayoutParams(textParams); textView.setGravity(Gravity.CENTER); textView.setTextSize(mPasswordSize); textView.setTextColor(mPasswordColor); textView.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD | InputType.TYPE_CLASS_NUMBER); textView.setTransformationMethod(mPasswordMethod); textView.setPadding(0, Utils.dp2px(mContext, 5), 0, 0); mTextViews[i] = textView; mShowLayout.addView(mTextViews[i]); if (i < mTextViews.length - 1) { View view = new View(mContext); view.setBackgroundColor(mBorderColor); mShowLayout.addView(view, splitParams); } } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { Editable edit = mEditText.getText(); Selection.setSelection(edit, edit.length()); } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (s.length() > 0) { int length = s.length(); for (int i = 0; i < mPasswordLength; i++) { if (i < length) { for (int j = 0; j < length; j++) { char ch = s.charAt(j); mTextViews[j].setText(String.valueOf(ch)); } } else { mTextViews[i].setText(""); } } } else { for (int i = 0; i < mPasswordLength; i++) { mTextViews[i].setText(""); } } if (s.length() == mPasswordLength) { if (onPasswordFinishListener != null) { onPasswordFinishListener.onFinishPassword(s.toString().trim()); } } } private OnPasswordFinishListener onPasswordFinishListener; public void setOnPasswordFinishListener(OnPasswordFinishListener listener) { onPasswordFinishListener = listener; if (mEditText == null) { showTextLayout(); } } public interface OnPasswordFinishListener { void onFinishPassword(String password); } }
点此查看Android开发笔记的完整目录
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
作者:aqi00 发表于2017/6/5 9:19:48 原文链接
阅读:148 评论:0 查看评论