以下为源码
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>