以下为源码
package com.zmk.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.example.common.MyLog;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class EditTextInputView extends View {
//对外提供提交方法
private OnEnterClickListener onEnterClickListener;
//字符数量,>>>对外开放
private int textLengh = 6;
//框框/下划线宽度,根据手机宽度计算出来,受限于rectMaxWidth
private int rectWidth;
//框框/下划线间距,根据框框宽度计算出来
private int rectMargin;
//框框间宽比
private float rectMarginWidthRatio = 0.2f;
//全框框宽与(屏幕-横杠长度宽)比
private float rectWidthWidthRatio = 0.85f;
//框框宽高比
private float rectAspectRatio = 0.72f;
//框框高度,根据宽高比和宽度计算出来
private int rectHeight;
//文字大小,根据框框宽度和文字大小与框框宽度比计算
private int textSize;
//文字大小与框框宽度比
private float textSizeRectWidthRatio = 0.75f;
//横杠宽度,根据宽度和横杠宽度与宽度比计算
private int lineWidth;
//横杠宽度与宽度比
private float lineWidthWidthRatio = 0.4f;
//横杠高度,根据横杠宽度计算
private int lineHeight;
//横杠高度与横杠宽度比
private float lineHeightLineWidthRatio = 0.15f;
//圆角大小,根据宽度和圆角半径与宽比计算
private int radius;
//宽与圆角半径比
private float widthRadiusRatio = 0.2f;
//框框/下划线粗细
private int rectStroke = 3;
//文字/游标颜色,>>>对外开放
private int textColor = Color.BLACK;
//文字显示,>>>对外开放
private boolean textVisable = true;
//横杠显示,>>>对外开放
private boolean lineVisable = false;
//框框风格
private final int STYLE_RECT = 0;
//下划线风格
private final int STYLE_UNDERLINE = 1;
//背景风格,>>>对外开放
private int style = STYLE_RECT;
//游标消失时间,毫秒
private int cursorDisappearanceTime = 600;
//游标长度,根据框框高度和游标长度与框框高度比计算
private int cursorHeight;
//游标长度与框框高度比
private float cursorHeightRectHeighRatio = 0.55f;
//游标直径,根据游标长度和游标长度与游标直径比计算
private int cursorWidth;
//游标直径和游标长度比
private float cursorWidthcursorHeightRatio = 0.1f;
//游标显示
private boolean cursorVisible = true;
//最大框框宽度
private int rectMaxWidth = 130;
//父控件宽度
private int width;
//父控件高度
private int height;
//框框/下划线画笔
private Paint rectPaint;
//横杠画笔
private Paint linePaint;
//框框/下划线颜色,>>>对外开放
private int rectColor = Color.BLACK;
//框框/下划线选中时颜色,>>>对外开放
private int rectSelectColor = Color.BLUE;
//游标画笔
private Paint cursorPaint;
//文字画笔
private Paint textPaint;
//矩形列表
private List<Rect> rects;
//焦点下标
private int rectIndex = -1;
//计时器
private Timer timer;
//计时任务
private TimerTask timerTask;
//字符数组
private List<String> texts;
private InputMethodManager inputManager;
private boolean isFoucus;
public EditTextInputView(Context context) {
super(context);
init();
}
public EditTextInputView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public EditTextInputView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
//初始化
private void init() {
//重写onCreateInputConnection方法必须,使具备输入焦点
setFocusableInTouchMode(true);
//获取Mannager,能用来打开软键盘
inputManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
//获取事件类型
int action = keyEvent.getAction();
if (action == KeyEvent.ACTION_DOWN) {
//按下事件
if (keyCode == KeyEvent.KEYCODE_DEL) {
//删除键
//删除最后一个字符
removeLastText();
return true;
} else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
//数字键
//在第一个空位添加字符
addText(String.valueOf(keyCode - 7));
//当输入后充满输入框
if (texts.size() >= textLengh) {
//隐藏软键盘
inputManager.hideSoftInputFromWindow(EditTextInputView.this.getWindowToken(), 0);
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_ENTER) {
//Enter键
return true;
}
} else if (action == KeyEvent.ACTION_UP) {
//抬起事件
if (keyCode == KeyEvent.KEYCODE_ENTER) {
//Enter键
//提交方法,含对外接口
commit();
return true;
}
}
return false;
}
});
//框框画笔
rectPaint = new Paint();
rectPaint.setColor(rectColor);
rectPaint.setStyle(Paint.Style.STROKE);//空心
rectPaint.setStrokeWidth(rectStroke);
//横杠画笔
linePaint = new Paint();
linePaint.setColor(rectColor);
//游标画笔
cursorPaint = new Paint();
cursorPaint.setColor(textColor);
//文字画笔
textPaint = new Paint();
textPaint.setColor(textColor);
//矩形列表
rects = new ArrayList<>();
//创建计时器任务
timerTask = new TimerTask() {
@Override
public void run() {
//改变游标的显示状态
cursorVisible = !cursorVisible;
postInvalidate();
}
};
//创建计时器
timer = new Timer();
//初始化字符集合
texts = new ArrayList<>();
//默认 Enter键点击监听事件
onEnterClickListener = new OnEnterClickListener() {
@Override
public void onClick() {
//收起软键盘
inputManager.hideSoftInputFromWindow(EditTextInputView.this.getWindowToken(), 0);
}
};
//当字符长度为奇数时,或使用下划线样式时,不显示不计算横杠
if (textLengh % 2 != 0 || style == STYLE_UNDERLINE) {
lineVisable = false;
}
//在极端情况下的健壮性
if (textLengh > 6) {
textLengh = 6;
}
}
//带参初始化
private void init(Context context, AttributeSet attrs) {
//获取xml文件中的自定义属性列表
TypedArray typedArray = context.getResources().obtainAttributes(attrs, R.styleable.EditTextInputView);
//加载文字最大长度
textLengh = typedArray.getInteger(R.styleable.EditTextInputView_text_lengh, textLengh);
//加载文字颜色
textColor = typedArray.getColor(R.styleable.EditTextInputView_text_color, textColor);
//加载文字显示模式
textVisable = typedArray.getBoolean(R.styleable.EditTextInputView_text_visable, textVisable);
//加载横杠显示模式
lineVisable = typedArray.getBoolean(R.styleable.EditTextInputView_line_visable, lineVisable);
//加载样式
style = typedArray.getInt(R.styleable.EditTextInputView_style, style);
//加载背景颜色
rectColor = typedArray.getColor(R.styleable.EditTextInputView_background_color, rectColor);
//加载选中背景颜色
rectSelectColor = typedArray.getColor(R.styleable.EditTextInputView_backgroud_select_color, rectSelectColor);
//复用无参初始化
init();
}
//测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取默认测量宽度
width = MeasureSpec.getSize(widthMeasureSpec);
//获取默认测量高度
height = MeasureSpec.getSize(heightMeasureSpec);
//获取宽度模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//获取高度模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//声明包裹内容宽度
int widthMeasure;
//声明包裹内容高度
int heightMeasure;
//根据是否需要绘制横杠,计算包裹内容宽度
if (lineVisable) {
widthMeasure = (int) (rectMaxWidth * textLengh + rectMaxWidth * rectMarginWidthRatio * (textLengh - 1));
} else {
widthMeasure = (int) (rectMaxWidth * textLengh + rectMaxWidth * rectMarginWidthRatio * (textLengh - 1)) + lineWidth;
}
//在极端情况下的健壮性
if (textLengh <= 0) {
widthMeasure = rectMaxWidth;
}
//设置高度,末尾的数字为修正属性,没有特殊意义
heightMeasure = (int) (rectMaxWidth / rectAspectRatio) + 10;
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//当纵横都是包裹自身
setMeasuredDimension(widthMeasure, heightMeasure);
} else if (widthMode == MeasureSpec.AT_MOST) {
//当宽度为包裹自身
setMeasuredDimension(widthMeasure, height);
} else if (heightMode == MeasureSpec.AT_MOST) {
//当高度为包裹自身
setMeasuredDimension(width, heightMeasure);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//重新测量父容器的宽与高
width = getMeasuredWidth();
height = getMeasuredHeight();
//根据横杠显示状态测量框框宽度
if (lineVisable) {
//获得框框宽和间隔宽之和
rectWidth = (int) ((width - lineWidth) / (float) textLengh);
//最终框框宽度值
rectWidth = (int) (rectWidth / (float) (1 + rectMarginWidthRatio));
} else {
rectWidth = (int) (width / (float) textLengh);
rectWidth = (int) (rectWidth / (1 + rectMarginWidthRatio));
}
//如果宽度不合适,即计算出的框框宽度大于最大宽度,则使用最大宽度
if (rectWidth > rectMaxWidth) {
rectWidth = rectMaxWidth;
}
//根据框框宽度与宽高比计算出【框框高度】
rectHeight = (int) (rectWidth / rectAspectRatio);
//如果高度不合适,即计算出的框框高度大于父控件高度,再重新根据高度测量【框框宽度】
if (rectHeight > height) {
rectHeight = height;
rectWidth = (int) (rectHeight * rectAspectRatio);
rectMargin = (int) (rectWidth * rectMarginWidthRatio);
}
//根据框框宽度计算出【框框间隔】
rectMargin = (int) (rectWidth * rectMarginWidthRatio);
//根据框框宽度计算出【圆角半径】
radius = (int) (rectWidth * widthRadiusRatio);
//根据框框宽度计算出【横杠宽度】
lineWidth = (int) (rectWidth * lineWidthWidthRatio);
//根据横杠宽度计算出【横杠高度】
lineHeight = (int) (lineWidth * lineHeightLineWidthRatio);
//根据框框高度计算出【游标长度】
cursorHeight = (int) (rectHeight * cursorHeightRectHeighRatio);
//根据游标长度计算出【游标宽度】
cursorWidth = (int) (cursorHeight * cursorWidthcursorHeightRatio);
//为游标画笔设置宽度
cursorPaint.setStrokeWidth(cursorWidth);
//根据框框宽度计算出【文字大小】
textSize = (int) (rectWidth * textSizeRectWidthRatio);
//为文字画笔设置文字大小
textPaint.setTextSize(textSize);
}
//绘制
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int middleX;
int middleY;
//每一个子背景的绘制属性
int left;
int top;
int right;
int bottom;
//中心点的坐标
middleX = width / 2;
middleY = height / 2;
top = middleY - rectHeight / 2;
bottom = middleY + rectHeight / 2;
//绘制文字,将会影响 rectIndex
for (int i = 0; i < texts.size(); i++) {
String s = texts.get(i);
//解决文字居中问题
Paint.FontMetrics fm = textPaint.getFontMetrics();
int textX = rects.get(i).left + rectWidth / 2 - textSize / 4;
int textY = (int) (middleY - (fm.descent - (-fm.ascent + fm.descent) / 2));
//根据文字是否显示,来显示具体文字或显示 *
if (textVisable) {
canvas.drawText(s, textX, textY, textPaint);
} else {
canvas.drawText("*", textX, textY, textPaint);
}
//游标永远指向最后一个文字的下一个位置
rectIndex = i + 1;
}
//处理最后一个字符被删除,上述循环无法执行的情况
if (texts.size() == 0 && rectIndex != -1) {
rectIndex = 0;
}
//如果View失焦,将游标下标改为-1,以隐藏游标
if (!isFoucus){
rectIndex = -1;
}
//绘制框框/下划线,会受到 rectIndex影响
//循环次数为字符数量
for (int i = 0; i < textLengh; i++) {
if (textLengh % 2 == 0) {
//字符长度为偶数
if (lineVisable) {
//有横杠
left = i < textLengh / 2 ?
middleX - rectWidth * (textLengh / 2 - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 - lineWidth / 2 :
middleX + rectWidth * (i - textLengh / 2) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2 + lineWidth / 2;
right = i < textLengh / 2 ?
middleX - rectWidth * ((textLengh / 2 - 1) - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 - lineWidth / 2 :
middleX + rectWidth * (i - (textLengh / 2 - 1)) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2 + lineWidth / 2;
} else {
//没有横杠
left = i < textLengh / 2 ?
middleX - rectWidth * (textLengh / 2 - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 :
middleX + rectWidth * (i - textLengh / 2) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2;
right = i < textLengh / 2 ?
middleX - rectWidth * ((textLengh / 2 - 1) - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 :
middleX + rectWidth * (i - (textLengh / 2 - 1)) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2;
}
} else {
//字符长度为奇数
left = i < textLengh / 2 + 1 ?
middleX - rectWidth * (textLengh / 2 - i) - rectMargin * (textLengh / 2 - i) - rectWidth / 2 :
middleX + rectWidth * (i - (textLengh / 2 + 1)) + rectMargin * (i - textLengh / 2) + rectWidth / 2;
right = i < textLengh / 2 ?
middleX - rectWidth * ((textLengh / 2 - 1) - i) - rectMargin * (textLengh / 2 - i) - rectWidth / 2 :
middleX + rectWidth * (i - textLengh / 2) + rectMargin * (i - textLengh / 2) + rectWidth / 2;
}
//将游标所在位置的背景,绘制成选中颜色
if (rectIndex == i) {
//改变画笔颜色
rectPaint.setColor(rectSelectColor);
}
switch (style) {
case STYLE_RECT:
canvas.drawRoundRect(left, top, right, bottom, radius, radius, rectPaint);
break;
case STYLE_UNDERLINE:
canvas.drawLine(left, bottom - rectStroke / 2, right, bottom - rectStroke / 2, rectPaint);
break;
default:
canvas.drawRoundRect(left, top, right, bottom, radius, radius, rectPaint);
break;
}
if (rectIndex == i) {
//复原画笔颜色
rectPaint.setColor(rectColor);
}
//第一次循环的时候,存储每一个背景的坐标
if (i == rects.size()) {
rects.add(new Rect(left, top, right, bottom));
}
}
//显示横杠时
if (lineVisable) {
//绘制横杠
canvas.drawRect(middleX - lineWidth / 2, middleY - lineHeight / 2, middleX + lineWidth / 2, middleY + lineHeight / 2, linePaint);
}
//绘制游标,并处理下标为-1和下标大于长度的问题,将会受到 rectIndex影响
if (cursorVisible && rectIndex < textLengh && rectIndex >= 0) {
int startX = rects.get(rectIndex).left + rectWidth / 2;
int startY = middleY - cursorHeight / 2;
int stopY = middleY + cursorHeight / 2;
//绘制游标
canvas.drawLine(startX, startY, startX, stopY, cursorPaint);
}
}
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
//按键抬起时事件
if (event.getAction() == KeyEvent.ACTION_UP) {
for (int i = 0; i < rects.size(); i++) {
Rect rect = rects.get(i);
if (eventX > rect.left && eventX < rect.right && eventY > rect.top && eventY < rect.bottom) {
Log.i("zmklog", "onTouchEvent: " + "请求弹出软键盘");
requestFocus();
inputManager.showSoftInput(this, 0);
//游标下标归零,将会在绘制文字时自动修正
rectIndex = 0;
invalidate();
break;
}
}
}
return true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//启动游标闪烁Timer
timer.schedule(timerTask, 0, cursorDisappearanceTime);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//关闭Timer
timer.cancel();
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
//当页面失焦,隐藏软键盘
if (!hasWindowFocus) {
inputManager.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (gainFocus) {
isFoucus = true;
}else {
//当该View失焦
isFoucus = false;
invalidate();
}
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
//限制输入内容仅为数字?
outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
return super.onCreateInputConnection(outAttrs);
}
@Deprecated
private void showRect(int positon) {
rectIndex = positon;
cursorVisible = true;
invalidate();
}
private void addText(String str) {
//限制字符长度添加
if (texts.size() < textLengh) {
texts.add(rectIndex, str);
invalidate();
}
}
private void removeLastText() {
//删除最后一个字符
if (texts.size() > 0) {
texts.remove(texts.size() - 1);
invalidate();
}
}
//删除所有字符
public void delectText(){
texts.clear();
}
//获取所有字符
public String getText() {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < texts.size(); i++) {
stringBuffer.append(texts.get(i));
}
return stringBuffer.toString();
}
//导入字符
public void setText(String str) {
for (int i = 0; i < str.length() && i < textLengh; i++) {
texts.add(String.valueOf(str.charAt(i)));
}
invalidate();
}
public void setOnEnterClickListener(OnEnterClickListener onEnterClickListener) {
this.onEnterClickListener = onEnterClickListener;
}
//提交
private void commit() {
onEnterClickListener.onClick();
}
//保存状态,其实并不需要
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("superState", super.onSaveInstanceState());
bundle.putString("texts", getText());
return bundle;
}
//复用状态,其实并不需要
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
if (texts.size() == 0) {
setText(bundle.getString("texts", ""));
}
state = bundle.getParcelable("superState");
}
super.onRestoreInstanceState(state);
}
//帮助文档
public void helpLog() {
String msg = "helpLog:" +
"\n1.最多支持6位字符输入" +
"\n2.仅支持数字输入" +
"\n3.对外开放的属性有:text_lengh字符长度、text_color字符颜色、text_visable是否显示具体数字、line_visable是否显示中间横杠、style圆角框/下划线两种样式、background_color背景颜色、backgroud_select_color选中时背景颜色" +
"\n4.其中style属性的值有:STYLE_RECT 0 方框、STYLE_UNDERLINE 1 下划线" +
"\n5.对外开放的接口有:setOnEnterClickListener() Enter键点击事件监听器" +
"\n6.获取和填充字符:setText()、getText(),清空字符:delectText()" +
"\n7.该View可以随宽高任意适配大小";
MyLog.i("zmklog", msg);
}
}
接口代码
package com.zmk.widget;
public interface OnEnterClickListener {
void onClick();
}
MyLog代码
package com.example.common;
import android.util.Log;
public class MyLog {
public static final String name_NORMAL = "normal_tag";
public static void i(String msg){
Log.i(name_NORMAL, msg);
}
public static void i(String name,String msg){
Log.i(name_NORMAL+"-"+name, msg);
}
public static void d(String msg){
Log.d(name_NORMAL, msg);
}
public static void d(String name,String msg){
Log.d(name_NORMAL+"-"+name, msg);
}
public static void e(String msg){
Log.e(name_NORMAL, msg);
}
public static void e(String name,String msg){
Log.e(name_NORMAL+"-"+name, msg);
}
public static void w(String msg){
Log.w(name_NORMAL, msg);
}
public static void w(String name,String msg){
Log.w(name_NORMAL+"-"+name, msg);
}
}
values.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="EditTextInputView">
<attr name="text_lengh" format="integer"/>
<attr name="text_color" format="color"/>
<attr name="text_visable" format="boolean"/>
<attr name="line_visable" format="boolean"/>
<attr name="style" format="integer">
<enum name="STYLE_RECT" value="0"/>
<enum name="STYLE_UNDERLINE" value="1"/>
</attr>
<attr name="background_color" format="color"/>
<attr name="backgroud_select_color" format="color"/>
</declare-styleable>
</resources>