Skip to content

Instantly share code, notes, and snippets.

@ShivamPokhriyal
Created November 27, 2020 15:03
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ShivamPokhriyal/8d0cf4aef062e6c59d00c04c53e03158 to your computer and use it in GitHub Desktop.
Save ShivamPokhriyal/8d0cf4aef062e6c59d00c04c53e03158 to your computer and use it in GitHub Desktop.
A custom view to handle otp input in multiple edittexts. It will move focus to next edittext, if available, when user enters otp and it will move focus to the previous edittext, if available, when user deletes otp. It will also delegate the paste option, if user long presses and pastes a string into the otp input.
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<declare-styleable name="OTPView">
<!-- Defines the next view to give focus to when the text is filled.-->
<attr name="nextView" format="reference"/>
<!-- Defines the previous view to give focus to when backButton is pressed.-->
<attr name="prevView" format="reference"/>
</declare-styleable>
...
</resources>
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* This class handles otp input in multiple edittexts.
* It will move focus to next edittext, if available, when user enters otp.
* And it will move focus to the previous edittext, if available, when user deletes otp.
* It will also delegate the paste option, if user long presses and pastes a string into the otp input.
*
* <b>XML attributes</b>
*
* @attr ref your_package_name.R.styleable#OTPView_nextView
* @attr ref your_package_name.R.styleable#OTPView_prevView
*
* @author $|-|!˅@M
*/
public class OTPEditText extends androidx.appcompat.widget.AppCompatEditText {
@Nullable
private View nextView;
@Nullable
private View previousView;
// Unfortunately getParent returns null inside the constructor. So we need to store the IDs.
private int nextViewId;
private int previousViewId;
@Nullable
private Listener listener;
private static final int NO_ID = -1;
public interface Listener {
void onPaste(String s);
}
public OTPEditText(@NonNull Context context) {
super(context);
}
public OTPEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public OTPEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public void setListener(Listener listener) {
this.listener = listener;
}
/**
* Called when a context menu option for the text view is selected. Currently
* this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
* {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
*
* @return true if the context menu item action was performed.
*/
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.paste) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
// Examines the item on the clipboard. If getText() does not return null, the clip item contains the
// text. Assumes that this application can only handle one item at a time.
ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
// Gets the clipboard as text.
CharSequence pasteData = item.getText();
if (listener != null && pasteData != null) {
listener.onPaste(pasteData.toString());
return true;
}
}
return super.onTextContextMenuItem(id);
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
// If we've gotten focus here
if (focused && this.getText() != null) {
this.setSelection(this.getText().length());
}
}
private void init(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.OTPView, 0, 0);
nextViewId = typedArray.getResourceId(R.styleable.OTPView_nextView, NO_ID);
previousViewId = typedArray.getResourceId(R.styleable.OTPView_prevView, NO_ID);
typedArray.recycle();
this.setOnKeyListener((v, keyCode, event) -> {
if (event.getAction()!= KeyEvent.ACTION_DOWN) {
return true;
}
//You can identify which key pressed by checking keyCode value with KeyEvent.KEYCODE_
if(keyCode == KeyEvent.KEYCODE_DEL) {
// Back pressed. If we have a previous view. Go to it.
if (getPreviousView() != null) {
getPreviousView().requestFocus();
return true;
}
}
return false;
});
this.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) { }
@Override
public void afterTextChanged(Editable s) {
if (s.length() == 1 && getNextView() != null) {
getNextView().requestFocus();
} else if (s.length() == 0 && getPreviousView() != null) {
getPreviousView().requestFocus();
}
}
});
// Android 3rd party keyboards show the copied text into the suggestion box for the user.
// Users can then simply tap on that suggestion to paste the text on the edittext.
// But I don't know of any API that allows handling of those paste actions.
// Below code will try to tell those keyboards to stop showing those suggestion.
this.setInputType(EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS | EditorInfo.TYPE_CLASS_NUMBER);
}
private View getNextView() {
if (nextView != null) {
return nextView;
}
if (nextViewId != NO_ID && getParent() instanceof View) {
nextView = ((View) getParent()).findViewById(nextViewId);
return nextView;
}
return null;
}
private View getPreviousView() {
if (previousView != null) {
return previousView;
}
if (previousViewId != NO_ID && getParent() instanceof View) {
previousView = ((View) getParent()).findViewById(previousViewId);
return previousView;
}
return null;
}
}
...
private void setupListener() {
otp1.setListener(this)
otp2.setListener(this)
otp3.setListener(this)
otp4.setListener(this)
}
@Override
public void onPaste(String s) {
if (s != null && s.length == 4) {
// Then only paste.
otp1.setText(s[0].toString())
otp2.setText(s[1].toString())
otp3.setText(s[2].toString())
otp4.setText(s[3].toString())
// Set cursor position in last edittext.
// Unfortunately setText doesn't really handle cursor position in a good way.
otp4.setSelection(1)
}
}
...
...
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<your_package_name.OTPEditText
android:id="@+id/otp1"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:inputType="number"
android:imeOptions="actionNext"
android:maxLength="1"
android:padding="12dp"
android:focusable="true"
app:nextView="@id/otp2"/>
<Space
android:layout_width="12dp"
android:layout_height="wrap_content" />
<your_package_name.OTPEditText
android:id="@+id/otp2"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:inputType="number"
android:imeOptions="actionNext"
android:maxLength="1"
android:padding="12dp"
android:focusable="true"
app:prevView="@id/otp1"
app:nextView="@id/otp3" />
<Space
android:layout_width="12dp"
android:layout_height="wrap_content" />
<your_package_name.OTPEditText
android:id="@+id/otp3"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:inputType="number"
android:imeOptions="actionNext"
android:maxLength="1"
android:padding="12dp"
android:focusable="true"
app:prevView="@id/otp2"
app:nextView="@id/otp4" />
<Space
android:layout_width="12dp"
android:layout_height="wrap_content" />
<your_package_name.OTPEditText
android:id="@+id/otp4"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:inputType="number"
android:imeOptions="actionDone"
android:maxLength="1"
android:padding="12dp"
android:focusable="true"
app:prevView="@id/otp3" />
</LinearLayout>
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment