本文為Android自定義View系列琳水,難度不大,都是些坐標(biāo)的計(jì)算,閱讀本文大概需要5分鐘顽素。
Demo效果圖
分析
寬高計(jì)算
各個(gè)變量含義
private int passwordLength;//密碼個(gè)數(shù)
private long cursorFlashTime;//光標(biāo)閃動間隔時(shí)間
private int passwordPadding;//每個(gè)密碼間的間隔
private int passwordSize = dp2px(40);//單個(gè)密碼大小
private int borderColor;//邊框顏色
private int borderWidth;//下劃線粗細(xì)
private int cursorPosition;//光標(biāo)位置
private int cursorWidth;//光標(biāo)粗細(xì)
private int cursorHeight;//光標(biāo)長度
private int cursorColor;//光標(biāo)顏色
private boolean isCursorShowing;//光標(biāo)是否正在顯示
private boolean isCursorEnable;//是否開啟光標(biāo)
private boolean isInputComplete;//是否輸入完畢
private int cipherTextSize;//密文符號大小
private boolean cipherEnable;//是否開啟密文
private static String CIPHER_TEXT = "*"; //密文符號
因?yàn)橄聞澗€的密碼輸入框和方框的密碼輸入框?qū)嶋H上僅有繪制橫線還是繪制方框的區(qū)別而已础倍,對于坐標(biāo)的計(jì)算以及邏輯都是一樣的烛占,所以這里我們就以下劃線密碼輸入框?yàn)槔樱瑢Ρ容^關(guān)鍵的代碼進(jìn)行一下分析沟启。
源碼分析
從圖片可以看出忆家,整個(gè)View的寬高度計(jì)算公式為 :
View寬度 = 單個(gè)密碼框的寬度 * 密碼位數(shù) + 密碼框間隔 * (密碼位數(shù) - 1)
View高度 = 密碼框的高度
現(xiàn)在我們已經(jīng)知道計(jì)算公式了,可以開始擼代碼了德迹,還是老套路芽卿,先繼承View
重寫onMeasure
方法,代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = 0;
switch (widthMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
//沒有指定大小胳搞,寬度 = 單個(gè)密碼框大小 * 密碼位數(shù) + 密碼框間距 *(密碼位數(shù) - 1)
width = passwordSize * passwordLength + passwordPadding * (passwordLength - 1);
break;
case MeasureSpec.EXACTLY:
//指定大小卸例,寬度 = 指定的大小
width = MeasureSpec.getSize(widthMeasureSpec);
//密碼框大小 = (寬度 - 密碼框間距 *(密碼位數(shù) - 1)) / 密碼位數(shù)
passwordSize = (width - (passwordPadding * (passwordLength - 1))) / passwordLength;
break;
}
setMeasuredDimension(width, passwordSize);
}
計(jì)算完View的寬高之后,我們還需要計(jì)算一下密碼文本的大小流酬,光標(biāo)的寬高币厕,這里我們在onSizeChanged
中進(jìn)行計(jì)算
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//文本大小
cipherTextSize = passwordSize / 2;
//光標(biāo)寬度
cursorWidth = dp2px(2);
//光標(biāo)長度
cursorHeight = passwordSize / 2;
}
現(xiàn)在我們需要的大小都計(jì)算好了,可以進(jìn)行關(guān)鍵的一步芽腾,便是繪制旦装,重寫onDraw
方法,代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mode == Mode.UNDERLINE) {
//繪制下劃線
drawUnderLine(canvas, paint);
} else {
//繪制方框
drawRect(canvas, paint);
}
//繪制光標(biāo)
drawCursor(canvas, paint);
//繪制密碼文本
drawCipherText(canvas, paint);
}
這里因?yàn)槲覀兪且韵聞澗€為例子摊滔,所以我們先看一下繪制下劃線的drawUnderLine
方法
/**
* 繪制密碼框下劃線
*
* @param canvas
* @param paint
*/
private void drawUnderLine(Canvas canvas, Paint paint) {
//畫筆初始化
paint.setColor(borderColor);
paint.setStrokeWidth(borderWidth);
paint.setStyle(Paint.Style.FILL);
for (int i = 0; i < passwordLength; i++) {
//根據(jù)密碼位數(shù)for循環(huán)繪制直線
// 起始點(diǎn)x = paddingLeft + (單個(gè)密碼框大小 + 密碼框邊距) * i , 起始點(diǎn)y = paddingTop + 單個(gè)密碼框大小
// 終止點(diǎn)x = 起始點(diǎn)x + 單個(gè)密碼框大小 , 終止點(diǎn)y與起始點(diǎn)一樣不變
canvas.drawLine(getPaddingLeft() + (passwordSize + passwordPadding) * i, getPaddingTop() + passwordSize,
getPaddingLeft() + (passwordSize + passwordPadding) * i + passwordSize, getPaddingTop() + passwordSize,
paint);
}
}
接下來是繪制密碼drawCipherText
方法
/**
* 繪制密碼替代符號
*
* @param canvas
* @param paint
*/
private void drawCipherText(Canvas canvas, Paint paint) {
//畫筆初始化
paint.setColor(Color.GRAY);
paint.setTextSize(cipherTextSize);
paint.setTextAlign(Paint.Align.CENTER);
paint.setStyle(Paint.Style.FILL);
//文字居中的處理
Rect r = new Rect();
canvas.getClipBounds(r);
int cHeight = r.height();
paint.getTextBounds(CIPHER_TEXT, 0, CIPHER_TEXT.length(), r);
float y = cHeight / 2f + r.height() / 2f - r.bottom;
//根據(jù)輸入的密碼位數(shù)阴绢,進(jìn)行for循環(huán)繪制
for (int i = 0; i < password.length; i++) {
if (!TextUtils.isEmpty(password[i])) {
// x = paddingLeft + 單個(gè)密碼框大小/2 + ( 密碼框大小 + 密碼框間距 ) * i
// y = paddingTop + 文字居中所需偏移量
if (cipherEnable) {
//沒有開啟明文顯示,繪制密碼密文
canvas.drawText(CIPHER_TEXT,
(getPaddingLeft() + passwordSize / 2) + (passwordSize + passwordPadding) * i,
getPaddingTop() + y, paint);
} else {
//明文顯示艰躺,直接繪制密碼
canvas.drawText(password[i],
(getPaddingLeft() + passwordSize / 2) + (passwordSize + passwordPadding) * i,
getPaddingTop() + y, paint);
}
}
}
}
然后接下來是繪制光標(biāo)drawCursor
方法呻袭,代碼如下:
/**
* 繪制光標(biāo)
*
* @param canvas
* @param paint
*/
private void drawCursor(Canvas canvas, Paint paint) {
paint.setColor(cursorColor);
paint.setStrokeWidth(cursorWidth);
paint.setStyle(Paint.Style.FILL);
//光標(biāo)未顯示 && 開啟光標(biāo) && 輸入位數(shù)未滿 && 獲得焦點(diǎn)
if (!isCursorShowing && isCursorEnable && !isInputComplete && hasFocus()) {
// 起始點(diǎn)x = paddingLeft + 單個(gè)密碼框大小 / 2 + (單個(gè)密碼框大小 + 密碼框間距) * 光標(biāo)下標(biāo)
// 起始點(diǎn)y = paddingTop + (單個(gè)密碼框大小 - 光標(biāo)大小) / 2
// 終止點(diǎn)x = 起始點(diǎn)x
// 終止點(diǎn)y = 起始點(diǎn)y + 光標(biāo)高度
canvas.drawLine((getPaddingLeft() + passwordSize / 2) + (passwordSize + passwordPadding) * cursorPosition,
getPaddingTop() + (passwordSize - cursorHeight) / 2,
(getPaddingLeft() + passwordSize / 2) + (passwordSize + passwordPadding) * cursorPosition,
getPaddingTop() + (passwordSize + cursorHeight) / 2,
paint);
}
}
現(xiàn)在我們繪制的工作基本完成,但是光標(biāo)還不會閃動腺兴,這里我們用一個(gè)定時(shí)器Timer
讓它閃動左电,具體代碼如下:
private void init() {
...
timerTask = new TimerTask() {
@Override
public void run() {
isCursorShowing = !isCursorShowing;
postInvalidate();
}
};
timer = new Timer();
}
代碼很簡單,就是將當(dāng)前顯示狀態(tài)置反,并且重繪篓足。我們在onAttachedToWindow
方法中開啟定時(shí)器
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
//cursorFlashTime為光標(biāo)閃動的間隔時(shí)間
timer.scheduleAtFixedRate(timerTask, 0, cursorFlashTime);
}
在onDetachedFromWindow
方法中停止定時(shí)器
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
timer.cancel();
}
到這里基本上我們的View已經(jīng)繪制完成并且可以展示段誊,但是我們還缺少密碼輸入還有鍵盤的監(jiān)聽事件呢對吧。這里我們只允許密碼為數(shù)字栈拖,重寫onCreateInputConnection
方法连舍,代碼如下:
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; //輸入類型為數(shù)字
return super.onCreateInputConnection(outAttrs);
}
并且定義一個(gè)監(jiān)聽密碼輸入狀態(tài)的監(jiān)聽者PasswordListener
/**
* 密碼監(jiān)聽者
*/
public interface PasswordListener {
/**
* 輸入/刪除監(jiān)聽
*
* @param changeText 輸入/刪除的字符
*/
void passwordChange(String changeText);
/**
* 輸入完成
*/
void passwordComplete();
/**
* 確認(rèn)鍵后的回調(diào)
*
* @param password 密碼
* @param isComplete 是否達(dá)到要求位數(shù)
*/
void keyEnterPress(String password, boolean isComplete);
}
實(shí)現(xiàn)我們自己的OnKeyListener
class MyKeyListener implements OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
int action = event.getAction();
if (action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_DEL) {
/**
* 刪除操作
*/
if (TextUtils.isEmpty(password[0])) {
return true;
}
String deleteText = delete();
if (passwordListener != null && !TextUtils.isEmpty(deleteText)) {
passwordListener.passwordChange(deleteText);
}
postInvalidate();
return true;
}
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
/**
* 只支持?jǐn)?shù)字
*/
if (isInputComplete) {
return true;
}
String addText = add((keyCode - 7) + "");
if (passwordListener != null && !TextUtils.isEmpty(addText)) {
passwordListener.passwordChange(addText);
}
postInvalidate();
return true;
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
/**
* 確認(rèn)鍵
*/
if (passwordListener != null) {
passwordListener.keyEnterPress(getPassword(), isInputComplete);
}
return true;
}
}
return false;
}
}
這里解釋下為什么輸入數(shù)字的時(shí)候需要將keyCode減去7,這里我們可以查看一下KeyEvent
的源碼
/** Key code constant: '0' key. */
public static final int KEYCODE_0 = 7;
/** Key code constant: '1' key. */
public static final int KEYCODE_1 = 8;
/** Key code constant: '2' key. */
public static final int KEYCODE_2 = 9;
/** Key code constant: '3' key. */
public static final int KEYCODE_3 = 10;
/** Key code constant: '4' key. */
public static final int KEYCODE_4 = 11;
/** Key code constant: '5' key. */
public static final int KEYCODE_5 = 12;
/** Key code constant: '6' key. */
public static final int KEYCODE_6 = 13;
/** Key code constant: '7' key. */
public static final int KEYCODE_7 = 14;
/** Key code constant: '8' key. */
public static final int KEYCODE_8 = 15;
/** Key code constant: '9' key. */
public static final int KEYCODE_9 = 16;
可以發(fā)現(xiàn),數(shù)字所對應(yīng)的keycode與自身數(shù)字相差7
我們接著看按下鍵盤刪除鍵時(shí)的方法delete
/**
* 刪除
*/
private String delete() {
String deleteText = null;
if (cursorPosition > 0) {
deleteText = password[cursorPosition - 1];
password[cursorPosition - 1] = null;
cursorPosition--;
} else if (cursorPosition == 0) {
deleteText = password[cursorPosition];
password[cursorPosition] = null;
}
isInputComplete = false;
return deleteText;
}
邏輯很簡單涩哟,就是記錄下刪除的字符索赏,然后根據(jù)當(dāng)前所處的下標(biāo)刪除后將其返回。
接下來看一下輸入密碼時(shí)的add
方法
/**
* 增加
*/
private String add(String c) {
String addText = null;
if (cursorPosition < passwordLength) {
addText = c;
password[cursorPosition] = c;
cursorPosition++;
if (cursorPosition == passwordLength) {
isInputComplete = true;
if (passwordListener != null) {
passwordListener.passwordComplete();
}
}
}
return addText;
}
邏輯與刪除差不多贴彼,其中多了一個(gè)對是否達(dá)到輸入位數(shù)進(jìn)行了判斷潜腻,并回調(diào)接口。
最后锻弓,別忘記設(shè)置View可點(diǎn)擊砾赔,并且對點(diǎn)擊事件進(jìn)行處理,點(diǎn)擊彈出軟鍵盤青灼,否則點(diǎn)擊控件是不會彈出軟鍵盤的
private void init() {
setFocusableInTouchMode(true);
MyKeyListener MyKeyListener = new MyKeyListener();
setOnKeyListener(MyKeyListener);
inputManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
...
}
點(diǎn)擊的時(shí)候彈出軟鍵盤,我們可以重寫onTouchEvent
方法
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
/**
* 彈出軟鍵盤
*/
requestFocus();
inputManager.showSoftInput(this, InputMethodManager.SHOW_FORCED);
return true;
}
return super.onTouchEvent(event);
}
在失去焦點(diǎn)時(shí)隱藏軟鍵盤妓盲,重寫onWindowFocusChanged
方法
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
inputManager.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
}
重寫onSaveInstanceState
方法和onRestoreInstanceState
對狀態(tài)進(jìn)行保存和恢復(fù)
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("superState", super.onSaveInstanceState());
bundle.putStringArray("password", password);
bundle.putInt("cursorPosition", cursorPosition);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
password = bundle.getStringArray("password");
cursorPosition = bundle.getInt("cursorPosition");
state = bundle.getParcelable("superState");
}
super.onRestoreInstanceState(state);
}
到這里基本主要的邏輯就已經(jīng)ok了杂拨,這里附上Demo的源碼,因?yàn)楸容^簡單悯衬,所以如果有哪個(gè)地方不夠詳細(xì)的話大家可以直接看一下源碼弹沽。