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) {
public PinEntryEditText(Context context, AttributeSet attrs) {
public PinEntryEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
protected void onAttachedToWindow() {
super.onAttachedToWindow();
accessibilityNodeInfo = createAccessibilityNodeInfo();
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);
setTextIsSelectable(false);
setTypeface(Typeface.DEFAULT_BOLD);
setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
setKeyListener(new DigitsKeyListener());
setFilters(new InputFilter[]{new InputFilter.LengthFilter(mNumChars)});
mNumChars = attrs.getAttributeIntValue(android.R.attr.maxLength, mNumChars);
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);
if (mBackgroundStyle == 2) {
mLinesPaint.setStyle(Paint.Style.FILL);
mLinesPaint.setStyle(Paint.Style.STROKE);
getPaint().setFakeBoldText(true);
super.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
public void onDestroyActionMode(ActionMode mode) {
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// When tapped, move cursor to end of text.
super.setOnClickListener(v -> {
Editable text = getText();
setSelection(text.length());
if (mClickListener != null) {
mClickListener.onClick(v);
private boolean isPassword() {
return accessibilityNodeInfo != null && accessibilityNodeInfo.isPassword();
public void setOnClickListener(OnClickListener l) {
public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
throw new RuntimeException("setCustomSelectionActionModeCallback() not supported.");
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);
protected void onDraw(Canvas canvas) {
int availableWidth = getWidth() - getPaddingRight() - getPaddingLeft();
float space = mSpace * (mNumChars - 1);
float charWidth = mCharSize * mNumChars;
if (availableWidth < charWidth) {
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);
canvas.drawRoundRect(startX, top, startX + mCharSize, bottom, mRadius, mRadius, mLinesPaint);
boolean isPassword = isPassword();
textWidth = textPaint.measureText("•");
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);
startX += (int) (mCharSize * 2);
startX += (int) (mCharSize + mSpace);
* @param next Is the current char the next character to be input?
private void updateColorForLines(boolean next) {
mLinesPaint.setStrokeWidth(mLineStrokeSelected);
if (mBackgroundStyle == 2) {
mLinesPaint.setColor(Color.parseColor("#ffF4F6F8"));
mLinesPaint.setColor(Color.parseColor("#666666"));
mLinesPaint.setColor(Color.parseColor("#008CFF"));
mLinesPaint.setStrokeWidth(mLineStroke);
if (mBackgroundStyle == 2) {
mLinesPaint.setColor(Color.parseColor("#ffF4F6F8"));
mLinesPaint.setColor(Color.parseColor("#666666"));