0%

六位密码输入框


又是很长一段时间没有更新blog,最近疫情又严重了,公司开启了WFH模式,闲暇之余有更多的时间来做技术总结和沉淀,最近在项目中遇到一个需求,绘制一个六位的密码输入框,需要用到自定义View的相关知识,中间还出了一点小插曲,在某些机型引发了一个bug在此做一个记录,欢迎交流和讨论~


最终效果图:
效果图

自定义LinearLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PassWordView extends LinearLayout {

public PassWordView(@NonNull Context context) {
super(context);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init(){

}
}

添加EditText和TextView

  • 在LinearLayout中添加1个EditText在底部,EditText的输入格式只能是数字或密码且输入长度为6,当前LinearLayout点击后触发EditText获取焦点并弹出软键盘
  • 监听输入过程把输入的文本替换为”●”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    private int defaultSize = 6;
    private EditText editText;

    // 清空当前输入的密码
    public void clearPassWord() {
    editText.setText(null);
    }

    @SuppressLint("CheckResult")
    private void init() {
    editText = new EditText(getContext());
    editText.setTextSize(12f);
    editText.setLayoutParams(new FrameLayout.LayoutParams(1, ViewGroup.LayoutParams.WRAP_CONTENT));
    editText.setCursorVisible(false);
    editText.setBackgroundDrawable(null);
    editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
    addView(editText);
    editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(defaultSize)});
    setOnClickListener(v -> {
    if (editText.requestFocus()) {
    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    if (imm != null) {
    imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
    }
    }
    });

    editText.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) {
    TextView childAt = (TextView) getChildAt(start + 1); // 获取当前聚焦的TextView
    if (before == 0) {
    childAt.setText("●");
    } else {
    childAt.setText("");
    }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
    });
    }

    // 获取当前密码
    public String getPwd() {
    return editText.getText().toString();
    }


    public void setFocusShape(boolean showFocusShape) {
    this.showFocusShape = showFocusShape;
    }

    添加TextView

  • 添加6个正方形的TextView在顶部,来实现我们的六位密码框输入布局,自定义TextView在onMeasure中设置宽等于高
  • 定义单个密码框之间间隔为6,第一个密码框marginStart为0,marginEnd为5/6 * space,第二个密码框marginStart为1/6 * space,marginEnd为4/6 * space,第三个密码框marginStart为2/6 * space,marginEnd为3/6 * space,前一个密码框的marginEnd + marginStart = space,所以marginStart = space * i / defaultSize,marginEnd = space * (defaultSize - 1 - i) / defaultSize
  • 设置当前输入的密码框聚焦背景

    小插曲

    这里最开始的实现思路有问题,把TextView的weight设置为并动态添加,对当前LinearLayout的onMeasure方法做了处理,获取每个TextView的宽度再设置其高度导致应用到底部弹窗布局时,小米机型上由于TextView的高度做了调整比调整前的高度更高,使得软件盘顶上去的高度不够,下面的文本被遮挡。但是在华为等机型上又表现正常,可能跟Rom的处理有关(这些机型上又自动刷新了一次)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    private boolean showFocusShape = true; // 是否设置聚焦背景
    @SuppressLint("CheckResult")
    private void init(){
    ...
    double space = SizeUtils.dp2px(6);
    for (int i = 0; i < defaultSize; i++) {
    TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.single_pwd_editext, null);
    LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
    layoutParams.setMarginStart((int) (space * i / defaultSize));
    layoutParams.setMarginEnd((int) (space * (defaultSize - i - 1) / defaultSize));
    addView(textView, layoutParams);
    }
    editText.setOnFocusChangeListener((v, hasFocus) -> {
    if (!showFocusShape) {
    return;
    }
    for (int i = 1; i < getChildCount(); i++) {
    getChildAt(i).setBackgroundResource(hasFocus ? R.drawable.shape_editext_border_green_2r : R.drawable.shape_edittext_border_background_with_error);
    }
    });
    ...
    }

    public static class SquareTextView extends AppCompatTextView {

    public SquareTextView(Context context) {
    super(context);
    }

    public SquareTextView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    if (widthMode == MeasureSpec.UNSPECIFIED) {
    throw new RuntimeException("mode is UNSPECIFIED");
    } else {
    setMeasuredDimension(widthSize, widthSize);
    }
    }
    }
  • shape_editext_border_green_2r.xml
    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/emphasis4" />
    <stroke
    android:width="@dimen/line_width"
    android:color="@color/primary" />
    <corners android:radius="2dp" />
    </shape>
  • shape_edittext_border_background_with_error.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_activated="true">
    <shape>
    <solid android:color="@color/emphasis4" />
    <stroke android:width="@dimen/line_width" android:color="@color/secondary" />
    <corners android:radius="6dp" />
    </shape>
    </item>
    <item android:state_activated="false">
    <shape>
    <solid android:color="@color/emphasis4" />
    <corners android:radius="6dp" />
    </shape>
    </item>
    </selector>
  • single_pwd_editext中引用自定义TextView
    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="utf-8"?>
    <view class="com.resources.widget.PassWordView$SquareTextView" xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/shape_edittext_border_background_with_error"
    android:gravity="center"
    android:textColor="@color/c_text60" />

    定义回调返回密码输入完成的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    private PasswordCallback passwordCallback;
    interface PassWordCallback {
    void complete(String password)
    }

    public void setPasswordCallback(PasswordCallback passwordCallback) {
    this.passwordCallback = passwordCallback;
    }

    @SuppressLint("CheckResult")
    private void init(){
    ...
    editText.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) {
    TextView childAt = (TextView) getChildAt(start + 1);
    if (before == 0) { // 新增情况下
    childAt.setText("●");
    } else {
    childAt.setText("");
    }
    if (!TextUtils.isEmpty(s) && s.length() == defaultSize) {
    if (passwordCallback != null) {
    try {
    passwordCallback.complete(s.toString());
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
    });
    ...
    }

最后附上完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public class PassWordView extends LinearLayout {

private int defaultSize = 6;
private PasswordCallback passwordCallback;
interface PassWordCallback {
void complete(String password)
}

public void setPasswordCallback(PasswordCallback passwordCallback) {
this.passwordCallback = passwordCallback;
}
private boolean showFocusShape = true;
private EditText editText;

public PassWordView(@NonNull Context context) {
super(context);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

public void clearPassWord() {
editText.setText(null);
}

@SuppressLint("CheckResult")
private void init() {
editText = new EditText(getContext());
editText.setTextSize(12f);
editText.setLayoutParams(new FrameLayout.LayoutParams(1, ViewGroup.LayoutParams.WRAP_CONTENT));
editText.setCursorVisible(false);
editText.setBackgroundDrawable(null);
editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
addView(editText);
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(defaultSize)});
setOnClickListener(v -> {
if (editText.requestFocus()) {
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
}
});

double space = SizeUtils.dp2px(6);
for (int i = 0; i < defaultSize; i++) {
TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.single_pwd_editext, null);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
layoutParams.setMarginStart((int) (space * i / defaultSize));
layoutParams.setMarginEnd((int) (space * (defaultSize - i - 1) / defaultSize));
addView(textView, layoutParams);
}
editText.setOnFocusChangeListener((v, hasFocus) -> {
if (!showFocusShape) {
return;
}
for (int i = 1; i < getChildCount(); i++) {
getChildAt(i).setBackgroundResource(hasFocus ? R.drawable.shape_editext_border_green_2r : R.drawable.shape_edittext_border_background_with_error);
}
});
editText.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) {
TextView childAt = (TextView) getChildAt(start + 1);
if (before == 0) {
childAt.setText("●");
} else {
childAt.setText("");
}
if (!TextUtils.isEmpty(s) && s.length() == defaultSize) {
if (passwordCallback != null) {
try {
passwordCallback.complete(s.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

@Override
public void afterTextChanged(Editable s) {

}
});
}

public String getPwd() {
return editText.getText().toString();
}


public void setFocusShape(boolean showFocusShape) {
this.showFocusShape = showFocusShape;
}

public static class SquareTextView extends AppCompatTextView {

public SquareTextView(Context context) {
super(context);
}

public SquareTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED) {
throw new RuntimeException("mode is UNSPECIFIED");
} else {
setMeasuredDimension(widthSize, widthSize);
}
}
}
}

在xml中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/c_large"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp">

<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/input_pwd"
android:textColor="@color/c_text"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingBottom="12dp"
android:src="@mipmap/icon_close"
app:tint="@color/emphasis60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<com.resources.widget.PassWordView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv1"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv1"
app:layout_goneMarginTop="20dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/password_view"
android:paddingTop="10dp"
android:paddingBottom="12dp"
android:text="@string/tips_input"
android:textColor="@color/secondary"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_view" />

<TextView
android:id="@+id/tv_forget_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/password_view"
android:paddingTop="10dp"
android:paddingBottom="12dp"
android:text="@string/forget_pwd"
android:textColor="@color/c_text60"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_view" />

</androidx.constraintlayout.widget.ConstraintLayout>