jCenter
依赖:
implementation 'coder.siy:password-textView:1.0.0'
首先看看效果:
图一
我为它的很多属性都开放了接口,可以根据自己的需要自由修改。
效果看了,接下谈谈它是怎么实现的。
主要是思路可以由下图来表示:
图二
控件是继承于系统控件TextView,然后重写onDraw(Canvas),这样可以减少很多麻烦。
根据图片的显示顺序
首先是绘制黑色的底:
/**
* 绘制边框,先绘制一整块区域
*/
private void drawBoarder(Canvas canvas) {
canvas.drawRoundRect(rect, borderRadius, borderRadius, borderPaint);
}
rect:黑色长方形大小。
borderRadius:黑色长方形的圆角度数,当为0时就是直角
borderPaint:黑色长方形的画笔
Canvas.drawRoundRect:绘制的是圆角矩形
然后绘制白色内容区域:
/**
* 绘制内容区域,和内容边界
*
* @param canvas
*/
private void drawContent(Canvas canvas) {
//每次绘制是都要重置rectIn的位置
rectIn.left = rect.left + borderWidth;
rectIn.top = rect.top + borderWidth;
rectIn.right = rectIn.left + contentWidth;
rectIn.bottom = rectIn.top + contentHeight;
for (int i = 0; i < pwdLen; i++) {
canvas.drawRoundRect(rectIn, contentRadius, contentRadius, contentPaint);
canvas.drawRoundRect(rectIn, contentRadius, contentRadius, contentBoardPaint);
rectIn.left = rectIn.right + contentMargin;
rectIn.right = rectIn.left + contentWidth;
}
}
rectIn:白色的正方形大小。
绘制白色正方形的时候有2个考虑点:borderWidth和contentMargin。
borderWidth:每一个白色的正方形顶部(底部)距离黑色长方形顶部(底部)距离,第一个白色正方形的左边距离黑色长方形左边的距离,最后一个白色正方形的右边距离黑色长方形右边的距离。
contentMargin:每一个白色正方形相互之间的间隔。
然后仔细计算每次绘制rectIn。
最后绘制密码显示的圆点:
/**
* 绘制密码
*
* @param canvas
*/
private void drawPwd(Canvas canvas) {
float cy = rect.top + height / 2;
float cx = rect.left + contentWidth / 2 + borderWidth;
CharSequence nowText = getText();
for (int i = 0; i < curLenght; i++) {
if (isShowPwdText) {
String drawText = String.valueOf(nowText.charAt(i));
canvas.drawText(drawText, 0, drawText.length(), cx, cy - pwdTextOffsetY, pwdTextPaint);
} else {
canvas.drawCircle(cx, cy, pwdWidth / 2, pwdPaint);
}
cx = cx + contentWidth + contentMargin;
}
}
绘制密码显示的圆点主要就是要计算出cx,cy。
cy:
cx:
大方向是解决了。还有一些细节需要处理一下。
1,如何保证内容区域始终是正方形。
2,文本框状态怎么自动保存恢复。
3,明文怎么绘制在对话框中间。
4,继承TextView怎么来的光标。
5,顺便提一下分割线的实现。
如何保证内容区域始终是正方形:
/**
* 当borderWidth,pwdLen,contentMargin修改之后需要调用此方法
* @param w TextView控件的宽
* @param h TextView控件的高
*/
private void calculateBorderAndContentSize(int w,int h){
//算出本应该的宽高
contentWidth = (w - 2 * borderWidth - (pwdLen - 1) * contentMargin) / (float) pwdLen;
contentHeight = h - 2 * borderWidth;
//为了绘制正方形,取小的数值
contentHeight = contentWidth = contentHeight > contentWidth ? contentWidth : contentHeight;
//变成正方形之后的宽度和高度
height = contentHeight + 2 * borderWidth;
width = (contentHeight * pwdLen) + 2 * borderWidth + contentMargin * (pwdLen - 1);
//变成正方形之后,重新计算绘制的起点
drawStartX = (w - width) / 2.0f;
drawStartY = (h - height) / 2.0f;
// 外边框
rect = new RectF(drawStartX, drawStartY, drawStartX + width, drawStartY + height);
//内容区域边框,也就是输入密码数字的那个格子
rectIn = new RectF();
}
contentWidth:图二中白色正方形的宽
contentHeight:图二中白色正方形的高
width:图二中黑色长方形的宽
height:图二中黑色长方形的高
drawStartX:黑色长方形左上角的X坐标(注:有一个透明底,原TextView站住的大小)
drawStartY:黑色长方形左上角的Y坐标(注:有一个透明底,原TextView站住的大小)
rect:黑色长方形的大小
rectIn:白色正方形大小
首先根据TextView的宽(w)高(h)计算出contentWidth和contentHeight,然后比较contentWidth和contentHeight的大小,取较小者赋值给conentWidth和cotentHeight,然后根据重新赋值的contentWidth和contentHeight计算出width和height,最后根据w,h和width,height计算出drawStartX和drawStartY。这样我就能根据计算出来的width,height,contentWidth,contentHeight,drawStartX和drawStartY保存绘制出来的内容框始终是正方形并且绘制的区域始终位于TextView的中心区域。
文本框状态怎么自动保存恢复:
@Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putString("curtext", getText().toString());
bundle.putBoolean("isShowCursor",isShowCursor);
bundle.putBoolean("isShowPwdText",isShowPwdText);
bundle.putInt("borderColor",borderColor);
bundle.putInt("borderRadius",borderRadius);
bundle.putInt("contentColor",contentColor);
bundle.putInt("contentBoardColor",contentBoardColor);
bundle.putInt("contentBoardWidth",contentBoardWidth);
bundle.putInt("contentRadius",contentRadius);
bundle.putInt("contentMargin",contentMargin);
bundle.putInt("splitLineColor",splitLineColor);
bundle.putInt("cursorColor",cursorColor);
bundle.putInt("cursorMargin",cursorMargin);
bundle.putInt("pwdLen",pwdLen);
bundle.putInt("pwdColor",pwdColor);
bundle.putInt("pwdWidth",pwdWidth);
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
String curText = bundle.getString("curtext");
if (!TextUtils.isEmpty(curText)) {
setText(curText);
}
boolean isShowCursor = bundle.getBoolean("isShowCursor");
if (isShowCursor){
showCursor();
}else{
hideCursor();
}
boolean isShowPwdText = bundle.getBoolean("isShowPwdText");
showPwdText(isShowPwdText);
int borderColor = bundle.getInt("borderColor");
setBorderColor(borderColor);
int borderRadius = bundle.getInt("borderRadius");
setBorderRadius(borderRadius);
int contentColor = bundle.getInt("contentColor");
setContentColor(contentColor);
int contentBoardColor = bundle.getInt("contentBoardColor");
setContentBoardColor(contentBoardColor);
int contentBoardWidth = bundle.getInt("contentBoardWidth");
setContentBoardWidth(contentBoardWidth);
int contentRadius = bundle.getInt("contentRadius");
setContentRadius(contentRadius);
int contentMargin = bundle.getInt("contentMargin");
setContentMargin(contentMargin);
int splitLineColor = bundle.getInt("splitLineColor");
setSplitLineColor(splitLineColor);
int cursorColor = bundle.getInt("cursorColor");
setCursorColor(cursorColor);
int cursorMargin = bundle.getInt("cursorMargin");
setCursorMargin(cursorMargin);
int pwdLen = bundle.getInt("pwdLen");
setPwdLen(pwdLen);
int pwdColor = bundle.getInt("pwdColor");
setPwdColor(pwdColor);
int pwdWidth = bundle.getInt("pwdWidth");
setPwdWidth(pwdWidth);
state = bundle.getParcelable("instanceState");
}
super.onRestoreInstanceState(state);
}
这个简单不做过多解释。
明文怎么绘制在对话框中间:
这个问题看起来简单其实并不是很简单。
如果你这样 canvas.drawText(drawText, 0, drawText.length(), cx, cy, pwdTextPaint);绘制明文你会发现明文其实是偏中上位置的。因为cy所代表的是基线的位置。具体可以查看这篇文章。
/**
* 绘制密码
*
* @param canvas
*/
private void drawPwd(Canvas canvas) {
float cy = rect.top + height / 2;
float cx = rect.left + contentWidth / 2 + borderWidth;
CharSequence nowText = getText();
for (int i = 0; i < curLenght; i++) {
if (isShowPwdText) {
String drawText = String.valueOf(nowText.charAt(i));
canvas.drawText(drawText, 0, drawText.length(), cx, cy - pwdTextOffsetY, pwdTextPaint);
} else {
canvas.drawCircle(cx, cy, pwdWidth / 2, pwdPaint);
}
cx = cx + contentWidth + contentMargin;
}
}
这里要如何计算出drawText的x,y的坐标呢?
x其实可以直接使用cx的,只要设置pwdTextPaint的setTextAlign(Paint.Align.CENTER),默认是Paint.Align.LEFT。这个属性是x相对于绘制字符串的位置,如果是Paint.Align.LEFT则x在绘制字符串的左边,如果是Paint.Align.CENTER则x在绘制字符串的中间,显然符合我们的需求。
y的坐标怎么计算呢?
目标:通过cy把baseline计算出来。
我觉得这幅图解释的很好了。为什么没有拿top和bottom计算baseLineY呢?因为系统建议的,绘制单个字符时字符的最高高度应该是ascent最低高度应该是descent。所以计算出来baseLineY:cy-(paint.ascent()+paint.descent())/2。
继承TextView怎么来的光标:
当然是自己绘制啊!!!
绘制光标:
/**
* 绘制光标
*
* @param canvas
*/
private void drawCursor(Canvas canvas) {
float startX, startY, stopY;
int sin = curLenght - 1;
float half = contentWidth / 2;
if (sin == -1) {
startX = borderWidth + half;
startY = cursorMargin + borderWidth;
stopY = height - borderWidth - cursorMargin;
canvas.drawLine(drawStartX + startX, drawStartY + startY, drawStartX + startX, drawStartY + stopY, cursorPaint);
} else {
startY = cursorMargin + borderWidth;
stopY = height - borderWidth - cursorMargin;
if (isShowPwdText) {
String s = String.valueOf(getText().charAt(sin));
pwdTextPaint.getTextBounds(s, 0, s.length(), textBoundrect);
startX = borderWidth + sin * (contentWidth + contentMargin) + half + textBoundrect.width() / 2 + cursourMarginPwd;
} else {
startX = borderWidth + sin * (contentWidth + contentMargin) + half + pwdWidth / 2 + cursourMarginPwd;
}
canvas.drawLine(drawStartX + startX, drawStartY + startY, drawStartX + startX, drawStartY + stopY, cursorPaint);
}
}
绘制光标的时候需要注意:第一个密码输入框在没有输入字符时它应该显示在密码输入框中间,如果输入了字符就显示在字符右边.还需要注意字符是明文还是密文,因为我们需要分别计算明文和密文的宽度。
光标闪烁:
/**
* 显示光标
*/
public void showCursor() {
if (handler == null) {
handler = new TimerHandler(this);
}
//只有当光标没有显示的时候才让它显示
if (!isShowCursor) {
isShowCursor = true;
handler.sendEmptyMessageDelayed(0, 500);
}
}
/**
* 隐藏光标
*/
public void hideCursor() {
if (handler != null) {
handler.removeCallbacksAndMessages(null);
}
showCursorSwitch = false;
isShowCursor = false;
invalidateView();
}
private static class TimerHandler extends Handler {
private WeakReference<PwdView> reference;
TimerHandler(PwdView view) {
reference = new WeakReference<>(view);
}
@Override
public void handleMessage(Message msg) {
PwdView view = reference.get();
if (view != null) {
view.showCursorSwitch = !view.showCursorSwitch;
view.invalidateView();
sendEmptyMessageDelayed(0, 500);
}
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
hideCursor();
}
光标的闪烁是通过Handler来实现。如果不是特别需要不建议打开光标闪烁。
顺便提一下分割线的实现:
就是在2个密码框的间隔距离之间画一个竖线。
/**
* 绘制分割线
*
* @param canvas
*/
private void drawSplitLine(Canvas canvas) {
float startX = rect.left + borderWidth + contentWidth + (contentMargin / 2.0f);
for (int i = 1; i < pwdLen; i++) {
canvas.drawLine(startX, rect.top + borderWidth, startX, rect.bottom - borderWidth, splitLinePaint);
startX = startX + contentWidth + contentMargin;
}
}
github地址:这里