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位址:這裡