工作中經常會用到驗證碼輸入框,但是網上好多都是用4個TextView和一個隱藏的EditText,這樣複用性不是很好。萬幸找到了一個自定義View的案例,可以說是用很方便,也有很好的複用性,也是一個學習自定義View的不錯的例子,通俗易懂,所以在這裏分享一下。首先感謝作者Android 自定義驗證碼/密碼輸入框,驗證碼的樣式完全由自定義Drawable決定
首先看一下效果
下面逐步說一下思路:
- 定義自定義屬性:這裏自定義了
itemCount,itemWidth,itemBackground,gapWidth,ciTextSize,ciTextColor
- 完成測量
- 這裏只需要考慮測量寬,高有xml指定具體值。
- 當爲wrap_content時,需要自行測量控件總寬度
- 爲match_parent或者具體指時,需要根據測量值重新計算itemWidth,這一點可以有不同實現
- 完成繪製
- 繪製背景和繪製字體的座標計算還是需要理解的
- 繪製背景:自定義背景xml使用了focuse表示當前獲取焦點的item,所以這裏需要設置item的狀態來繪製不同的背景
- 繪製字體—繪製字體居中在item的區域主要有兩個關鍵:
- 設置畫筆
textPaint.setTextAlign(Paint.Align.CENTER)
這樣方便找畫字體的x座標 - 計算baseLine,參考了《Android自定義開發控件開發與入門實戰》中的片段
- 設置畫筆
- 軟鍵盤的使用:
- 控件能夠彈出軟鍵盤需要
setFocusableInTouchMode(true)
和在事件處理,在onTouchEvent
中判斷按下事件彈出軟鍵盤 - 軟鍵盤的控制
- 重寫
onCreateInputConnection
來控制彈出的軟鍵盤的類型,類似於EditText的inputType並重寫onCheckIsTextEditor
- 監聽鍵盤按鍵事件,這裏重寫了
onKeyDown
這樣不知道有沒有問題,我這邊自行測試沒啥問題 - 在監聽鍵盤按鍵事件中,要計算按下的是什麼,設置監聽回調,並重繪。
- 重寫
- 控件能夠彈出軟鍵盤需要
部分代碼
-
測量
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int mode = MeasureSpec.getMode(widthMeasureSpec); switch (mode) { case MeasureSpec.UNSPECIFIED: case MeasureSpec.AT_MOST: //沒有指定寬度 寬度等於單個的寬度*個數+總間距 mWidth = itemWidth * itemCount + gapWidth * (itemCount - 1); break; case MeasureSpec.EXACTLY: mWidth = MeasureSpec.getSize(widthMeasureSpec); itemWidth = mWidth - (gapWidth * (itemCount - 1)) / itemCount; break; } mHeight = MeasureSpec.getSize(heightMeasureSpec); calculateStartAndEndPoint(itemCount); setMeasuredDimension(mWidth, mHeight); }
-
繪製
private void drawLine(Canvas canvas) { if (codeBuilder == null) return; int inputLength = codeBuilder.length(); //畫背景 for (int i = 0; i < itemCount; i++) { if (itemBackground != null) { //繪製背景區域 itemBackground.setBounds((int) itemPoints[i].x, 0, (int) itemPoints[i].y, mHeight); //關鍵點4:設置背景的狀態,索引等於字符串長度,代表當前item處於FOCUSED狀態,否則處於普通狀態 //這樣可以使用xml文件Selector來的定義不同狀態下的背景 itemBackground.setState(i == inputLength ? FOCUSED_STATE_SET : EMPTY_STATE_SET); itemBackground.draw(canvas);//把背景畫上去 } } //畫字體 Paint.FontMetricsInt fontMetricsInt = textPaint.getFontMetricsInt(); //關鍵點5:基線的y座標(固定套路) int baseline = mHeight / 2 + (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom; for (int i = 0; i < inputLength; i++) { if (inputType == InputMode.INPUT_TYPE_NUMBER_PASSWORD || inputType == InputMode.NPUT_TYPE_PASSWORD) { canvas.drawCircle(itemPoints[i].y - itemWidth / 2, mHeight / 2, testSize / 4, textPaint); } else { canvas.drawText(codeBuilder.toString(), i, i + 1, itemPoints[i].y - itemWidth / 2, baseline, textPaint); } }
-
彈出軟鍵盤
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { // requestFocus();//關鍵點6:只有請求獲取焦點,纔會彈出軟鍵盤 KeyBoardUtil.showKeyboard(getContext(), this); return true; } return false; }
4.定義鍵盤類型
/** * //關鍵點7:定義軟鍵盤類型 * 創建一個新的InputConnection以便當前視圖可以使用InputMethod,此方法默認實現返回null, * 因此它不支持輸入法。因此如果當前視圖想要使用輸入法,則必須重寫此方法,只有具有焦點和需要文本輸入的視圖 * 才需要主動調用此方法 * Create a new InputConnection for an InputMethod to interact * with the view. The default implementation returns null, since it doesn't * support input methods. You can override this to implement such support. * This is only needed for views that take focus and text input. * <p> * 當實現此方法後,最好也重寫onCheckIsTextEditor方法來表明你將返回一個非空的InputConnection * <p>When implementing this, you probably also want to implement * {@link #onCheckIsTextEditor()} to indicate you will return a * non-null InputConnection.</p> * <p> * 另外,你必須指定EditorInfo的一些參數,以便系統輸入法引擎可以參考這些參數來決定軟鍵盤的信息 * <p>Also, take good care to fill in the {@link android.view.inputmethod.EditorInfo} * object correctly and in its entirety, so that the connected IME(Input Method Engine) can rely * on its values. For example, {@link android.view.inputmethod.EditorInfo#initialSelStart} * and {@link android.view.inputmethod.EditorInfo#initialSelEnd} members * must be filled in with the correct cursor position for IMEs to work correctly * with your application.</p> * <p> * 配置有關連接的屬性信息 * * @param outAttrs Fill in with attribute information about the connection. */ @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { BaseInputConnection bic = new BaseInputConnection(this, false); outAttrs.actionLabel = null; if (inputType == InputMode.INPUT_TYPE_NUMBER) { outAttrs.inputType = InputType.TYPE_CLASS_NUMBER; } else if (inputType == InputMode.INPUT_TYPE_TEXT) { outAttrs.inputType = InputType.TYPE_CLASS_TEXT; } else if (inputType == InputMode.INPUT_TYPE_TEXT_CAP_CHARACTERS) { outAttrs.inputType = InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; } else if (inputType == InputMode.NPUT_TYPE_PASSWORD) { outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD; } else if (inputType == InputMode.INPUT_TYPE_NUMBER_PASSWORD) { outAttrs.inputType = InputType.TYPE_NUMBER_VARIATION_PASSWORD; } outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT; return bic; }
-
處理鍵盤按鍵監聽
/** * 關鍵點9 後續研究監聽軟鍵盤的更好解決方案 * 鍵盤迴調函數,默認實現了ENTER鍵的動作 * Default implementation of {@link KeyEvent.Callback#onKeyDown(int, KeyEvent) * KeyEvent.Callback.onKeyDown()}: perform press of the view * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER} * is released, if the view is enabled and clickable. * 按壓軟鍵盤的按鍵通常不會觸發此監聽,儘管某些情況下有些人會選擇實現此方法監聽軟鍵盤的鍵盤監聽 * 但是不要依靠此來監聽軟鍵盤按鍵 * Key presses in software keyboards will generally NOT trigger this * listener, although some may elect to do so in some situations. Do not * rely on this to catch software key presses. * * @param keyCode a key code that represents the button pressed, from * {@link android.view.KeyEvent} * @param event the KeyEvent object that defines the button action */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (codeBuilder == null) codeBuilder = new StringBuilder(); if (keyCode == KeyEvent.KEYCODE_DEL) { //刪除 deleteLast(); } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { //純數字 appendText(String.valueOf(event.getNumber())); } else if (((inputType == InputMode.INPUT_TYPE_TEXT || inputType == InputMode.INPUT_TYPE_TEXT_CAP_CHARACTERS || inputType == InputMode.NPUT_TYPE_PASSWORD)) && keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) { String text = String.valueOf((char) event.getUnicodeChar()); appendText(inputType == InputMode.INPUT_TYPE_TEXT_CAP_CHARACTERS ? text.toUpperCase() : text); } if (codeBuilder.length() >= itemCount || keyCode == KeyEvent.KEYCODE_ENTER) { KeyBoardUtil.hideKeyboard(getContext(), this); } return super.onKeyDown(keyCode, event); }
完整代碼見github