概述
先描述一下具体需求吧,我们在项目中可能会遇到修改用户名及密码的需求,为保证一定的完全性,服务端一般会接入短信验证码的功能。我们需要将接受到的验证码返回给服务端进行验证。可能会有以下的界面让用户输入验证码:
实现
1. 特性
- 支持设置框数量
- 支持设置框的风格样式
- 支持根据状态区分框颜色
- 基于EditText实现,更优雅
2. 效果图
3. 属性
4. 代码
- attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SplitEditText">
<attr name="setStrokeWidth" format="dimension"/>
<attr name="setBorderColor" format="color"/>
<attr name="setInputBorderColor" format="color"/>
<attr name="setFocusBorderColor" format="color"/>
<attr name="setBoxBackgroundColor" format="color"/>
<attr name="setBorderCornerRadius" format="dimension"/>
<attr name="setBorderSpacing" format="dimension"/>
<attr name="setMaxLength" format="integer"/>
<attr name="setBorderStyle" format="enum">
<enum name="box" value="0"/>
<enum name="line" value="1"/>
</attr>
<attr name="setTextStyle" format="enum">
<enum name="plain_text" value="0"/>
<enum name="cipher_text" value="1"/>
</attr>
<attr name="setCipherMask" format="string"/>
<attr name="setFakeBoldText" format="boolean"/>
</declare-styleable>
</resources>
- SplitEditText.java
public class SplitEditText extends AppCompatEditText {
/**
* 画笔
*/
private Paint mPaint;
/**
* 画笔宽度
*/
private float mStrokeWidth;
/**
* 边框颜色
*/
private int mBorderColor = 0xFF666666;
/**
* 输入的边框颜色
*/
private int mInputBorderColor = 0xFF1E90FF;
/**
* 焦点的边框颜色
*/
private int mFocusBorderColor;
/**
* 框的背景颜色
*/
private int mBoxBackgroundColor;
/**
* 框的圆角大小
*/
private float mBorderCornerRadius;
/**
* 框与框之间的间距大小
*/
private float mBorderSpacing;
/**
* 输入框宽度
*/
private float mBoxWidth;
/**
* 输入框高度
*/
private float mBoxHeight;
/**
* 允许输入的最大长度
*/
private int mMaxLength = 6;
/**
* 文本长度
*/
private int mTextLength;
/**
* 路径
*/
private Path mPath;
private RectF mRectF;
private float[] mRadiusFirstArray;
private float[] mRadiusLastArray;
/**
* 边框风格
*/
private @BorderStyle int mBorderStyle = BorderStyle.BOX;
/**
* 边框风格
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({BorderStyle.BOX, BorderStyle.LINE})
public @interface BorderStyle {
/**
* 框
*/
int BOX = 0;
/**
* 线
*/
int LINE = 1;
}
/**
* 文本风格
*/
private @TextStyle int mTextStyle = TextStyle.PLAIN_TEXT;
/**
* 文本风格
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({TextStyle.PLAIN_TEXT, TextStyle.CIPHER_TEXT})
public @interface TextStyle {
/**
* 明文
*/
int PLAIN_TEXT = 0;
/**
* 密文
*/
int CIPHER_TEXT = 1;
}
/**
* 密文掩码
*/
private String mCipherMask;
/**
* 是否是粗体
*/
private boolean isFakeBoldText;
private static final String DEFAULT_CIPHER_MASK = "*";
private boolean isDraw;
private OnTextInputListener mOnTextInputListener;
public SplitEditText(@NonNull Context context) {
this(context,null);
}
public SplitEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs,android.R.attr.editTextStyle);
}
public SplitEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}
private void init(@NonNull Context context, @Nullable AttributeSet attrs){
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
mStrokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1f,displayMetrics);
mBorderSpacing = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,8f,displayMetrics);
setPadding(0,0,0,0);
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.SplitEditText);
final int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = a.getIndex(i);
if(attr == R.styleable.SplitEditText_setStrokeWidth){
mStrokeWidth = a.getDimension(attr,mStrokeWidth);
}else if (attr == R.styleable.SplitEditText_setBorderColor){
mBorderColor = a.getColor(attr,mBorderColor);
}else if (attr == R.styleable.SplitEditText_setInputBorderColor){
mInputBorderColor = a.getColor(attr,mInputBorderColor);
}else if (attr == R.styleable.SplitEditText_setFocusBorderColor){
mFocusBorderColor = a.getColor(attr,mFocusBorderColor);
}else if (attr == R.styleable.SplitEditText_setBoxBackgroundColor){
mBoxBackgroundColor = a.getColor(attr,mBoxBackgroundColor);
}else if (attr == R.styleable.SplitEditText_setBorderCornerRadius){
mBorderCornerRadius = a.getDimension(attr,mBorderCornerRadius);
}else if (attr == R.styleable.SplitEditText_setBorderSpacing){
mBorderSpacing = a.getDimension(attr,mBorderSpacing);
}else if (attr == R.styleable.SplitEditText_setMaxLength){
mMaxLength = a.getInt(attr,mMaxLength);
}else if (attr == R.styleable.SplitEditText_setBorderStyle){
mBorderStyle = a.getInt(attr,mBorderStyle);
}else if (attr == R.styleable.SplitEditText_setTextStyle){
mTextStyle = a.getInt(attr,mTextStyle);
}else if (attr == R.styleable.SplitEditText_setCipherMask){
mCipherMask = a.getString(attr);
}else if (attr == R.styleable.SplitEditText_setFakeBoldText){
isFakeBoldText = a.getBoolean(attr,false);
}
}
a.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setTextAlign(Paint.Align.CENTER);
mPath = new Path();
mRadiusFirstArray = new float[8];
mRadiusLastArray = new float[8];
mRectF = new RectF(0,0,0,0);
if(TextUtils.isEmpty(mCipherMask)){
mCipherMask = DEFAULT_CIPHER_MASK;
}else if(mCipherMask.length() > 1){
mCipherMask = mCipherMask.substring(0,1);
}
setBackground(null);
setCursorVisible(false);
setFilters(new InputFilter[]{new InputFilter.LengthFilter(mMaxLength)});
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int width = w - getPaddingLeft() - getPaddingRight();
int height = h - getPaddingTop() - getPaddingBottom();
updateSizeChanged(width,height);
}
private void updateSizeChanged(int width,int height){
//如果框与框之间的间距小于0或者总间距大于控件可用宽度则将间距重置为0
if(mBorderSpacing < 0 || (mMaxLength - 1) * mBorderSpacing > width){
mBorderSpacing = 0;
}
//计算出每个框的宽度
mBoxWidth = (width - (mMaxLength - 1) * mBorderSpacing) / mMaxLength - mStrokeWidth;
mBoxHeight = height - mStrokeWidth;
}
@Override
protected void onDraw(Canvas canvas) {
//移除super.onDraw(canvas);不绘制EditText相关的
//绘制边框
drawBorders(canvas);
}
private void drawBorders(Canvas canvas){
isDraw = true;
//遍历绘制未输入文本的框边界
for(int i = mTextLength; i < mMaxLength; i++){
drawBorder(canvas,i,mBorderColor);
}
int color = mInputBorderColor != 0 ? mInputBorderColor : mBorderColor;
//遍历绘制已输入文本的框边界
for(int i = 0; i < mTextLength; i++){
drawBorder(canvas,i,color);
}
//绘制焦点框边界
if(mTextLength < mMaxLength && mFocusBorderColor != 0 && isFocused()){
drawBorder(canvas,mTextLength,mFocusBorderColor);
}
}
private void drawBorder(Canvas canvas,int position,int borderColor){
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setFakeBoldText(false);
mPaint.setColor(borderColor);
//计算出对应的矩形
float left = getPaddingLeft() + mStrokeWidth / 2 + (mBoxWidth + mBorderSpacing) * position;
float top = getPaddingTop() + mStrokeWidth / 2;
mRectF.set(left,top,left + mBoxWidth,top + mBoxHeight);
//边框风格
switch (mBorderStyle){
case BorderStyle.BOX:
drawBorderBox(canvas,position,borderColor);
break;
case BorderStyle.LINE:
drawBorderLine(canvas);
break;
}
if(mTextLength > position && !TextUtils.isEmpty(getText())){
drawText(canvas,position);
}
}
private void drawText(Canvas canvas,int position){
mPaint.setStrokeWidth(0);
mPaint.setColor(getCurrentTextColor());
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setTextSize(getTextSize());
mPaint.setFakeBoldText(isFakeBoldText);
float x = mRectF.centerX();
//y轴坐标 = 中心线 + 文字高度的一半 - 基线到文字底部的距离(也就是bottom)
float y = mRectF.centerY() + (mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top) / 2 - mPaint.getFontMetrics().bottom;
switch (mTextStyle){
case TextStyle.PLAIN_TEXT:
canvas.drawText(String.valueOf(getText().charAt(position)),x,y,mPaint);
break;
case TextStyle.CIPHER_TEXT:
canvas.drawText(mCipherMask,x,y,mPaint);
break;
}
}
/**
* 绘制框风格
* @param canvas
* @param position
*/
private void drawBorderBox(Canvas canvas,int position,int borderColor){
if(mBorderCornerRadius > 0){//当边框带有圆角时
if(mBorderSpacing == 0){//当边框之间的间距为0时,只需要开始一个和最后一个框有圆角
if(position == 0 || position == mMaxLength - 1){
if(mBoxBackgroundColor != 0){
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBoxBackgroundColor);
canvas.drawPath(getRoundRectPath(mRectF,position == 0),mPaint);
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(borderColor);
canvas.drawPath(getRoundRectPath(mRectF,position == 0),mPaint);
}else{
if(mBoxBackgroundColor != 0){
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBoxBackgroundColor);
canvas.drawRect(mRectF,mPaint);
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(borderColor);
canvas.drawRect(mRectF,mPaint);
}
}else{
if(mBoxBackgroundColor != 0){
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBoxBackgroundColor);
canvas.drawRoundRect(mRectF,mBorderCornerRadius,mBorderCornerRadius,mPaint);
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(borderColor);
canvas.drawRoundRect(mRectF,mBorderCornerRadius,mBorderCornerRadius,mPaint);
}
}else{
if(mBoxBackgroundColor != 0){
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mBoxBackgroundColor);
canvas.drawRect(mRectF,mPaint);
}
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(borderColor);
canvas.drawRect(mRectF,mPaint);
}
}
/**
* 绘制线风格
* @param canvas
*/
private void drawBorderLine(Canvas canvas){
float y = getPaddingTop() + mBoxHeight;
canvas.drawLine(mRectF.left,y,mRectF.right,y,mPaint);
}
private Path getRoundRectPath(RectF rectF,boolean isFirst){
mPath.reset();
if(isFirst){
//左上角
mRadiusFirstArray[0] = mBorderCornerRadius;
mRadiusFirstArray[1] = mBorderCornerRadius;
//左下角
mRadiusFirstArray[6] = mBorderCornerRadius;
mRadiusFirstArray[7] = mBorderCornerRadius;
mPath.addRoundRect(rectF,mRadiusFirstArray, Path.Direction.CW);
}else{
//右上角
mRadiusLastArray[2] = mBorderCornerRadius;
mRadiusLastArray[3] = mBorderCornerRadius;
//右下角
mRadiusLastArray[4] = mBorderCornerRadius;
mRadiusLastArray[5] = mBorderCornerRadius;
mPath.addRoundRect(rectF,mRadiusLastArray, Path.Direction.CW);
}
return mPath;
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
mTextLength = text.length();
refreshView();
//改变监听
if(mOnTextInputListener != null){
mOnTextInputListener.onTextInputChanged(text.toString(),mTextLength);
if(mTextLength == mMaxLength){
mOnTextInputListener.onTextInputCompleted(text.toString());
}
}
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (selStart == selEnd) {
setSelection(getText() == null ? 0 : getText().length());
}
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
//焦点改变时刷新状态
refreshView();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isDraw = false;
}
public int getBorderColor() {
return mBorderColor;
}
public int getInputBorderColor() {
return mInputBorderColor;
}
public int getFocusBorderColor() {
return mFocusBorderColor;
}
public int getBoxBackgroundColor() {
return mBoxBackgroundColor;
}
public float getBorderCornerRadius() {
return mBorderCornerRadius;
}
public float getBorderSpacing() {
return mBorderSpacing;
}
@BorderStyle
public int getBorderStyle() {
return mBorderStyle;
}
public void setBorderColor(int borderColor) {
this.mBorderColor = borderColor;
refreshView();
}
public void setInputBorderColor(int inputBorderColor) {
this.mInputBorderColor = inputBorderColor;
refreshView();
}
public void setFocusBorderColor(int focusBorderColor) {
this.mFocusBorderColor = focusBorderColor;
refreshView();
}
public void setBoxBackgroundColor(int boxBackgroundColor) {
this.mBoxBackgroundColor = boxBackgroundColor;
refreshView();
}
public void setBorderCornerRadius(float borderCornerRadius) {
this.mBorderCornerRadius = borderCornerRadius;
refreshView();
}
public void setBorderSpacing(float borderSpacing) {
this.mBorderSpacing = borderSpacing;
refreshView();
}
public void setBorderStyle(@TextStyle int borderStyle) {
this.mBorderStyle = borderStyle;
refreshView();
}
@TextStyle
public int getTextStyle() {
return mTextStyle;
}
public void setTextStyle(@TextStyle int textStyle) {
this.mTextStyle = textStyle;
refreshView();
}
public String getCipherMask() {
return mCipherMask;
}
/**
* 是否粗体
* @param fakeBoldText
*/
public void setFakeBoldText(boolean fakeBoldText) {
isFakeBoldText = fakeBoldText;
refreshView();
}
/**
* 设置密文掩码 不设置时,默认为{@link #DEFAULT_CIPHER_MASK}
* @param cipherMask
*/
public void setCipherMask(String cipherMask) {
this.mCipherMask = cipherMask;
refreshView();
}
/**
* 刷新视图
*/
private void refreshView(){
if(isDraw){
invalidate();
}
}
/**
* 设置文本输入监听
* @param onTextInputListener
*/
public void setOnTextInputListener(OnTextInputListener onTextInputListener) {
this.mOnTextInputListener = onTextInputListener;
}
public static abstract class OnSimpleTextInputListener implements OnTextInputListener{
@Override
public void onTextInputChanged(String text, int length) {
}
}
/**
* 文本输入监听
*/
public interface OnTextInputListener{
/**
* Text改变监听
* @param text
* @param length
*/
void onTextInputChanged(String text,int length);
/**
* Text输入完成
* @param text
*/
void onTextInputCompleted(String text);
}
}
- 使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
tools:context=".MainActivity">
<com.hjq.bar.TitleBar
android:layout_width="match_parent"
android:layout_height="60dp"
app:title="验证码"/>
<com.hkt.textsurfacedemo.view.SplitEditText
android:id="@+id/set_number"
android:layout_width="match_parent"
android:layout_height="45dp"
android:inputType="number"/>
<com.hkt.textsurfacedemo.view.SplitEditText
android:id="@+id/set_box"
android:layout_width="match_parent"
android:layout_height="45dp"
android:inputType="number"
android:layout_marginTop="20dp"
app:setFakeBoldText="true"
app:setBorderStyle="box"
app:setTextStyle="plain_text"
app:setBoxBackgroundColor="@color/tab_gray"
app:setBorderCornerRadius="10dp"/>
<com.hkt.textsurfacedemo.view.SplitEditText
android:id="@+id/set_box3"
android:layout_width="match_parent"
android:layout_height="45dp"
android:inputType="number"
android:layout_marginTop="20dp"
app:setMaxLength="6"
app:setBorderStyle="line"/>
</LinearLayout>
//设置监听
splitEditText.setOnTextInputListener(new SplitEditText.OnTextInputListener(){
@Override
public void onTextInputChanged(String text, int length) {
//TODO 文本输入改变
}
@Override
public void onTextInputCompleted(String text) {
//TODO 文本输入完成
}
});