在项目开发中经常需要自定义键盘和自定义输入框,尤其是涉及修改密码的相关功能;故此学习后进行记录~

因为是自己在Git上找的 功能项目,写Demo时发现引用不是很便利;所以查看作者源码了解到该功能采用了自定义控件实现,抱着求知的心态,就一步步跟着走了一次,索性最后是成功的,有兴趣的可以 下载Demo


一别数年

  • 基本了解
  • 实现过程
  • 自定义 输入框
  • 自定义 数字键盘
  • 场景使用


这是2018年时记录的一篇blog,当时只顾着以实现业务功能为目的学习,其实内部实现并未完全搞懂,特此在2023重新完善一下

基本了解

Demo效果

Androidstudio记住密码功能_输入框

分包结构

Androidstudio记住密码功能_Androidstudio记住密码功能_02


实现过程

主要由输入框自定义控件数字键盘自定义控件使用场景组成,俩款自定义控件的实现方式也是采用了 xml + 自定义逻辑 的方式实现,相对更适合新手入门学习 ~

自定义 输入框

以前没有细看逻辑,只顾着实现功能了,现在补充一下实现逻辑

根据输入框的实现方式,承载输入内容的其实有俩者

  1. 视图控件效果(每个单独输入框的单数据
  2. 用于提交使用的完整数据(所有输入框的综合数据

调用数字键盘的监听回调,当用户点击数字键盘时获取用户操作

  • 如果为数字,则设置输入框单数据完整数据appen拼接
  • 如果为删除,则清除输入框单数据从后往前del完整数据
  • 如果为完成这部分逻辑每个人都不同,自定定义即可

单独对第六个输入框实行监听(addTextChangedListener),判断完整数据的有效性,然后将最终结果回调到外部即可

PayEditText

package com.example.yongliu.payview;

import android.content.Context;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * author  yongliu
 * date  2018/2/28.
 * desc: 自定义输入框样式、功能
 */

public class PayEditText extends LinearLayout{
    private Context context;
    private TextView tvFirst, tvSecond, tvThird, tvForth, tvFifth, tvSixth;
    private StringBuilder mPassword;
    private OnInputFinishedListener onInputFinishedListener;

    public PayEditText(Context context){
        this(context, null);
        initPayEditText();
        initEvent();
    }

    public PayEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        initPayEditText();
        initEvent();
    }

    public PayEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initPayEditText();
        initEvent();
    }

    private void initEvent() {
        tvSixth.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 (onInputFinishedListener != null && mPassword != null && mPassword.toString().length() == 6 && !TextUtils.isEmpty(s.toString())) {
                    onInputFinishedListener.onInputFinished(mPassword.toString());
                }
            }
        });
    }

    /**
     * 初始化PayEditText
     */
    private void initPayEditText() {
        View view = View.inflate(context, R.layout.view_pay_edit, null);
        tvFirst = (TextView) view.findViewById(R.id.tv_pay1);
        tvSecond = (TextView) view.findViewById(R.id.tv_pay2);
        tvThird = (TextView) view.findViewById(R.id.tv_pay3);
        tvForth = (TextView) view.findViewById(R.id.tv_pay4);
        tvFifth = (TextView) view.findViewById(R.id.tv_pay5);
        tvSixth = (TextView) view.findViewById(R.id.tv_pay6);

        mPassword = new StringBuilder();
        addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    }

    /**
     * 输入第一个密码
     * @param first
     */
    public void setTextFirst(String first) {
        tvFirst.setText(first);
        mPassword.append(first);
    }

    /**
     * 输入第二个密码
     * @param second
     */
    public void setTextSecond(String second) {
        tvSecond.setText(second);
        mPassword.append(second);
    }

    /**
     * 输入第三个密码
     * @param third
     */
    public void setTextThird(String third) {
        tvThird.setText(third);
        mPassword.append(third);
    }

    /**
     * 输入第四个密码
     * @param forth
     */
    public void setTextForth(String forth) {
        tvForth.setText(forth);
        mPassword.append(forth);
    }

    /**
     * 输入第五个密码
     * @param fifth
     */
    public void setTextFifth(String fifth) {
        tvFifth.setText(fifth);
        mPassword.append(fifth);
    }

    /**
     * 输入第六位密码
     * @param sixth
     */
    public void setTextSixth(String sixth) {
        tvSixth.setText(sixth);
        mPassword.append(sixth);
    }

    /**
     * 输入密码
     * @param value
     */
    public void add(String value) {
        if (mPassword != null && mPassword.length() < 6) {
            mPassword.append(value);
            if (mPassword.length() == 1) {
                tvFirst.setText(value);
            } else if (mPassword.length() == 2) {
                tvSecond.setText(value);
            }else if (mPassword.length() == 3) {
                tvThird.setText(value);
            }else if (mPassword.length() == 4) {
                tvForth.setText(value);
            }else if (mPassword.length() == 5) {
                tvFifth.setText(value);
            }else if (mPassword.length() == 6) {
                tvSixth.setText(value);
            }
        }
    }

    /**
     * 删除密码
     */
    public void remove() {
        if (mPassword != null && mPassword.length() > 0) {
            if (mPassword.length() == 1) {
                tvFirst.setText("");
            } else if (mPassword.length() == 2) {
                tvSecond.setText("");
            }else if (mPassword.length() == 3) {
                tvThird.setText("");
            }else if (mPassword.length() == 4) {
                tvForth.setText("");
            }else if (mPassword.length() == 5) {
                tvFifth.setText("");
            }else if (mPassword.length() == 6) {
                tvSixth.setText("");
            }
            mPassword.deleteCharAt(mPassword.length() - 1);
        }
    }

    /**
     * 返回输入的内容
     * @return 返回输入内容
     */
    public String getText() {
        return (mPassword == null) ? null : mPassword.toString();
    }

    /**
     * 当密码输入完成时的回调接口
     */
    public interface OnInputFinishedListener {
        void onInputFinished(String password);
    }

    /**
     * 对外开放的方法
     * @param onInputFinishedListener
     */
    public void setOnInputFinishedListener(OnInputFinishedListener onInputFinishedListener) {
        this.onInputFinishedListener = onInputFinishedListener;
    }
}

view_pay_edit

实现效果

为了方便看效果,临时将根布局 layout_height 换为 wrap_content ,不必关注~

Androidstudio记住密码功能_Androidstudio记住密码功能_03

整体布局方式采用 LinearLayout 横向权重 + shape 实现

<?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"
              android:background="@drawable/shape_pay_edit"
              android:orientation="horizontal">

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

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

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

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

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

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

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

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

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

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

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

</LinearLayout>

shape_pay_edit(输出框背景圆角化

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

自定义 数字键盘

现在补充一下实现逻辑

  • 因为该数字键盘样式相对简单,这里采用GridView(网格布局),可以快速实现三等分效果
  • 采用列表布局时(网格布局也属于列表,只不过后来都用RecyvlerView,然后设置对应Manger),都需要写适配器(Adapter),在Adapter中通过itemType来决定是加载数字按钮样式,还是删除按钮样式
  • 针对用户点击不同按钮的行为,为数字键盘加入监听,内部通过监听GridView回调事件向外部抛出一个监听回调

KeyBoard (数字键盘)

package com.example.yongliu.payview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.RelativeLayout;
import android.widget.TextView;

/**
 * author  yongliu
 * date  2018/2/28.
 * desc: 自定义数字键盘
 */

public class Keyboard extends RelativeLayout {
    private Context context;
    private GridView gvKeyboard;

    private String[] key;
    private OnClickKeyboardListener onClickKeyboardListener;

	//因为数据源是在外部通过Api(setKeyboardKeys)动态传入,故无需在构造参数中写死
    public Keyboard(Context context) {
        this(context, null);
    }

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

    public Keyboard(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
    }

    /**
     * 初始化KeyboardView
     */
    private void initKeyboardView() {
        View view = View.inflate(context, R.layout.view_keyboard, this);
        gvKeyboard = (GridView) view.findViewById(R.id.gv_keyboard);
        gvKeyboard.setAdapter(keyboardAdapter);
        initEvent();
    }
    
    /**
     * 初始化键盘的点击事件
     */
    private void initEvent() {
        gvKeyboard.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (onClickKeyboardListener != null && position >= 0) {
                    onClickKeyboardListener.onKeyClick(position, key[position]);
                }
            }
        });
    }

    public interface OnClickKeyboardListener {
        void onKeyClick(int position, String value);
    }

    /**
     * 对外开放的方法
     *
     * @param onClickKeyboardListener
     */
    public void setOnClickKeyboardListener(OnClickKeyboardListener onClickKeyboardListener) {
        this.onClickKeyboardListener = onClickKeyboardListener;
    }

    /**
     * 设置键盘所显示的内容
     *
     * @param key
     */
    public void setKeyboardKeys(String[] key) {
        this.key = key;
        initKeyboardView();
    }

    private BaseAdapter keyboardAdapter = new BaseAdapter() {
        private static final int KEY_NINE = 9;

        @Override
        public int getCount() {
            return key.length;
        }

        @Override
        public Object getItem(int position) {
            return key[position];
        }

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

        @Override
        public int getViewTypeCount() {
            return 2;
        }

        @Override
        public int getItemViewType(int position) {
            return (getItemId(position) == KEY_NINE) ? 2 : 1;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder = null;
            if (convertView == null) {
                if (getItemViewType(position) == 1) {
                    //数字键
                    convertView = LayoutInflater.from(context).inflate(R.layout.item_grid_keyboard, parent, false);
                    viewHolder = new ViewHolder(convertView);
                } else {
                    //删除键
                    convertView = LayoutInflater.from(context).inflate(R.layout.item_grid_keyboard_delete, parent, false);
                }
            }

            if (getItemViewType(position) == 1) {
                viewHolder = (ViewHolder) convertView.getTag();
                viewHolder.tvKey.setText(key[position]);
            }
            return convertView;
        }
    };

    /**
     * ViewHolder,view缓存
     */
    static class ViewHolder {
        private TextView tvKey;
        public ViewHolder(View view) {
            tvKey = (TextView) view.findViewById(R.id.tv_keyboard_keys);
            view.setTag(this);
        }
    }
}

view_keyboard数字键盘

<?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"
    android:orientation="vertical">

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

    <GridView
        android:id="@+id/gv_keyboard"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:horizontalSpacing="1px"
        android:numColumns="3"
        android:background="#999999"
        android:verticalSpacing="1px" />

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

</LinearLayout>

item_grid_keyboard键盘中数字按钮、完成按钮样式

<?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">

    <TextView
        android:id="@+id/tv_keyboard_keys"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/selector_keyboard_key_bg"
        android:gravity="center"
        android:padding="10dp"
        android:textSize="25sp"/>

</LinearLayout>

item_grid_keyboard_delete键盘中删除按钮样式

<?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"
              android:clipChildren="false"
              android:clipToPadding="false">

    <LinearLayout
        android:layout_width="120dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_height="52dp">

        <ImageView
            android:layout_width="26dp"
            android:layout_height="26dp"
            android:scaleType="fitXY"
            android:src="@mipmap/ic_delete"/>

    </LinearLayout>

</LinearLayout>

selector_keyboard_key_bg点击数字键盘时的按钮状态:分为默认状态和点击状态(提升用户体验)

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

    </item>
    <item android:state_pressed="true" >
        <shape android:shape="rectangle">
            <solid android:color="#e5e5e5"/>
        </shape>
    </item>

</selector>

场景使用

MainActivity

package com.example.yongliu.payview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private static final String[] KEY = new String[]{
            "1", "2", "3",
            "4", "5", "6",
            "7", "8", "9",
            "<<", "0", "完成"
    };

    private PayEditText payEditText;
    private Keyboard keyboard;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        setSubView();
        initEvent();
    }

    private void initView() {
        setContentView(R.layout.activity_main);
        payEditText = (PayEditText) findViewById(R.id.PayEditText_pay);
        keyboard = (Keyboard) findViewById(R.id.KeyboardView_pay);
    }

    private void setSubView() {
        //设置键盘
        keyboard.setKeyboardKeys(KEY);
    }

    private void initEvent() {
        keyboard.setOnClickKeyboardListener(new Keyboard.OnClickKeyboardListener() {
            @Override
            public void onKeyClick(int position, String value) {
                if (position < 11 && position != 9) {
                    payEditText.add(value);
                } else if (position == 9) {
                    payEditText.remove();
                } else if (position == 11) {
                    //当点击完成的时候,也可以通过payEditText.getText()获取密码,此时不应该注册OnInputFinishedListener接口
                    Toast.makeText(getApplication(), "您的密码是:" + payEditText.getText(), Toast.LENGTH_SHORT).show();
                    finish();
                }
            }
        });

        /**
         * 当密码输入完成时的回调
         */
        payEditText.setOnInputFinishedListener(new PayEditText.OnInputFinishedListener() {
            @Override
            public void onInputFinished(String password) {
                Toast.makeText(getApplication(), "您的密码是:" + password, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

activity_main

<?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">

    <com.example.yongliu.payview.PayEditText
        android:id="@+id/PayEditText_pay"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_alignParentTop="true"
        android:layout_marginTop="20dp"
        android:paddingLeft="12dp"
        android:paddingRight="12dp" />

    <com.example.yongliu.payview.Keyboard
        android:id="@+id/KeyboardView_pay"
        android:layout_width="match_parent"
        android:layout_height="215dp"
        android:layout_alignParentBottom="true" />

</RelativeLayout>