package com.xscm.moduleutil.widget; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.text.InputFilter; import android.util.AttributeSet; import android.util.TypedValue; import android.view.InflateException; import androidx.appcompat.widget.AppCompatEditText; import androidx.core.content.ContextCompat; import com.xscm.moduleutil.R; public class SplitEditText extends AppCompatEditText { //密码显示模式:隐藏密码,显示圆形 public static final int CONTENT_SHOW_MODE_PASSWORD = 1; //密码显示模式:显示密码 public static final int CONTENT_SHOW_MODE_TEXT = 2; //输入框相连的样式 public static final int INPUT_BOX_STYLE_CONNECT = 1; //单个的输入框样式 public static final int INPUT_BOX_STYLE_SINGLE = 2; //下划线输入框样式 public static final int INPUT_BOX_STYLE_UNDERLINE = 3; //画笔 private RectF mRectFConnect; private RectF mRectFSingleBox; private Paint mPaintDivisionLine; private Paint mPaintContent; private Paint mPaintBorder; private Paint mPaintUnderline; //边框大小 private Float mBorderSize; //边框颜色 private int mBorderColor; //圆角大小 private float mCornerSize; //分割线大小 private float mDivisionLineSize; //分割线颜色 private int mDivisionColor; //圆形密码的半径大小 private float mCircleRadius; //密码框长度 private int mContentNumber; //密码显示模式 private int mContentShowMode; //单框和下划线输入样式下,每个输入框的间距 private float mSpaceSize; //输入框样式 private int mInputBoxStyle; //字体大小 private float mTextSize; //字体颜色 private int mTextColor; //每个输入框是否是正方形标识 private boolean mInputBoxSquare; private OnInputListener inputListener; private Paint mPaintCursor; private CursorRunnable cursorRunnable; private int mCursorColor;//光标颜色 private float mCursorWidth;//光标宽度 private int mCursorHeight;//光标高度 private int mCursorDuration;//光标闪烁时长 private int mUnderlineFocusColor;//下划线输入样式下,输入框获取焦点时下划线颜色 private int mUnderlineNormalColor;//下划线输入样式下,下划线颜色 public SplitEditText(Context context) { this(context, null); } //这里没有写成默认的EditText属性样式android.R.attr.editTextStyle,这样会存在EditText默认的样式 public SplitEditText(Context context, AttributeSet attrs) { this(context, attrs, 0); } /*public SplitEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); initAttrs(context, attrs); }*/ public SplitEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); } private void initAttrs(Context c, AttributeSet attrs) { TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.SplitEditText); mBorderSize = array.getDimension(R.styleable.SplitEditText_borderSize, dp2px(1f)); mBorderColor = array.getColor(R.styleable.SplitEditText_borderColor, Color.BLACK); mCornerSize = array.getDimension(R.styleable.SplitEditText_corner_size, 0f); mDivisionLineSize = array.getDimension(R.styleable.SplitEditText_divisionLineSize, dp2px(1f)); mDivisionColor = array.getColor(R.styleable.SplitEditText_divisionLineColor, Color.BLACK); mCircleRadius = array.getDimension(R.styleable.SplitEditText_circleRadius, dp2px(5f)); mContentNumber = array.getInt(R.styleable.SplitEditText_contentNumber, 6); mContentShowMode = array.getInteger(R.styleable.SplitEditText_contentShowMode, CONTENT_SHOW_MODE_PASSWORD); mInputBoxStyle = array.getInteger(R.styleable.SplitEditText_inputBoxStyle, INPUT_BOX_STYLE_CONNECT); mSpaceSize = array.getDimension(R.styleable.SplitEditText_spaceSize, dp2px(10f)); mTextSize = array.getDimension(R.styleable.SplitEditText_android_textSize, sp2px(16f)); mTextColor = array.getColor(R.styleable.SplitEditText_android_textColor, Color.BLACK); mInputBoxSquare = array.getBoolean(R.styleable.SplitEditText_inputBoxSquare, true); mCursorColor = array.getColor(R.styleable.SplitEditText_cursorColor, ContextCompat.getColor(c,R.color.colorAccent)); mCursorDuration = array.getInt(R.styleable.SplitEditText_cursorDuration, 500); mCursorWidth = array.getDimension(R.styleable.SplitEditText_cursorWidth, dp2px(2f)); mCursorHeight = (int) array.getDimension(R.styleable.SplitEditText_cursorHeight, 0); mUnderlineNormalColor = array.getInt(R.styleable.SplitEditText_underlineNormalColor, Color.BLACK); mUnderlineFocusColor = array.getInt(R.styleable.SplitEditText_underlineFocusColor, 0); array.recycle(); init(); } private void init() { mPaintBorder = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBorder.setStyle(Paint.Style.FILL_AND_STROKE); mPaintBorder.setStrokeWidth(mBorderSize); mPaintBorder.setColor(mBorderColor); mPaintDivisionLine = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintDivisionLine.setStyle(Paint.Style.STROKE); mPaintDivisionLine.setStrokeWidth(mDivisionLineSize); mPaintDivisionLine.setColor(mDivisionColor); mPaintContent = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintContent.setTextSize(mTextSize); mPaintCursor = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintCursor.setStrokeWidth(mCursorWidth); mPaintCursor.setColor(mCursorColor); mPaintUnderline = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintUnderline.setStrokeWidth(mBorderSize); mPaintUnderline.setColor(mUnderlineNormalColor); //避免onDraw里面重复创建RectF对象,先初始化RectF对象,在绘制时调用set()方法 //单个输入框样式的RectF mRectFSingleBox = new RectF(); //绘制Connect样式的矩形框 mRectFConnect = new RectF(); //设置单行输入 setSingleLine(); //若构造方法中没有写成android.R.attr.editTextStyle的属性,应该需要设置该属性,EditText默认是获取焦点的 setFocusableInTouchMode(true); //取消默认的光标 //这里默认不设置该属性,不然长按粘贴有问题(一开始长按不能粘贴,输入内容就可以长按粘贴) //setCursorVisible(false); //设置透明光标,若是直接不显示光标的话,长按粘贴会没效果 /*try { Field f = TextView.class.getDeclaredField("mCursorDrawableRes"); f.setAccessible(true); f.set(this, 0); } catch (Exception e) { e.printStackTrace(); }*/ //设置光标的TextSelectHandle //这里判断版本,10.0以及以上直接通过方法调用,以下通过反射设置 // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // setTextSelectHandle(android.R.color.transparent); // } else { // //通过反射改变光标TextSelectHandle的样式 // try { // Field f = TextView.class.getDeclaredField("mTextSelectHandleRes"); // f.setAccessible(true); // f.set(this, android.R.color.transparent); // } catch (Exception e) { // e.printStackTrace(); // } // } //设置InputFilter,设置输入的最大字符长度为设置的长度 setFilters(new InputFilter[]{new InputFilter.LengthFilter(mContentNumber)}); } @Override protected void onAttachedToWindow() { // L.e("SplitEditText","onAttachedToWindow------->"); super.onAttachedToWindow(); if(hasFocus()){ startCursor(); } } private void startCursor(){ if(cursorRunnable==null){ cursorRunnable = new CursorRunnable(); } postDelayed(cursorRunnable, mCursorDuration); } private void stopCursor(){ if(cursorRunnable!=null){ removeCallbacks(cursorRunnable); } } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); if(focused){ startCursor(); }else{ stopCursor(); } invalidate(); } @Override protected void onDetachedFromWindow() { // L.e("SplitEditText","onDetachedFromWindow------->"); stopCursor(); super.onDetachedFromWindow(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mInputBoxSquare) { int width = MeasureSpec.getSize(widthMeasureSpec); //计算view高度,使view高度和每个item的宽度相等,确保每个item是一个正方形 float itemWidth = getContentItemWidthOnMeasure(width); switch (mInputBoxStyle) { case INPUT_BOX_STYLE_UNDERLINE: setMeasuredDimension(width, (int) (itemWidth + mBorderSize)); break; case INPUT_BOX_STYLE_SINGLE: case INPUT_BOX_STYLE_CONNECT: default: setMeasuredDimension(width, (int) (itemWidth + mBorderSize * 2)); break; } } } @Override protected void onDraw(Canvas canvas) { //绘制输入框 switch (mInputBoxStyle) { case INPUT_BOX_STYLE_SINGLE: drawSingleStyle(canvas); break; case INPUT_BOX_STYLE_UNDERLINE: drawUnderlineStyle(canvas); break; case INPUT_BOX_STYLE_CONNECT: default: drawConnectStyle(canvas); break; } //绘制输入框内容 drawContent(canvas); //绘制光标 if(hasFocus()){ drawCursor(canvas); } } /** * 根据输入内容显示模式,绘制内容是圆心还是明文的text */ private void drawContent(Canvas canvas) { int cy = getHeight() / 2; String password = getText().toString().trim(); if (mContentShowMode == CONTENT_SHOW_MODE_PASSWORD) { mPaintContent.setColor(Color.BLACK); for (int i = 0; i < password.length(); i++) { float startX = getDrawContentStartX(i); canvas.drawCircle(startX, cy, mCircleRadius, mPaintContent); } } else { mPaintContent.setColor(mTextColor); //计算baseline float baselineText = getTextBaseline(mPaintContent, cy); for (int i = 0; i < password.length(); i++) { float startX = getDrawContentStartX(i); //计算文字宽度 String text = String.valueOf(password.charAt(i)); float textWidth = mPaintContent.measureText(text); //绘制文字x应该还需要减去文字宽度的一半 canvas.drawText(text, startX - textWidth / 2, baselineText, mPaintContent); } } } /** * 绘制光标 * 光标只有一个,所以不需要根据循环来绘制,只需绘制第N个就行 * 即: * 当输入内容长度为0,光标在第0个位置 * 当输入内容长度为1,光标应在第1个位置 * ... * 所以光标所在位置为输入内容的长度 * 这里光标的长度默认就是 height/2 */ private void drawCursor(Canvas canvas) { if (mCursorHeight > getHeight()) { throw new InflateException("cursor height must smaller than view height"); } String content = getText().toString().trim(); float startX = getDrawContentStartX(content.length()); //如果设置得有光标高度,那么startY = (高度-光标高度)/2+边框宽度 if (mCursorHeight == 0) { mCursorHeight = getHeight() / 2; } int sy = (getHeight() - mCursorHeight) / 2; float startY = sy + mBorderSize; float stopY = getHeight() - sy - mBorderSize; //此时的绘制光标竖直线,startX = stopX canvas.drawLine(startX, startY, startX, stopY, mPaintCursor); } /** * 、 * 计算三种输入框样式下绘制圆和文字的x坐标 * * @param index 循环里面的下标 i */ private float getDrawContentStartX(int index) { switch (mInputBoxStyle) { case INPUT_BOX_STYLE_SINGLE: //单个输入框样式下的startX //即 itemWidth/2 + i*itemWidth + i*每一个间距宽度 + 前面所有的左右边框 // i = 0,左侧1个边框 // i = 1,左侧3个边框(一个完整的item的左右边框+ 一个左侧边框) // i = ..., (2*i+1)*mBorderSize return getContentItemWidth() / 2 + index * getContentItemWidth() + index * mSpaceSize + (2 * index + 1) * mBorderSize; case INPUT_BOX_STYLE_UNDERLINE: //下划线输入框样式下的startX //即 itemWidth/2 + i*itemWidth + i*每一个间距宽度 return getContentItemWidth() / 2 + index * mSpaceSize + index * getContentItemWidth(); case INPUT_BOX_STYLE_CONNECT: //矩形输入框样式下的startX //即 itemWidth/2 + i*itemWidth + i*分割线宽度 + 左侧的一个边框宽度 default: return getContentItemWidth() / 2 + index * getContentItemWidth() + index * mDivisionLineSize + mBorderSize; } } /** * 绘制下划线输入框样式 * 线条起点startX:每个字符所占宽度itemWidth + 每个字符item之间的间距mSpaceSize * 线条终点stopX:stopX与startX之间就是一个itemWidth的宽度 */ private void drawUnderlineStyle(Canvas canvas) { String content = getText().toString().trim(); for (int i = 0; i < mContentNumber; i++) { //计算绘制下划线的startX float startX = i * getContentItemWidth() + i * mSpaceSize; //stopX float stopX = getContentItemWidth() + startX; //对于下划线这种样式,startY = stopY float startY = getHeight() - mBorderSize / 2; //这里判断是否设置有输入框获取焦点时,下划线的颜色 if (mUnderlineFocusColor != 0) { if (content.length() >= i) { mPaintUnderline.setColor(mUnderlineFocusColor); } else { mPaintUnderline.setColor(mUnderlineNormalColor); } } canvas.drawLine(startX, startY, stopX, startY, mPaintUnderline); } } /** * 绘制单框输入模式 * 这里计算left、right时有点饶, * 理解、计算时最好根据图形、参照drawConnectStyle()绘制带边框的矩形 * left:绘制带边框的矩形等图形时,去掉边框的一半即 + mBorderSize / 2,同时加上每个字符item的间距 + i*mSpaceSize * 另外,每个字符item的宽度 + i*itemWidth * 最后,绘制时都是以整个view的宽度计算,绘制第N个时,都应该加上以前的边框宽度 * 即第一个:i = 0 ,边框的宽度为0 * 第二个:i = 1,边框的宽度 2*mBorderSize,左右两个的边框宽度 * 以此...最后应该 + i*2*mBorderSize * 同理 * right:去掉边框的一半: -mBorderSize/2,还应该加上前面一个item的宽度:+(i+1)*itemWidth * 同样,绘制时都是以整个view的宽度计算,绘制后面的,都应该加上前面的所有宽度 * 即 间距:+i*mSpaceSize * 边框:(注意是计算整个view) * 第一个:i = 0,2个边框2*mBorderSize * 第二个:i = 1,4个边框,即 (1+1)*2*mBorderSize * 所以算上边框 +(i+1)*2*mBorderSize */ private void drawSingleStyle(Canvas canvas) { for (int i = 0; i < mContentNumber; i++) { mRectFSingleBox.setEmpty(); float left = i * getContentItemWidth() + i * mSpaceSize + i * mBorderSize * 2 + mBorderSize / 2; float right = i * mSpaceSize + (i + 1) * getContentItemWidth() + (i + 1) * 2 * mBorderSize - mBorderSize / 2; //为避免在onDraw里面创建RectF对象,这里使用rectF.set()方法 mRectFSingleBox.set(left, mBorderSize / 2, right, getHeight() - mBorderSize / 2); canvas.drawRoundRect(mRectFSingleBox, mCornerSize, mCornerSize, mPaintBorder); } } /** * 绘制矩形外框 * 在绘制圆角矩形的时候,应该减掉边框的宽度 * 不然会有所偏差 *

* 在绘制矩形以及其它图形的时候,矩形(图形)的边界是边框的中心,不是边框的边界 * 所以在绘制带边框的图形的时候应该减去边框宽度的一半 * https://blog.csdn.net/a512337862/article/details/74161988 */ private void drawConnectStyle(Canvas canvas) { //每次重新绘制时,先将rectF重置下 mRectFConnect.setEmpty(); //需要减去边框的一半 mRectFConnect.set( mBorderSize / 2, mBorderSize / 2, getWidth() - mBorderSize / 2, getHeight() - mBorderSize / 2 ); canvas.drawRoundRect(mRectFConnect, mCornerSize, mCornerSize, mPaintBorder); //绘制分割线 drawDivisionLine(canvas); } /** * 分割线条数为内容框数目-1 * 这里startX应该要加上左侧边框的宽度 * 应该还需要加上分割线的一半 * 至于startY和stopY不是 mBorderSize/2 而是 mBorderSize * startX是计算整个宽度的,需要算上左侧的边框宽度,所以不是+mBorderSize/2 而是+mBorderSize * startY和stopY:分割线是紧挨着边框内部的,所以应该是mBorderSize,而不是mBorderSize/2 */ private void drawDivisionLine(Canvas canvas) { float stopY = getHeight() - mBorderSize; for (int i = 0; i < mContentNumber - 1; i++) { //对于分割线条,startX = stopX float startX = (i + 1) * getContentItemWidth() + i * mDivisionLineSize + mBorderSize + mDivisionLineSize / 2; canvas.drawLine(startX, mBorderSize, startX, stopY, mPaintDivisionLine); } } /** * 计算3种样式下,相应每个字符item的宽度 */ private float getContentItemWidth() { //计算每个密码字符所占的宽度,每种输入框样式下,每个字符item所占宽度也不一样 float tempWidth; switch (mInputBoxStyle) { case INPUT_BOX_STYLE_SINGLE: //单个输入框样式:宽度-间距宽度(字符数-1)*每个间距宽度-每个输入框的左右边框宽度 tempWidth = getWidth() - (mContentNumber - 1) * mSpaceSize - 2 * mContentNumber * mBorderSize; break; case INPUT_BOX_STYLE_UNDERLINE: //下划线样式:宽度-间距宽度(字符数-1)*每个间距宽度 tempWidth = getWidth() - (mContentNumber - 1) * mSpaceSize; break; case INPUT_BOX_STYLE_CONNECT: //矩形输入框样式:宽度-左右两边框宽度-分割线宽度(字符数-1)*每个分割线宽度 default: tempWidth = getWidth() - (mDivisionLineSize * (mContentNumber - 1)) - 2 * mBorderSize; break; } return tempWidth / mContentNumber; } /** * 根据view的测量宽度,计算每个item的宽度 * * @param measureWidth view的measure * @return onMeasure时的每个item宽度 */ private float getContentItemWidthOnMeasure(int measureWidth) { //计算每个密码字符所占的宽度,每种输入框样式下,每个字符item所占宽度也不一样 float tempWidth; switch (mInputBoxStyle) { case INPUT_BOX_STYLE_SINGLE: //单个输入框样式:宽度-间距宽度(字符数-1)*每个间距宽度-每个输入框的左右边框宽度 tempWidth = measureWidth - (mContentNumber - 1) * mSpaceSize - 2 * mContentNumber * mBorderSize; break; case INPUT_BOX_STYLE_UNDERLINE: //下划线样式:宽度-间距宽度(字符数-1)*每个间距宽度 tempWidth = measureWidth - (mContentNumber - 1) * mSpaceSize; break; case INPUT_BOX_STYLE_CONNECT: //矩形输入框样式:宽度-左右两边框宽度-分割线宽度(字符数-1)*每个分割线宽度 default: tempWidth = measureWidth - (mDivisionLineSize * (mContentNumber - 1)) - 2 * mBorderSize; break; } return tempWidth / mContentNumber; } /** * 计算绘制文本的基线 * * @param paint 绘制文字的画笔 * @param halfHeight 高度的一半 */ private float getTextBaseline(Paint paint, float halfHeight) { Paint.FontMetrics fontMetrics = paint.getFontMetrics(); float dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom; return halfHeight + dy; } /** * 设置密码是否可见 */ public void setContentShowMode(int mode) { if (mode != CONTENT_SHOW_MODE_PASSWORD && mode != CONTENT_SHOW_MODE_TEXT) { throw new IllegalArgumentException( "the value of the parameter must be one of" + "{1:EDIT_SHOW_MODE_PASSWORD} or " + "{2:EDIT_SHOW_MODE_TEXT}" ); } mContentShowMode = mode; invalidate(); } /** * 获取密码显示模式 */ public int getContentShowMode() { return mContentShowMode; } /** * 设置输入框样式 */ public void setInputBoxStyle(int inputBoxStyle) { if (inputBoxStyle != INPUT_BOX_STYLE_CONNECT && inputBoxStyle != INPUT_BOX_STYLE_SINGLE && inputBoxStyle != INPUT_BOX_STYLE_UNDERLINE ) { throw new IllegalArgumentException( "the value of the parameter must be one of" + "{1:INPUT_BOX_STYLE_CONNECT}, " + "{2:INPUT_BOX_STYLE_SINGLE} or " + "{3:INPUT_BOX_STYLE_UNDERLINE}" ); } mInputBoxStyle = inputBoxStyle; // 这里没有调用invalidate因为会存在问题 // invalidate会重绘,但是不会去重新测量,当输入框样式切换的之后,item的宽度其实是有变化的,所以此时需要重新测量 // requestLayout,调用onMeasure和onLayout,不一定会调用onDraw,当view的l,t,r,b发生改变时会调用onDraw requestLayout(); //invalidate(); } public void setOnInputListener(OnInputListener listener) { this.inputListener = listener; } /** * 通过复写onTextChanged来实现对输入的监听 * 如果在onDraw里面监听text的输入长度来实现,会重复的调用该方法,就不妥当 */ @Override protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { super.onTextChanged(text, start, lengthBefore, lengthAfter); String content = text.toString().trim(); if (inputListener != null) { inputListener.onInputChanged(content); if (content.length() == mContentNumber) { inputListener.onInputFinished(content); } } } /** * 获取输入框样式 */ public int getInputBoxStyle() { return mInputBoxStyle; } private float dp2px(float dpValue) { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dpValue, Resources.getSystem().getDisplayMetrics() ); } private float sp2px(float spValue) { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, spValue, Resources.getSystem().getDisplayMetrics() ); } /** * 光标Runnable * 通过Runnable每500ms执行重绘,每次runnable通过改变画笔的alpha值来使光标产生闪烁的效果 */ private class CursorRunnable implements Runnable { @Override public void run() { //获取光标画笔的alpha值 int alpha = mPaintCursor.getAlpha(); //设置光标画笔的alpha值 mPaintCursor.setAlpha(alpha == 0 ? 255 : 0); invalidate(); postDelayed(this, mCursorDuration); // L.e("SplitEditText","CursorRunnable-----run-->"); } } /** * 输入的监听抽象类 * 没定义接口的原因是可以在抽象类里面定义空实现的方法,可以让用户根据需求选择性的复写某些方法 */ public interface OnInputListener { /** * 输入完成的抽象方法 * * @param content 输入内容 */ public abstract void onInputFinished(String content); /** * 输入的内容 */ public void onInputChanged(String text) ; } }