skip to content
Posts · February 2024

Android PinEntryEditText


使用

<com.yeahka.widget.safekeyboard.PinEntryEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:textSize="20sp"
app:backgroundStyle="outline"
app:charSize="40dp" />
<com.yeahka.widget.safekeyboard.PinEntryEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberPassword"
app:backgroundStyle="fill"
app:charSpace="15dp"
app:charSize="45dp" />
<com.yeahka.widget.safekeyboard.PinEntryEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
app:backgroundStyle="underline"
app:charSize="40dp" />

源码

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextPaint;
import android.text.method.DigitsKeyListener;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
public class PinEntryEditText extends AppCompatEditText {
private float mCharSize = 40;
private float mSpace = 10;
private float mRadius = 8;
private int mNumChars = 6;
private int mBackgroundStyle = 1;
private float mLineStroke = 0.5f;
private float mLineStrokeSelected = 0.5f;
private AccessibilityNodeInfo accessibilityNodeInfo;
private OnClickListener mClickListener;
private Paint mLinesPaint;
public PinEntryEditText(Context context) {
super(context);
init(context, null);
}
public PinEntryEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public PinEntryEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
accessibilityNodeInfo = createAccessibilityNodeInfo();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
accessibilityNodeInfo = null;
}
private void init(Context context, @Nullable AttributeSet attrs) {
float multi = context.getResources().getDisplayMetrics().density;
mCharSize = multi * mCharSize; //convert to pixels for our density
mSpace = multi * mSpace; //convert to pixels for our density
mRadius = multi * mRadius;
mLineStroke = multi * mLineStroke;
mLineStrokeSelected = multi * mLineStrokeSelected;
mLinesPaint = new Paint(getPaint());
mLinesPaint.setStrokeWidth(mLineStroke);
setBackgroundResource(0);
setCursorVisible(false);
setTextIsSelectable(false);
setHint("");
setTypeface(Typeface.DEFAULT_BOLD);
if (attrs == null) {
setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
setKeyListener(new DigitsKeyListener());
setFilters(new InputFilter[]{new InputFilter.LengthFilter(mNumChars)});
} else {
mNumChars = attrs.getAttributeIntValue(android.R.attr.maxLength, mNumChars);
//noinspection resource
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PinEntryEditText);
mBackgroundStyle = typedArray.getInt(R.styleable.PinEntryEditText_backgroundStyle, mBackgroundStyle);
mCharSize = typedArray.getDimensionPixelSize(R.styleable.PinEntryEditText_charSize, (int) mCharSize);
mSpace = typedArray.getDimensionPixelSize(R.styleable.PinEntryEditText_charSpace, (int) mSpace);
typedArray.recycle();
}
if (mBackgroundStyle == 2) {
mLinesPaint.setStyle(Paint.Style.FILL);
} else {
mLinesPaint.setStyle(Paint.Style.STROKE);
}
if (isPassword()) {
getPaint().setFakeBoldText(true);
}
//Disable copy paste
super.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
public void onDestroyActionMode(ActionMode mode) {
}
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
});
// When tapped, move cursor to end of text.
super.setOnClickListener(v -> {
Editable text = getText();
if (text != null) {
setSelection(text.length());
}
if (mClickListener != null) {
mClickListener.onClick(v);
}
});
}
private boolean isPassword() {
return accessibilityNodeInfo != null && accessibilityNodeInfo.isPassword();
}
@Override
public void setOnClickListener(OnClickListener l) {
mClickListener = l;
}
@Override
public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
throw new RuntimeException("setCustomSelectionActionModeCallback() not supported.");
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = (int) mCharSize + getPaddingTop() + getPaddingBottom();
System.out.println("onMeasure: " + width + "," + height);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
int availableWidth = getWidth() - getPaddingRight() - getPaddingLeft();
float space = mSpace * (mNumChars - 1);
float charWidth = mCharSize * mNumChars;
if (availableWidth < charWidth) {
return;
}
if (availableWidth - charWidth < space) {
space = availableWidth - charWidth;
mSpace = space / (mNumChars - 1);
}
float leftOffset = (availableWidth - charWidth - space) / 2;
int startX = (int) (getPaddingLeft() + leftOffset);
int bottom = getHeight() - getPaddingBottom();
int top = getPaddingTop();
Editable text = getText();
if (text == null) return;
int textLength = text.length();
TextPaint textPaint = getPaint();
for (int i = 0; i < mNumChars; i++) {
updateColorForLines(i == textLength);
if (mBackgroundStyle == 0) {
canvas.drawLine(startX, bottom, startX + mCharSize, bottom, mLinesPaint);
} else if (mBackgroundStyle == 1) {
canvas.drawRoundRect(startX, top, startX + mCharSize, bottom, mRadius, mRadius, mLinesPaint);
} else {
canvas.drawRoundRect(startX, top, startX + mCharSize, bottom, mRadius, mRadius, mLinesPaint);
}
if (textLength > i) {
float textWidth;
boolean isPassword = isPassword();
if (isPassword) {
textWidth = textPaint.measureText("•");
} else {
textWidth = textPaint.measureText(text.charAt(i) + "");
}
Paint.FontMetrics metrics = textPaint.getFontMetrics();
float textHeight = metrics.bottom - metrics.top;
float middle = startX + mCharSize / 2;
float x = middle - textWidth / 2;
float y = bottom - mCharSize / 2 + textHeight / 2 - metrics.descent;
String ch = isPassword ? "•" : Character.toString(text.charAt(i));
canvas.drawText(ch, x, y, textPaint);
}
if (mSpace < 0) {
startX += (int) (mCharSize * 2);
} else {
startX += (int) (mCharSize + mSpace);
}
}
}
/**
* @param next Is the current char the next character to be input?
*/
private void updateColorForLines(boolean next) {
if (isFocused()) {
mLinesPaint.setStrokeWidth(mLineStrokeSelected);
if (mBackgroundStyle == 2) {
mLinesPaint.setColor(Color.parseColor("#ffF4F6F8"));
} else {
mLinesPaint.setColor(Color.parseColor("#666666"));
if (next) {
mLinesPaint.setColor(Color.parseColor("#008CFF"));
}
}
} else {
mLinesPaint.setStrokeWidth(mLineStroke);
if (mBackgroundStyle == 2) {
mLinesPaint.setColor(Color.parseColor("#ffF4F6F8"));
} else {
mLinesPaint.setColor(Color.parseColor("#666666"));
}
}
}
}
<declare-styleable name="PinEntryEditText">
<attr name="charSpace" format="dimension" />
<attr name="charSize" format="dimension" />
<attr name="backgroundStyle">
<enum name="underline" value="0" />
<enum name="outline" value="1" />
<enum name="fill" value="2" />
</attr>
</declare-styleable>

弹出系统键盘

// 部分Android 10 无效
WindowInsetsControllerCompat(window, it).show(WindowInsetsCompat.Type.ime())