Edit text for OTP with Each letter in separate positions
OtpEditText.java (Custom EditText):
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Editable;
import android.util.AttributeSet;
import android.view.ActionMode;
import android.view.View;
import androidx.appcompat.widget.AppCompatEditText;
public class OtpEditText extends AppCompatEditText {
private float mSpace = 24; //24 dp by default, space between the lines
private float mNumChars = 4;
private float mLineSpacing = 8; //8dp by default, height of the text from our lines
private int mMaxLength = 4;
private float mLineStroke = 2;
private Paint mLinesPaint;
private OnClickListener mClickListener;
public OtpEditText(Context context) {
super(context);
}
public OtpEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public OtpEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
float multi = context.getResources().getDisplayMetrics().density;
mLineStroke = multi * mLineStroke;
mLinesPaint = new Paint(getPaint());
mLinesPaint.setStrokeWidth(mLineStroke);
mLinesPaint.setColor(getResources().getColor(R.color.colorPrimaryDark));
setBackgroundResource(0);
mSpace = multi * mSpace; //convert to pixels for our density
mLineSpacing = multi * mLineSpacing; //convert to pixels for our density
mNumChars = mMaxLength;
super.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// When tapped, move cursor to end of text.
setSelection(getText().length());
if (mClickListener != null) {
mClickListener.onClick(v);
}
}
});
}
@Override
public void setOnClickListener(OnClickListener l) {
mClickListener = l;
}
@Override
public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
throw new RuntimeException("setCustomSelectionActionModeCallback() not supported.");
}
@Override
protected void onDraw(Canvas canvas) {
int availableWidth = getWidth() - getPaddingRight() - getPaddingLeft();
float mCharSize;
if (mSpace < 0) {
mCharSize = (availableWidth / (mNumChars * 2 - 1));
} else {
mCharSize = (availableWidth - (mSpace * (mNumChars - 1))) / mNumChars;
}
int startX = getPaddingLeft();
int bottom = getHeight() - getPaddingBottom();
//Text Width
Editable text = getText();
int textLength = text.length();
float[] textWidths = new float[textLength];
getPaint().getTextWidths(getText(), 0, textLength, textWidths);
for (int i = 0; i < mNumChars; i++) {
canvas.drawLine(startX, bottom, startX + mCharSize, bottom, mLinesPaint);
if (getText().length() > i) {
float middle = startX + mCharSize / 2;
canvas.drawText(text, i, i + 1, middle - textWidths[0] / 2, bottom - mLineSpacing, getPaint());
}
if (mSpace < 0) {
startX += mCharSize * 2;
} else {
startX += mCharSize + mSpace;
}
}
}
}
Use this customised EditText in your XML like below:
<OtpEditText
android:id="@+id/et_otp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:cursorVisible="false"
android:digits="1234567890"
android:inputType="number"
android:maxLength="4"
android:textIsSelectable="false"
android:textSize="20sp"/>
Reference:
Article: https://medium.com/@ali.muzaffar/building-a-pinentryedittext-in-android-5f2eddcae5d3
Sample Code: https://gist.github.com/alphamu/0d3055e0233c5749b8d6
After all of these answers, I didn't find what I wanted as considering the UI/UX, the deletion of element was flawed in such a way that to go back to previous EditText
, current EditText should not be empty.
Here's the solution I've implemented in Kotlin which works for Deletion by the Delete Key, pressed on the keyboard. Also, the delete function is implemented as such that when the current EditText
is empty and Delete key is pressed, it switches back to previous EditText
and delete its element also.
-
Call the functions as such:
//GenericTextWatcher here works only for moving to next EditText when a number is entered //first parameter is the current EditText and second parameter is next EditText editText1.addTextChangedListener(GenericTextWatcher(editText1, editText2)) editText2.addTextChangedListener(GenericTextWatcher(editText2, editText3)) editText3.addTextChangedListener(GenericTextWatcher(editText3, editText4)) editText4.addTextChangedListener(GenericTextWatcher(editText4, null)) //GenericKeyEvent here works for deleting the element and to switch back to previous EditText //first parameter is the current EditText and second parameter is previous EditText editText1.setOnKeyListener(GenericKeyEvent(editText1, null)) editText2.setOnKeyListener(GenericKeyEvent(editText2, editText1)) editText3.setOnKeyListener(GenericKeyEvent(editText3, editText2)) editText4.setOnKeyListener(GenericKeyEvent(editText4,editText3))
-
Now, paste these two classes in your current class
class GenericKeyEvent internal constructor(private val currentView: EditText, private val previousView: EditText?) : View.OnKeyListener{ override fun onKey(p0: View?, keyCode: Int, event: KeyEvent?): Boolean { if(event!!.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DEL && currentView.id != R.id.editText1 && currentView.text.isEmpty()) { //If current is empty then previous EditText's number will also be deleted previousView!!.text = null previousView.requestFocus() return true } return false } } class GenericTextWatcher internal constructor(private val currentView: View, private val nextView: View?) : TextWatcher { override fun afterTextChanged(editable: Editable) { // TODO Auto-generated method stub val text = editable.toString() when (currentView.id) { R.id.editText1 -> if (text.length == 1) nextView!!.requestFocus() R.id.editText2 -> if (text.length == 1) nextView!!.requestFocus() R.id.editText3 -> if (text.length == 1) nextView!!.requestFocus() //You can use EditText4 same as above to hide the keyboard } } override fun beforeTextChanged( arg0: CharSequence, arg1: Int, arg2: Int, arg3: Int ) { // TODO Auto-generated method stub } override fun onTextChanged( arg0: CharSequence, arg1: Int, arg2: Int, arg3: Int ) { // TODO Auto-generated method stub } }
Further, to disable the visible cursor, you can either use android:cursorVisible="false"
in your EditText
tag in the Layout or can use the java function setCursorVisible(false)
.
Edit: I'm using stock widget EditTexts
so if you want to display a box around them, just create a drawable layout and set it as background of EditTexts
and give them a padding of 5dp. This will create a box and will make it look cooler.
You can try this, by making TextWatcher more Generic, so its easy to use and understand
Use below class:
public class GenericTextWatcher implements TextWatcher
{
private View view;
private GenericTextWatcher(View view)
{
this.view = view;
}
@Override
public void afterTextChanged(Editable editable) {
// TODO Auto-generated method stub
String text = editable.toString();
switch(view.getId())
{
case R.id.editText1:
if(text.length()==1)
et2.requestFocus();
break;
case R.id.editText2:
if(text.length()==1)
et3.requestFocus();
else if(text.length()==0)
et1.requestFocus();
break;
case R.id.editText3:
if(text.length()==1)
et4.requestFocus();
else if(text.length()==0)
et2.requestFocus();
break;
case R.id.editText4:
if(text.length()==0)
et3.requestFocus();
break;
}
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
@Override
public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
}
How to use above class
et1.addTextChangedListener(new GenericTextWatcher(et1));
et2.addTextChangedListener(new GenericTextWatcher(et2));
et3.addTextChangedListener(new GenericTextWatcher(et3));
et4.addTextChangedListener(new GenericTextWatcher(et4));
Here et1,et2,et3 and et4 are your EditTexts, I know its bad naming convention as per Java Standard, but you can replace it with yours.
P.S You can find the xml design for this here GitHub some other, sample design xml for reference
I implemented the following code based on other answers.
I wanted this code to be very simple, optimized and understandable for changes.
Don't use android:maxLength="1"
in your xml.
//package your package
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
public class PinActivity extends AppCompatActivity {
private EditText editText1, editText2, editText3, editText4;
private EditText[] editTexts;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pin);
editText1 = (EditText) findViewById(R.id.otpEdit1);
editText2 = (EditText) findViewById(R.id.otpEdit2);
editText3 = (EditText) findViewById(R.id.otpEdit3);
editText4 = (EditText) findViewById(R.id.otpEdit4);
editTexts = new EditText[]{editText1, editText2, editText3, editText4};
editText1.addTextChangedListener(new PinTextWatcher(0));
editText2.addTextChangedListener(new PinTextWatcher(1));
editText3.addTextChangedListener(new PinTextWatcher(2));
editText4.addTextChangedListener(new PinTextWatcher(3));
editText1.setOnKeyListener(new PinOnKeyListener(0));
editText2.setOnKeyListener(new PinOnKeyListener(1));
editText3.setOnKeyListener(new PinOnKeyListener(2));
editText4.setOnKeyListener(new PinOnKeyListener(3));
}
public class PinTextWatcher implements TextWatcher {
private int currentIndex;
private boolean isFirst = false, isLast = false;
private String newTypedString = "";
PinTextWatcher(int currentIndex) {
this.currentIndex = currentIndex;
if (currentIndex == 0)
this.isFirst = true;
else if (currentIndex == editTexts.length - 1)
this.isLast = true;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
newTypedString = s.subSequence(start, start + count).toString().trim();
}
@Override
public void afterTextChanged(Editable s) {
String text = newTypedString;
/* Detect paste event and set first char */
if (text.length() > 1)
text = String.valueOf(text.charAt(0)); // TODO: We can fill out other EditTexts
editTexts[currentIndex].removeTextChangedListener(this);
editTexts[currentIndex].setText(text);
editTexts[currentIndex].setSelection(text.length());
editTexts[currentIndex].addTextChangedListener(this);
if (text.length() == 1)
moveToNext();
else if (text.length() == 0)
moveToPrevious();
}
private void moveToNext() {
if (!isLast)
editTexts[currentIndex + 1].requestFocus();
if (isAllEditTextsFilled() && isLast) { // isLast is optional
editTexts[currentIndex].clearFocus();
hideKeyboard();
}
}
private void moveToPrevious() {
if (!isFirst)
editTexts[currentIndex - 1].requestFocus();
}
private boolean isAllEditTextsFilled() {
for (EditText editText : editTexts)
if (editText.getText().toString().trim().length() == 0)
return false;
return true;
}
private void hideKeyboard() {
if (getCurrentFocus() != null) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
}
}
public class PinOnKeyListener implements View.OnKeyListener {
private int currentIndex;
PinOnKeyListener(int currentIndex) {
this.currentIndex = currentIndex;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
if (editTexts[currentIndex].getText().toString().isEmpty() && currentIndex != 0)
editTexts[currentIndex - 1].requestFocus();
}
return false;
}
}
}