Android验证码输入框支持粘贴

验证码输入框,满足剪切板内容自动填充,看效果
Android验证码输入框支持粘贴_第1张图片
原本做法是6个EditText,后来发现,这样写最大问题是,无法满足粘贴功能,验证码短信 一般都带“复制”,点击 短信通知栏 的“复制”后,6位验证码会自动显示在软键盘左上角,点击一下即完成填充。

如果牺牲掉了验证码“通知栏短信-复制-点击填充”功能 ,用户必须一次性记住6位,逐个输入;若是用户习惯性点击了复制后,发现app竟然无法填充,自己也没记验证码,再次下拉看通知栏看短信时,发现通知栏短信也没了,就必须要回到短信收件箱里查找,这种用户体验,WTF,狠操蛋!!!一定要规避这种打破用户操作习惯,引起用户不爽的细节。

先聊聊思路:
1.首先想到 写一个EditText,然后setBackground()为6个框,字间距刚好让每个数字处于框中间;然而字间距的方法没找到合适的,全部是按比例分间距的,累觉不爱,适配是个巨坑,前路艰险,性价比太低,放弃之。
2.我打开滴滴,美团,结果大厂的复制粘贴各种花式bug啪啪打脸,就不一一拉出来细评了。
3.功夫不负有心人,终于找到一个支持粘贴的app—建设银行,尽管被我测出了bug,也给我启发,让我看出了端倪。
Android验证码输入框支持粘贴_第2张图片

看到上图我猜想,蓝色水滴中间才是全部编辑框字体,于是我剪切,结果正如我所料,框内字体被清除了,并且无论我如何点击,双击,长按最后一个框,光标始终在第二个框里跳动。
于是,我有思路了,所有的框就是TextView,而真正的编辑框内容是透明的。为了点击最后一个框也能唤起软键盘,需要让EditText的宽度与六个框一样宽;

建行app 有个缺点就是 光标可以随着手势左右滑动游走,怎么避免呢,

  • 第一道防线:EditText.setTextSize(0.01f),即便不小心弹出选中操作框,字体足够小,小到可以避免光标随手势左右游走,精度层避免该现象,当然这只是补救措施,根本杜绝的话,需要避免选中操作框弹出;
  • 第二道防线:屏蔽长按事件避免出现“剪切,复制,粘贴”的那个系统选项框;
  • 第三道防线:屏蔽双击事件,经测小米、华为等手机,双击和长按都会弹出“剪切,复制,粘贴”系统选项框,宜将剩勇追穷寇,务必赶尽杀绝。

实现功能:
1.点短信复制后,支持剪切板自动填充,即粘贴;
2.屏蔽长按粘贴,和双击选中;
3.输入完成回调;
4.根据屏幕宽度和左右间距 自动适配 输入方框大小

明显缺点
此种情况下,无法显示光标,暂时没有想到简单易行的解决办法,如有思路,求评论区赐教。

上代码吧:

/**
 * Created by @author iblade.Wang on 2019/4/4.
 * 验证码输入框
 * EditText字号极小,且颜色透明
 */

public class VerCodeInputView extends FrameLayout {
        /**
     * 输入框个数
     */
    private int inputNum;
    /**
     * 输入框宽度
     */
    private int inputWidth;
    private int inputHeight;
    /**
     * 输入框之间的间隔
     */
    private int childPadding;
    /**
     * 输入框背景
     */
    private int editTextBg;
    /**
     * 文本颜色
     */
    private int textColor;
    /**
     * 文本字体大小
     */
    private int textSize;
    /**
     * 输入类型
     */
    private int inputType;


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

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

    public VerCodeInputView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.VerCodeInputView, defStyleAttr, 0);
        inputNum = ta.getInteger(R.styleable.VerCodeInputView_inputNum, 6);
        inputWidth = ta.getDimensionPixelSize(R.styleable.VerCodeInputView_inputWidth, DensityUtil.dip2px(context, 43));
        inputHeight = inputWidth;
        childPadding = ta.getDimensionPixelSize(R.styleable.VerCodeInputView_inputPadding, DensityUtil.dip2px(context, 7.5f));
        textColor = ta.getColor(R.styleable.VerCodeInputView_inputTxtColor, Color.parseColor("#333333"));
        textSize = ta.getDimensionPixelSize(R.styleable.VerCodeInputView_inputTxtSize, 24);
        editTextBg = ta.getResourceId(R.styleable.VerCodeInputView_inputBg, R.drawable.selector_bg_edit);
        inputType = ta.getInt(R.styleable.VerCodeInputView_inputType, InputType.TYPE_CLASS_NUMBER);
        ta.recycle();
        textViewList = new ArrayList<>(inputNum);
        initViews();
    }

    private List<TextView> textViewList;
    private EditText editText;

    private void initViews() {
        textViewList.clear();
        //textViewList = new ArrayList<>(inputNum);
        LinearLayout llTextViewRoot = new LinearLayout(getContext());
        LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        llTextViewRoot.setLayoutParams(layoutParams);
        llTextViewRoot.setOrientation(LinearLayout.HORIZONTAL);
        addView(llTextViewRoot);
        for (int i = 0; i < inputNum; i++) {
            TextView textView = new TextView(getContext());
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(inputWidth, inputHeight);
            if (i != inputNum - 1) {//最后一个textView 不设置margin
                params.rightMargin = childPadding;
            }
            params.gravity = Gravity.CENTER;
            textView.setLayoutParams(params);
            textView.setTextColor(textColor);
            textView.setTextSize(textSize);
            textView.setGravity(Gravity.CENTER);
            textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1)});
            textView.setInputType(inputType);
            textView.setBackgroundResource(editTextBg);
            if (i == 0) textView.setSelected(true);//默认首个方框选中
            textView.setId(i);
            llTextViewRoot.addView(textView);
            textViewList.add(textView);
        }
        editText = new EditText(getContext());
        LayoutParams layoutParam2 = new LayoutParams(LayoutParams.MATCH_PARENT, inputHeight);
        editText.setLayoutParams(layoutParam2);
        editText.setTextSize(0.01f);
        //设置透明光标,如果直接不显示光标的话,长按粘贴会没效果
        try {
            Field f = TextView.class.getDeclaredField("mCursorDrawableRes");
            f.setAccessible(true);
            f.set(editText, R.drawable.edit_cursor_bg_transparent);
        } catch (Exception e) {
            e.printStackTrace();
        }
        editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(inputNum)});
        editText.setInputType(inputType);
        editText.setTextColor(ContextCompat.getColor(getContext(), R.color.transparent));
        editText.setBackground(null);
        editText.addTextChangedListener(textWatcher);
        addView(editText);
        initListener();
    }

    private void initListener() {
        //屏蔽双击: 好多手机双击会出现 选择 剪切 粘贴 的选项卡,
        new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                return true;
            }
        });
    }

    private TextWatcher textWatcher = 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 editable) {
            String inputContent = (null == editText.getText()) ? "" : editText.getText().toString();
            //已经有输入时,屏蔽长按和光标
            if (inputContent.length() > 0) {
                editText.setLongClickable(false);
                editText.setCursorVisible(false);
            } else {
                editText.setLongClickable(true);
                editText.setCursorVisible(true);
            }
            if (listener != null && inputContent.length() >= inputNum) {
                listener.onComplete(inputContent);
            }
            for (int i = 0, len = textViewList.size(); i < len; i++) {
                TextView textView = textViewList.get(i);
                textView.setSelected(false);
                if (i < inputContent.length()) {
                    textView.setText(String.valueOf(inputContent.charAt(i)));
                } else {
                    textView.setText("");
                    //选中待输入的textView
                    if (i == inputContent.length()) {
                        textView.setSelected(true);
                    }
                }
            }
        }
    };

    private boolean isAuto = false;

    /**
     * 设置宽高自适应,单个框的宽度平分父布局总宽度
     */
    public void setAutoWidth() {
        isAuto = true;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMeasuredWidth();
        if (isAuto && width > 0) {
            isAuto = false;
            //resetWH(width);
            resetMargin(width);
        }
    }

    /*    private void resetWH(int w) {
            int paddings = childPadding * (inputNum - 1);
            inputWidth = (w - paddings) / (inputNum);
            inputHeight = inputWidth;
            for (int i = 0, len = textViewList.size(); i < len; i++) {
                View child = textViewList.get(i);
                child.getLayoutParams().height = inputHeight;
                child.getLayoutParams().width = inputWidth;
            }
            editText.getLayoutParams().height = inputHeight;
        }*/
    private void resetMargin(int width) {
        if (width > 0) {
            int remainWidth = width - (inputNum * inputWidth);
            if (remainWidth > 0 && inputNum > 1) {
                childPadding = remainWidth / (inputNum - 1);
                for (int i = 0, len = textViewList.size(); i < len; i++) {
                    View child = textViewList.get(i);
                    LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) child.getLayoutParams();
                    if (i != inputNum - 1) {//最后一个textView 不设置margin
                        params.rightMargin = childPadding;
                    }
                    params.gravity = Gravity.CENTER;
                    child.setLayoutParams(params);
                    child.getLayoutParams().height = inputHeight;
                    child.getLayoutParams().width = inputWidth;
                }
                editText.getLayoutParams().height = inputHeight;
            }
        }
    }

    /**
     * 获取编辑框内容
     *
     * @return 编辑框内容
     */
    public String getEditContent() {
        return editText.getText().toString();
    }

    public OnCompleteListener listener;

    public void setOnCompleteListener(OnCompleteListener listener) {
        this.listener = listener;
    }

    public interface OnCompleteListener {
        /**
         * 完成验证码的填写
         *
         * @param content 填写内容
         */
        void onComplete(String content);
    }
}

如何调用:

/**
 * @author YlWang
 */
public class MainActivity extends AppCompatActivity {
    private VerCodeInputView codeInputCard;


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


    private void initView() {
        codeInputCard = findViewById(R.id.edit);
        codeInputCard.setAutoWidth();
        codeInputCard.setOnCompleteListener(new VerCodeInputView.OnCompleteListener() {
            @Override
            public void onComplete(String content) {
                Toast.makeText(MainActivity.this, "您输入了:" + content, Toast.LENGTH_LONG).show();
            }
        });

    }
}

另外,框框背景drawable目录下 bg_edit_vercode.xml


<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/color_ffffff" />
            <stroke android:width="@dimen/dp_05" android:color="@color/color_00b38a" />
        shape>
    item>
    <item android:state_selected="false">
        <shape android:shape="rectangle">
            <solid android:color="@color/color_ffffff" />
            <stroke android:width="@dimen/dp_05" android:color="@color/color_dfdfdf" />
        shape>
    item>
selector>

------------------------------2019.4.10更新-----------------------

经测试Vivo,华为部分机型 不会把复制短信的内容呈现在软键盘上方,复制完之后,想粘贴有两种办法:①需要资深玩家在软键盘里找到粘贴按键(缺点:操作麻烦),②大家习惯的长按出现粘贴;

然鹅。。。上面代码又把长按屏蔽了。哎~心塞塞!!!
继续优化:

我们期待长按后出现这种:
Android验证码输入框支持粘贴_第3张图片
可是一旦不屏蔽长按,恶魔放出了瓶子;
例如:
Android验证码输入框支持粘贴_第4张图片
有坑警告:为了满足长按出现粘贴,删除editText.setLongClickable(false);,长按了好久了,不出现粘贴;同步代码,Clean,Rebuild,各种大动作,仍然长按无效;
抱着没啥希望的心态 加上editText.setLongClickable(true)再试一把,果然没希望,还是长按无效。
Android验证码输入框支持粘贴_第5张图片
WHY?

经测删除editText.setCursorVisible(false);再试,长按终于出粘贴了。

结论:设置光标不可见时,长按将会无效。

为了满足长按可用,只好设置光标可见,大不了颜色设成透明的(效果上等同于设置不可见)。

上图蓝色圆球和一大串操作栏好丑,坚决不能出现,那就不得不动态设置了:
例如不输入内容时,不屏蔽长按;一旦有了输入内容后,屏蔽长按,设置光标不可见,这样就不会出现了,所以修改后代码是这样的。

    private void initViews() {
        textViewList = new ArrayList<>(inputNum);
        LinearLayout llTextViewRoot = new LinearLayout(getContext());
        LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        llTextViewRoot.setLayoutParams(layoutParams);
        llTextViewRoot.setOrientation(LinearLayout.HORIZONTAL);
        addView(llTextViewRoot);
        for (int i = 0; i < inputNum; i++) {
            TextView textView = new TextView(getContext());
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(inputWidth, inputHeight);
            if (i != inputNum - 1) {//最后一个textView 不设置margin
                params.rightMargin = childPadding;
            }
            params.gravity = Gravity.CENTER;
            textView.setLayoutParams(params);
            textView.setTextColor(textColor);
            textView.setTextSize(textSize);
            textView.setGravity(Gravity.CENTER);
            textView.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1)});
            textView.setInputType(inputType);
            textView.setBackgroundResource(editTextBg);
            textView.setId(i);
            llTextViewRoot.addView(textView);
            textViewList.add(textView);
        }
        editText = new EditText(getContext());
        LayoutParams layoutParam2 = new LayoutParams(LayoutParams.MATCH_PARENT, inputHeight);
        editText.setLayoutParams(layoutParam2);
        editText.setTextSize(0.01f);
        //设置透明光标,如果直接不显示光标的话,长按粘贴会没效果
        try {
            Field f = TextView.class.getDeclaredField("mCursorDrawableRes");
            f.setAccessible(true);
            f.set(editText, R.drawable.edit_cursor_bg_transparent);
        } catch (Exception e) {
            e.printStackTrace();
        }
        editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(inputNum)});
        editText.setInputType(inputType);
        editText.setTextColor(ContextCompat.getColor(getContext(), R.color.transparent));
        editText.setBackground(null);
        editText.addTextChangedListener(textWatcher);
        addView(editText);
        initListener();
    }
 
     @Override
     public void afterTextChanged(Editable editable) {
         String inputContent = (null == editText.getText()) ? "" : editText.getText().toString();
         //已经有输入时,屏蔽长按和光标
         if (inputContent.length() > 0) {
             editText.setLongClickable(false);
             editText.setCursorVisible(false);
         } else {
             editText.setLongClickable(true);
             editText.setCursorVisible(true);
         }
         if (listener != null && inputContent.length() >= inputNum) {
             listener.onComplete(inputContent);
         }
         for (int i = 0, len = textViewList.size(); i < len; i++) {
             TextView textView = textViewList.get(i);
             if (i < inputContent.length()) {
                 textView.setText(String.valueOf(inputContent.charAt(i)));
             } else {
                 textView.setText("");
             }
         }
     }

其中:edit_cursor_bg_transparent.xml


<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <size android:width="0.01dp" />

    <solid android:color="@android:color/transparent" />
shape>

2020.04.26更新,加上了 待输入方框的选中色。效果如下:
Android验证码输入框支持粘贴_第6张图片

最近发现有同行也做了类似需求:https://www.jianshu.com/p/3238a5afc21c

你可能感兴趣的:(Android)