1. 程式人生 > >自定義控制元件之 PasswordEditText(密碼輸入框)

自定義控制元件之 PasswordEditText(密碼輸入框)

前兩天在掘金上看到了一個驗證碼輸入框,然後自己實現了一下,以前都是繼承的 View,這次繼承了 ViewGroup,也算是嘗試了一點不同的東西。先看看最終效果:

事實上就是用將輸入的密碼用幾個文字框來顯示而已,要打造這樣一個東西我剛開始也是一頭霧水,不急,直接寫不會,我們可以採取曲線救國的方法。下面我來說說我的思路。

1 準備工作

光看圖上效果沒有什麼頭緒的話,但是我相信下面這個佈局大家肯定都會寫:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.qinshou.passwordedittext.MainActivity">

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="80dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:background="@drawable/bg_inputing" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:background="@drawable/bg_inputed" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:background="@drawable/bg_inputed" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:background="@drawable/bg_inputed" />
        </LinearLayout>

        <EditText
            android:id="@+id/et_password"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@null"
            android:cursorVisible="false" />
    </RelativeLayout>
</android.support.constraint.ConstraintLayout>

 

這樣佈局寫出來的效果是這樣:

 

這就是我們要的效果吧,所以下面的工作就簡單了,就是把這個佈局給封裝到一個自定義控制元件中去。這裡面有好多 View,所以我們需要繼承的是一個 ViewGroup,根據上面的佈局,選擇繼承 RelativeLayout。

 

2 寫PasswordEditText佈局

我們需要在一個 RelativeLayout 中新增 1 個橫向的 LinearLayout 用來裝 4 個 用來顯示的 EditText,為什麼用 EditText 不用 TextView 是因為 EditText 可以密文顯示。然後還需要一個 EditText 來彈出軟鍵盤用來輸入。

 

public class PasswordEditText extends RelativeLayout {
    private Context mContext;
    private EditText mEditText;
    private List<EditText> editTexts;   //選用 EditText 而不用 TextView 是因為 EditText 可以密文顯示

    public PasswordEditText(Context context) {
        this(context, null);
    }

    public PasswordEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initView();
    }


    /**
     * Description:新增控制元件,密碼框和不可見的輸入框
     * Date:2017/8/18
     */
    private void initView() {
        //新建一個容器
        LinearLayout mLinearLayout = new LinearLayout(mContext);
        LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        mLinearLayout.setLayoutParams(linearLayoutParams);
        mLinearLayout.setOrientation(LinearLayout.HORIZONTAL);

        //根據 count 新增一定數量的密碼框
        editTexts = new ArrayList<EditText>();
        LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams(getScreenWidth(mContext) / 4 - dip2px(mContext, 20), getScreenWidth(mContext) / count - dip2px(mContext, 20));
        textViewParams.setMargins(dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10));
        for (int i = 0; i < 4; i++) {
            EditText mEditText = new EditText(mContext);
            mEditText.setLayoutParams(textViewParams);
            mEditText.setBackgroundResource(R.drawable.bg_inputed); //設定背景
            mEditText.setGravity(Gravity.CENTER);   //設定文字顯示位置
            mEditText.setTextSize(24);    //設定文字大小
            mEditText.setFocusable(false);  //設定無法獲得焦點
            editTexts.add(mEditText);
            mLinearLayout.addView(mEditText);
        }
        editTexts.get(0).setBackgroundResource(R.drawable.bg_inputing);

        //新增不可見的 EditText
        LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        mEditText = new EditText(mContext);
        mEditText.setLayoutParams(editTextParams);
        mEditText.setCursorVisible(false);  //設定輸入遊標不可見
        mEditText.setBackgroundResource(0); //設定透明背景,讓下劃線不可見
        mEditText.setAlpha(0.0f);   //設定為全透明,讓輸入的內容不可見
        mEditText.setInputType(InputType.TYPE_CLASS_NUMBER);    //設定只能輸入數字
        mEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(count)});   //限制輸入長度

        addView(mLinearLayout);
        addView(mEditText);
    }

    /**
     * Description:獲取螢幕寬度
     * Date:2017/8/18
     */
    private int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        return wm.getDefaultDisplay().getWidth();
    }

    /**
     * 根據手機的解析度從 dp 的單位 轉成為 px(畫素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根據手機的解析度從 px(畫素) 的單位 轉成為 dp
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

}


現在的效果就跟剛才一樣了:

 

3 新增輸入監聽器

光有佈局還不行,我們要讓它可以輸入,讓輸入的內容顯示到對應的 4 個密碼框中,這裡監聽的肯定是輸入的那個 EditText,為其新增 TextWatcher,這裡介紹一下 TextWatcher 這個監聽器。

TextWatch 有 3 個回撥方法:

public void beforeTextChanged(CharSequence s, int start, int count, int after):這是監聽內容改變前,s 是內容改變前的文字,start 是內容改變操作後輸入游標所在位置,count 刪除內容時是刪除字元的個數,增加內容時為 0,after 增加內容時是增加字元的個數,刪除內容時為 0。
public void onTextChanged(CharSequence s, final int start, int before, int count):監聽內容改變後,s 是內容改變後的文字,start 是內容改變操作後游標的位置,count 增加內容時是增加字元的個數,刪除內容時為 0,after 刪除內容時是刪除字元的個數,增加內容時為 0。

public void afterTextChanged(Editable s):監聽內容改變後,s 為內容改變後的文字。

 

我們主要的操作就是在 onTextChanged() 中,增加第一個密碼,顯示到第一個密碼框中,增加第二個,顯示到第二個上,刪除時也是從後往前依次刪除。

            mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            /**
             *
             * @param s 輸入後的文字
             * @param start 輸入的位置
             * @param before 輸入的位置上之前的字元數
             * @param count 輸入的位置上新輸入的字元數
             */
            @Override
            public void onTextChanged(CharSequence s, final int start, int before, int count) {
                if (before == 0 && count == 1) {
                    //為對應顯示框設定對應顯示內容
                    editTexts.get(start).setText(s.subSequence(start, start + 1));
                    //修改輸入了內容的密碼框的背景
                    editTexts.get(start).setBackgroundResource(R.drawable.bg_inputed);
                    //如果還有下一個密碼框,將其背景設定為待輸入的背景
                    if (start + 1 < editTexts.size()) {
                        editTexts.get(start + 1).setBackgroundResource(R.drawable.bg_inputing);
                    } else {
                        //輸入完成後關閉軟鍵盤
                        InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
                                Context.INPUT_METHOD_SERVICE);
                        imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
                    }
                } else if (before == 1 && count == 0) {
                    //清除退格位置對應顯示框的內容
                    editTexts.get(start).setText("");
                    //將其退格的位置設定為明文顯示
                    editTexts.get(start).setTransformationMethod(HideReturnsTransformationMethod.getInstance());
                    //設定退格位置的背景
                    for (EditText editText : editTexts) {
                        editText.setBackgroundResource(R.drawable.bg_inputed);
                    }
                    editTexts.get(start).setBackgroundResource(R.drawable.bg_inputing);
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
            }
        });

 

現在效果已經初具成效了:


 

4 密文顯示

然後我們需要在輸入後一段時間變為密文顯示,這裡我選擇了 0.5s,同時,在下一個密碼框有輸入時,如果上一個密碼框還沒有變為密文顯示的話則立即將其設定為密文顯示,這裡同樣是在 onTextChanged() 方法中操作:

            mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            /**
             *
             * @param s 輸入後的文字
             * @param start 輸入的位置
             * @param before 輸入的位置上之前的字元數
             * @param count 輸入的位置上新輸入的字元數
             */
            @Override
            public void onTextChanged(CharSequence s, final int start, int before, int count) {
                if (before == 0 && count == 1) {
                    //為對應顯示框設定對應顯示內容
                    editTexts.get(start).setText(s.subSequence(start, start + 1));
                    //修改輸入了內容的密碼框的背景
                    editTexts.get(start).setBackgroundResource(R.drawable.bg_inputed);
                    //如果還有下一個密碼框,將其背景設定為待輸入的背景
                    if (start + 1 < editTexts.size()) {
                        editTexts.get(start + 1).setBackgroundResource(R.drawable.bg_inputing);
                    } else {
                        //輸入完成後關閉軟鍵盤
                        InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
                                Context.INPUT_METHOD_SERVICE);
                        imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
                    }
                    //如果需要密文顯示,則 0.5s 後設置為密文顯示
                    editTexts.get(start).postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            editTexts.get(start).setTransformationMethod(PasswordTransformationMethod.getInstance());
                        }
                    }, 500);
                    //如果上一個顯示框還不是密文顯示的話,立即將其設定為密文顯示,前提是需要密文顯示
                    if (start > 0 && editTexts.get(start - 1).getTransformationMethod() instanceof HideReturnsTransformationMethod) {
                        editTexts.get(start - 1).setTransformationMethod(PasswordTransformationMethod.getInstance());
                    }
                } else if (before == 1 && count == 0) {
                    //清除退格位置對應顯示框的內容
                    editTexts.get(start).setText("");
                    //將其退格的位置設定為明文顯示
                    editTexts.get(start).setTransformationMethod(HideReturnsTransformationMethod.getInstance());
                    //設定退格位置的背景
                    for (EditText editText : editTexts) {
                        editText.setBackgroundResource(R.drawable.bg_inputed);
                    }
                    editTexts.get(start).setBackgroundResource(R.drawable.bg_inputing);
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
            }
        });

 

效果如下:

 

5 新增自定義屬性

至此,我已經實現了我想要的效果了,為了讓它有更好的適應性,我們可以為其新增一些自定義屬性,然後動態獲取這些屬性來更好的自定義這個控制元件,畢竟老去修改原始碼還是挺麻煩的:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PasswordEditText">
        <!-- 密碼框數量 -->
        <attr name="count" format="integer" />
        <!-- 密碼字型大小 -->
        <attr name="passwordSize" format="integer" />
        <!-- 是否顯示輸入的密碼 -->
        <attr name="showPassword" format="boolean" />
        <!-- 下一個待輸入的密碼框的背景 -->
        <attr name="bgInputing" format="reference" />
        <!-- 已經輸入了密碼框的背景 -->
        <attr name="bgInputed" format="reference" />
    </declare-styleable>
</resources>

 

如何獲取自定義屬性這個網上很多教程,我也不贅述如何設定,具體的可以看看最後的完整原始碼。

 

6 總結

這是第一次寫繼承一個 ViewGroup,自定義控制元件這個東西,可以說是一直都得學,但永遠也學不完,因為需求總是在變的,但是萬變不離其宗,多寫幾個控制元件,我相信再複雜的控制元件,我們在加以思考後也能實現。最近找到了新工作,真的感覺自己很幸運,公司不錯,同事也很好,希望自己能夠快速融入其中然後幫著做事,實現自己的價值。新公司不算是很忙,但是感覺自己很多不會的,在閒暇時間我要繼續提高自己,對得起自己的工作。

 

7 原始碼

package com.qinshou.passwordedittext;

import android.content.Context;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.HideReturnsTransformationMethod;
import android.text.method.PasswordTransformationMethod;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;

import java.util.ArrayList;
import java.util.List;

/**
 * Description:密碼輸入框,也可明文顯示,用作輸入驗證碼
 * Created by 禽獸先生
 * Created on 2017/8/17
 */

public class PasswordEditText extends RelativeLayout {
    private Context mContext;
    private EditText mEditText;
    private List<EditText> editTexts;   //選用 EditText 而不用 TextView 是因為 EditText 可以密文顯示
    private int count = 4;  //密碼框數量
    private int passwordSize = 24;  //密碼文字大小
    private boolean showPassword = true;   //密碼是否密文顯示,true 為一直明文顯示,false 為 0.5s 後密文顯示
    private int bgInputing = R.drawable.bg_inputing; //待輸入的密碼框的背景
    private int bgInputed = R.drawable.bg_inputed;  //非待輸入的密碼框的背景
    private onCompletionListener mOnCompletionListener;

    public PasswordEditText(Context context) {
        this(context, null);
    }

    public PasswordEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initAttr(attrs, defStyleAttr);
        initView();
        addListener();
    }

    /**
     * Description:初始化自定義屬性
     * Date:2017/8/19
     */
    private void initAttr(AttributeSet attrs, int defStyleAttr) {
        TypedArray mTypeArray = mContext.getTheme().obtainStyledAttributes(attrs, R.styleable.PasswordEditText, defStyleAttr, 0);
        count = mTypeArray.getInt(R.styleable.PasswordEditText_count, 4);
        passwordSize = mTypeArray.getInt(R.styleable.PasswordEditText_passwordSize, 24);
        showPassword = mTypeArray.getBoolean(R.styleable.PasswordEditText_showPassword, true);
        bgInputing = mTypeArray.getResourceId(R.styleable.PasswordEditText_bgInputing, R.drawable.bg_inputing);
        bgInputed = mTypeArray.getResourceId(R.styleable.PasswordEditText_bgInputed, R.drawable.bg_inputed);
    }

    /**
     * Description:新增控制元件,密碼框和不可見的輸入框
     * Date:2017/8/18
     */
    private void initView() {
        //新建一個容器
        LinearLayout mLinearLayout = new LinearLayout(mContext);
        LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        mLinearLayout.setLayoutParams(linearLayoutParams);
        mLinearLayout.setOrientation(LinearLayout.HORIZONTAL);

        //根據 count 新增一定數量的密碼框
        editTexts = new ArrayList<EditText>();
        LinearLayout.LayoutParams textViewParams = new LinearLayout.LayoutParams(getScreenWidth(mContext) / count - dip2px(mContext, 20), getScreenWidth(mContext) / count - dip2px(mContext, 20));
        textViewParams.setMargins(dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10), dip2px(mContext, 10));
        for (int i = 0; i < count; i++) {
            EditText mEditText = new EditText(mContext);
            mEditText.setLayoutParams(textViewParams);
            mEditText.setBackgroundResource(R.drawable.bg_inputed); //設定背景
            mEditText.setGravity(Gravity.CENTER);   //設定文字顯示位置
            mEditText.setTextSize(passwordSize);    //設定文字大小
            mEditText.setFocusable(false);  //設定無法獲得焦點
            editTexts.add(mEditText);
            mLinearLayout.addView(mEditText);
        }
        editTexts.get(0).setBackgroundResource(bgInputing);

        //新增不可見的 EditText
        LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        mEditText = new EditText(mContext);
        mEditText.setLayoutParams(editTextParams);
        mEditText.setCursorVisible(false);  //設定輸入遊標不可見
        mEditText.setBackgroundResource(0); //設定透明背景,讓下劃線不可見
        mEditText.setAlpha(0.0f);   //設定為全透明,讓輸入的內容不可見
        mEditText.setInputType(InputType.TYPE_CLASS_NUMBER);    //設定只能輸入數字
        mEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(count)});   //限制輸入長度

        addView(mLinearLayout);
        addView(mEditText);
    }

    /**
     * Description:為輸入框新增監聽器
     * Date:2017/8/18
     */
    private void addListener() {
        mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            /**
             *
             * @param s 輸入後的文字
             * @param start 輸入的位置
             * @param before 輸入的位置上之前的字元數
             * @param count 輸入的位置上新輸入的字元數
             */
            @Override
            public void onTextChanged(CharSequence s, final int start, int before, int count) {
                if (before == 0 && count == 1) {
                    //為對應顯示框設定對應顯示內容
                    editTexts.get(start).setText(s.subSequence(start, start + 1));
                    //修改輸入了內容的密碼框的背景
                    editTexts.get(start).setBackgroundResource(bgInputed);
                    //如果還有下一個密碼框,將其背景設定為待輸入的背景
                    if (start + 1 < editTexts.size()) {
                        editTexts.get(start + 1).setBackgroundResource(bgInputing);
                    } else {
                        //輸入完成後關閉軟鍵盤
                        InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
                                Context.INPUT_METHOD_SERVICE);
                        imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
                        //如果添加了監聽器,則回撥
                        if (mOnCompletionListener != null) {
                            mOnCompletionListener.onCompletion(s.toString());
                        }
                    }
                    //如果需要密文顯示,則 0.5s 後設置為密文顯示
                    if (!showPassword) {
                        editTexts.get(start).postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                editTexts.get(start).setTransformationMethod(PasswordTransformationMethod.getInstance());
                            }
                        }, 500);
                    }
                    //如果上一個顯示框還不是密文顯示的話,立即將其設定為密文顯示,前提是需要密文顯示
                    if (!showPassword && start > 0 && editTexts.get(start - 1).getTransformationMethod() instanceof HideReturnsTransformationMethod) {
                        editTexts.get(start - 1).setTransformationMethod(PasswordTransformationMethod.getInstance());
                    }
                } else if (before == 1 && count == 0) {
                    //清除退格位置對應顯示框的內容
                    editTexts.get(start).setText("");
                    //將其退格的位置設定為明文顯示
                    editTexts.get(start).setTransformationMethod(HideReturnsTransformationMethod.getInstance());
                    //設定退格位置的背景
                    for (EditText editText : editTexts) {
                        editText.setBackgroundResource(bgInputed);
                    }
                    editTexts.get(start).setBackgroundResource(bgInputing);
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
            }
        });
    }

    public void setOnCompleteListener(onCompletionListener onCompletionListener) {
        this.mOnCompletionListener = onCompletionListener;
    }

    /**
     * Description:獲取螢幕寬度
     * Date:2017/8/18
     */
    private int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        return wm.getDefaultDisplay().getWidth();
    }

    /**
     * 根據手機的解析度從 dp 的單位 轉成為 px(畫素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * 根據手機的解析度從 px(畫素) 的單位 轉成為 dp
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    public interface onCompletionListener {
        void onCompletion(String code);
    }
}

這個小東西已經傳到 Github 上了,傳送門