Android自定義驗證碼/密碼輸入框(輸入框樣式完全由自定義背景決定),高度複用性

工作中經常會用到驗證碼輸入框,但是網上好多都是用4個TextView和一個隱藏的EditText,這樣複用性不是很好。萬幸找到了一個自定義View的案例,可以說是用很方便,也有很好的複用性,也是一個學習自定義View的不錯的例子,通俗易懂,所以在這裏分享一下。首先感謝作者Android 自定義驗證碼/密碼輸入框,驗證碼的樣式完全由自定義Drawable決定

首先看一下效果


下面逐步說一下思路:

  1. 定義自定義屬性:這裏自定義了itemCount,itemWidth,itemBackground,gapWidth,ciTextSize,ciTextColor
  2. 完成測量
    1. 這裏只需要考慮測量寬,高有xml指定具體值。
    2. 當爲wrap_content時,需要自行測量控件總寬度
    3. 爲match_parent或者具體指時,需要根據測量值重新計算itemWidth,這一點可以有不同實現
  3. 完成繪製
    1. 繪製背景和繪製字體的座標計算還是需要理解的
    2. 繪製背景:自定義背景xml使用了focuse表示當前獲取焦點的item,所以這裏需要設置item的狀態來繪製不同的背景
    3. 繪製字體—繪製字體居中在item的區域主要有兩個關鍵:
      • 設置畫筆textPaint.setTextAlign(Paint.Align.CENTER)這樣方便找畫字體的x座標
      • 計算baseLine,參考了《Android自定義開發控件開發與入門實戰》中的片段
  4. 軟鍵盤的使用:
    1. 控件能夠彈出軟鍵盤需要setFocusableInTouchMode(true)和在事件處理,在onTouchEvent中判斷按下事件彈出軟鍵盤
    2. 軟鍵盤的控制
      1. 重寫onCreateInputConnection來控制彈出的軟鍵盤的類型,類似於EditText的inputType並重寫onCheckIsTextEditor
      2. 監聽鍵盤按鍵事件,這裏重寫了onKeyDown這樣不知道有沒有問題,我這邊自行測試沒啥問題
      3. 在監聽鍵盤按鍵事件中,要計算按下的是什麼,設置監聽回調,並重繪。

部分代碼

  1. 測量

    @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);
     }
    
  2. 繪製

    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);
            }
        }
    
  3. 彈出軟鍵盤

    @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;
    }
    
  4. 處理鍵盤按鍵監聽

     /**
      * 關鍵點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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章