1. 程式人生 > >Android自定義控制元件開發系列(三)——仿支付寶六位支付密碼輸入頁面

Android自定義控制元件開發系列(三)——仿支付寶六位支付密碼輸入頁面

        在移動互聯領域,有那麼幾家龍頭一直是我等學習和追求的目標,比如支付寶、微信、餓了麼、酷狗音樂等等,大神舉不勝舉,他們設計的介面、互動方式已經培養了中國(有可能會是世界)民眾的操作習慣:舉個小例子,對話方塊“確定”按鈕的左右位置就很有學問,如果大家都是左邊取消右邊確定,你的作品偏偏相反,就會導致使用者在操作時候很不適應,甚至會習慣性點錯,這一小小的問題將嚴重影響產品的體驗,閒話少說,開始今天的主題。

        今天來模仿一下支付寶6位支付密碼的輸入控制元件。

        IOS支付寶6位支付密碼        我實現的效果

        我們先來照圖分析一下:(1)限制輸入6位,每一位都有自己的框格,每個格顯示一位;(2)有回退/取消支付按鈕;(3)有忘記密碼連結;(4)自定義的只能輸入數字的鍵盤輸入區;(5)在6位輸完後自動進行密碼校驗和支付交易。如上圖左邊是IOS支付寶支付密碼輸入控制元件,右邊是我模仿實現的效果。下邊我們來一步一步完成這樣的效果:

        首先,我們需要一個頁面來完成以上的靜態佈局,.xml程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:gravity="bottom" >

    <LinearLayout
        android:id="@+id/linear_pass"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="5dp" >

            <!-- 取消按鈕 -->

            <ImageView
                android:id="@+id/img_cancel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/icon_clean" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="輸入密碼"
                android:textColor="#898181"
                android:textSize="20sp" />
        </RelativeLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:background="#555555" />

        <!-- 6位密碼框佈局,需要一個圓角邊框的shape作為layout的背景 -->

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="40dp"
            android:layout_marginRight="40dp"
            android:layout_marginTop="20dp"
            android:background="@drawable/shape_input_area"
            android:orientation="horizontal" >

            <!--
                 inputType設定隱藏密碼明文  
                 textSize設定大一點,否則“點”太小了,不美觀
            -->

            <TextView
                android:id="@+id/tv_pass1"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:inputType="numberPassword"
                android:textSize="32sp" />

            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#999999" />

            <TextView
                android:id="@+id/tv_pass2"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:inputType="numberPassword"
                android:textSize="32sp" />

            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#999999" />

            <TextView
                android:id="@+id/tv_pass3"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:inputType="numberPassword"
                android:textSize="32sp" />

            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#999999" />

            <TextView
                android:id="@+id/tv_pass4"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:inputType="numberPassword"
                android:textSize="32sp" />

            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#999999" />

            <TextView
                android:id="@+id/tv_pass5"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:inputType="numberPassword"
                android:textSize="32sp" />

            <View
                android:layout_width="1dp"
                android:layout_height="match_parent"
                android:background="#999999" />

            <TextView
                android:id="@+id/tv_pass6"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:inputType="numberPassword"
                android:textSize="32sp" />
        </LinearLayout>

        <!-- 忘記密碼連結 -->

        <TextView
            android:id="@+id/tv_forgetPwd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_margin="15dp"
            android:text="忘記密碼?"
            android:textColor="#354EEF" />
    </LinearLayout>

    <!-- 輸入鍵盤 -->

    <GridView
        android:id="@+id/gv_keybord"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/linear_pass"
        android:layout_marginTop="40dp"
        android:background="@android:color/black"
        android:horizontalSpacing="0.5dp"
        android:listSelector="@null"
        android:numColumns="3"
        android:verticalSpacing="0.5dp" /><!-- android:listSelector="@null"取消系統自帶的按下效果,否則模擬鍵盤外圍會有黑邊 -->

</RelativeLayout>

        其中需要圓角背景shape_input_area.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="5dp"/>
    <stroke android:color="@android:color/darker_gray"
        android:width="1dp"/>
    <solid android:color="@android:color/white"/>
</shape>

        需要數字按鈕的背景selector_gride.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="false">
        <shape>
            <solid android:color="#C0C4C7" />
        </shape>
    </item>
    <item android:state_enabled="true" android:state_pressed="false">
        <shape>
            <solid android:color="@android:color/white" />
        </shape>
    </item>
    <item android:state_enabled="true" android:state_pressed="true">
        <shape>
            <solid android:color="#C0C4C7" />
        </shape>
    </item>
</selector>

        需要回退鍵背景selector_key_del.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="false">
        <shape>
            <solid android:color="#C0C4C7" />
        </shape>
    </item>
    <item android:state_enabled="true" android:state_pressed="false">
        <shape>
            <solid android:color="#C0C4C7" />
        </shape>
    </item>
    <item android:state_enabled="true" android:state_pressed="true">
        <shape>
            <solid android:color="@android:color/white" />
        </shape>
    </item>
</selector>

        下面來完成我們的自定義控制元件PasswordView.java:

public class PasswordView extends RelativeLayout implements View.OnClickListener {
    Context context;

    private String strPassword;     //輸入的密碼
    private TextView[] tvList;      //用陣列儲存6個TextView,為什麼用陣列?
                                    //因為就6個輸入框不會變了,用陣列記憶體申請固定空間,比List省空間(自己認為)
    private GridView gridView;    //用GrideView佈局鍵盤,其實並不是真正的鍵盤,只是模擬鍵盤的功能
    private ArrayList<Map<String, String>> valueList;    //有人可能有疑問,為何這裡不用陣列了?
                                                       //因為要用Adapter中適配,用陣列不能往adapter中填充

    private ImageView imgCancel;
    private TextView tvForget;
    private int currentIndex = -1;    //用於記錄當前輸入密碼格位置

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

    public PasswordView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        View view = View.inflate(context, R.layout.layout_popup_bottom, null);
        
        valueList = new ArrayList<Map<String, String>>();
        tvList = new TextView[6];
        
        imgCancel = (ImageView) view.findViewById(R.id.img_cancel);
        imgCancel.setOnClickListener(this);

        tvForget = (TextView) view.findViewById(R.id.tv_forgetPwd);
        tvForget.setOnClickListener(this);
        
        tvList[0] = (TextView) view.findViewById(R.id.tv_pass1);
        tvList[1] = (TextView) view.findViewById(R.id.tv_pass2);
        tvList[2] = (TextView) view.findViewById(R.id.tv_pass3);
        tvList[3] = (TextView) view.findViewById(R.id.tv_pass4);
        tvList[4] = (TextView) view.findViewById(R.id.tv_pass5);
        tvList[5] = (TextView) view.findViewById(R.id.tv_pass6);

        gridView = (GridView) view.findViewById(R.id.gv_keybord);

        setView();
        
        addView(view);      //必須要,不然不顯示控制元件
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.img_cancel:
                Toast.makeText(context, "Cancel", Toast.LENGTH_SHORT).show();
                break;
            case R.id.tv_forgetPwd:
                Toast.makeText(context, "Forget", Toast.LENGTH_SHORT).show();
                break;
        }
    }

    private void setView() {
    	/* 初始化按鈕上應該顯示的數字 */
        for (int i = 1; i < 13; i++) {
            Map<String, String> map = new HashMap<String, String>();
            if (i < 10) {
                map.put("name", String.valueOf(i));
            } else if (i == 10) {
                map.put("name", "");
            } else if (i == 12) {
                map.put("name", "<<-");
            } else if (i == 11) {
                map.put("name", String.valueOf(0));
            }
            valueList.add(map);
        }

        gridView.setAdapter(adapter);
        gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (position < 11 && position != 9) {    //點選0~9按鈕
                    if (currentIndex >= -1 && currentIndex < 5) {      //判斷輸入位置————要小心陣列越界
                        tvList[++currentIndex].setText(valueList.get(position).get("name"));
                    }
                } else {
                    if (position == 11) {      //點選退格鍵
                        if (currentIndex - 1 >= -1) {      //判斷是否刪除完畢————要小心陣列越界
                            tvList[currentIndex--].setText("");
                        }
                    }
                }
            }
        });
    }

    //設定監聽方法,在第6位輸入完成後觸發
    public void setOnFinishInput(final OnPasswordInputFinish pass) {
        tvList[5].addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {
                if (s.toString().length() == 1) {
                    strPassword = "";     //每次觸發都要先將strPassword置空,再重新獲取,避免由於輸入刪除再輸入造成混亂
                    for (int i = 0; i < 6; i++) {
                        strPassword += tvList[i].getText().toString().trim();
                    }
                    pass.inputFinish();    //介面中要實現的方法,完成密碼輸入完成後的響應邏輯
                }
            }
        });
    }

    /* 獲取輸入的密碼 */
    public String getStrPassword() {
        return strPassword;
    }

    /* 暴露取消支付的按鈕,可以靈活改變響應 */
    public ImageView getCancelImageView() {
        return imgCancel;
    }

    /* 暴露忘記密碼的按鈕,可以靈活改變響應 */
    public TextView getForgetTextView() {
        return tvForget;
    }

    //GrideView的介面卡
    BaseAdapter adapter = new BaseAdapter() {
        @Override
        public int getCount() {
            return valueList.size();
        }

        @Override
        public Object getItem(int position) {
            return valueList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            if (convertView == null) {
                convertView = View.inflate(context, R.layout.item_gride, null);
                viewHolder = new ViewHolder();
                viewHolder.btnKey = (TextView) convertView.findViewById(R.id.btn_keys);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            viewHolder.btnKey.setText(valueList.get(position).get("name"));
            if(position == 9){
                viewHolder.btnKey.setBackgroundResource(R.drawable.selector_key_del);
                viewHolder.btnKey.setEnabled(false);
            }
            if(position == 11){
                viewHolder.btnKey.setBackgroundResource(R.drawable.selector_key_del);
            }

            return convertView;
        }
    };

    /**
     * 存放控制元件
     */
    public final class ViewHolder {
        public TextView btnKey;
    }
}

        自認為程式碼註釋還是可以的。就是在實現過程中要注意陣列的越界問題,在輸入邏輯響應中要注意邏輯處理,也就是grideView的OnItemClickListener事件處理。其中用到自定義的介面OnPasswordInputFinish來實現輸入完成的事件回掉:

/**
 * Belong to the Project —— MyPayUI 
 * Created by WangJ on 2015/11/25 17:15.
 * 
 * 自定義介面,用於給密碼輸入完成添加回掉事件
 */
public interface OnPasswordInputFinish {
	void inputFinish();
}

        還有就是Adapter中用到的每個按鈕Item的佈局item_gride.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 模擬鍵盤按鈕,當然你可以用Button,但要注意Button和GrideView的點選響應問題 -->
    <TextView
        android:id="@+id/btn_keys"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp"
        android:gravity="center"
        android:textSize="25sp"
        android:background="@drawable/selector_gride"/>
</LinearLayout>

        好了,到此我們的自定義控制元件——模仿支付寶6位支付密碼輸入控制元件就完成了,下邊我們在Activity中用一下,檢驗一下效果:

        我們在MianActivity中用用一下我們定義好的控制元件:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        /************* 第一種用法————開始 ***************/
        setContentView(R.layout.activity_main);

        final PasswordView pwdView = (PasswordView) findViewById(R.id.pwd_view);
        
        //新增密碼輸入完成的響應
        pwdView.setOnFinishInput(new OnPasswordInputFinish() {
            @Override
            public void inputFinish() {
            	//輸入完成後我們簡單顯示一下輸入的密碼
            	//也就是說——>實現你的交易邏輯什麼的在這裡寫
                Toast.makeText(MainActivity.this, pwdView.getStrPassword(), Toast.LENGTH_SHORT).show();
            }
        });
        
        /**
         *  可以用自定義控制元件中暴露出來的cancelImageView方法,重新提供相應
         *  如果寫了,會覆蓋我們在自定義控制元件中提供的響應
         *  可以看到這裡toast顯示 "Biu Biu Biu"而不是"Cancel"*/
        pwdView.getCancelImageView().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "Biu Biu Biu", Toast.LENGTH_SHORT).show();
            }
        });
        /************ 第一種用法————結束 ******************/

        
        /************* 第二種用法————開始 *****************/
//        final PasswordView pwdView = new PasswordView(this);
//        setContentView(pwdView);
//        pwdView.setOnFinishInput(new OnPasswordInputFinish() {
//            @Override
//            public void inputFinish() {
//                Toast.makeText(MainActivity.this, pwdView.getStrPassword(), Toast.LENGTH_SHORT).show();
//            }
//        });
        /************** 第二種用法————結束 ****************/
    }
}

        在第一種方法中我們用到的佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    android:id="@+id/xxx"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#624762">

    <com.wangj.mypayview.PasswordView
        android:id="@+id/pwd_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"/>
</RelativeLayout>

        看圖(左方法一、右方法二):

        

肯定有人有疑問,為什麼兩個差別這麼大?為什麼這裡不想支付寶一樣是從底部向上彈出?這裡我需要解釋一下:

    (1)我們這裡只是完成了這樣一個控制元件,至於像支付寶一樣從底部向上彈出,需要藉助別的方法,比如用PopupWindow將控制元件包裹進行彈窗等方法,我們這裡只是顯示出來驗證一下;

    (2)我們沒有單獨定義自己的密碼鍵盤,只是模擬了一下鍵盤的功能,所以密碼輸入框和鍵盤不能分離,如果有需求,你要單獨定義安全鍵盤;

    (3)這裡每個按鈕我是用一個只包含TextView的佈局模擬的,你可以根據需求進行更改,甚至每個按鈕都可以自定義,我這裡只是提供一種實現思路。

        以上是我自己想到的實現方式,如果各位有更好的方法,麻煩留言教教我啊,程式猿大人再次先謝了水平有限,如有不足,歡迎指出