Android自定義View之密碼輸入框

本文為Android自定義View系列琳水,難度不大,都是些坐標(biāo)的計(jì)算,閱讀本文大概需要5分鐘顽素。

Demo效果圖

方框模式

下劃線模式

分析

寬高計(jì)算
各個(gè)長寬
各個(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ì)的話大家可以直接看一下源碼弹沽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市筋粗,隨后出現(xiàn)的幾起案子策橘,更是在濱河造成了極大的恐慌,老刑警劉巖娜亿,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丽已,死亡現(xiàn)場離奇詭異,居然都是意外死亡买决,警方通過查閱死者的電腦和手機(jī)沛婴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來督赤,“玉大人嘁灯,你說我怎么就攤上這事《闵啵” “怎么了丑婿?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我羹奉,道長秒旋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任尘奏,我火速辦了婚禮滩褥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘炫加。我一直安慰自己瑰煎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布俗孝。 她就那樣靜靜地躺著酒甸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赋铝。 梳的紋絲不亂的頭發(fā)上插勤,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機(jī)與錄音革骨,去河邊找鬼农尖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛良哲,可吹牛的內(nèi)容都是我干的盛卡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼筑凫,長吁一口氣:“原來是場噩夢啊……” “哼滑沧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巍实,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎令漂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洗显,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年原环,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挠唆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘱吗。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡滔驾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俄讹,到底是詐尸還是另有隱情哆致,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布患膛,位于F島的核電站,受9級特大地震影響踪蹬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漱牵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一疚漆、第九天 我趴在偏房一處隱蔽的房頂上張望酣胀。 院中可真熱鬧娶聘,春花似錦、人聲如沸丸升。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽波闹。三九已至,卻和暖如春精堕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背歹篓。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工庄撮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留背捌,地道東北人洞斯。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像么抗,于是被迫代替她去往敵國和親毅否。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容