1

I am implementing a custom view for OTP of varying length upto 6 digits. I have extended a LinearLayout and use multiple edit text as its child view. Each edit text holds one digit. I want to implement the delete action from the soft keyboard for the above custom view. The following is the code for the OTP custom view.

public class OTPEditText extends LinearLayout {
    private int mDigitSpacing = 8;  // Space between digits 
    private int mDigitNumber = 6;   // Number of digits
    private int mDigitSize = 28;    // Font size of the digits
    private ArrayList<EditText> mEditTexts; // List of edit text each holding one digit
    private OnCompleteListener mCompleteListener;   //when all the edit text gets one digit each

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

    public OTPEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public OTPEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * Add the required number of Edit Texts
     * @param number - number of digits
     */
    public void setDigitNumber(int number){
        this.mDigitNumber = number;
        addViews();
    }

    public void setOnCompleteListener(OnCompleteListener listener) {
        this.mCompleteListener = listener;
    }

    private void addViews() {
        removeAllViews();
        mEditTexts = new ArrayList<>();
        for(int i = 0; i < mDigitNumber; i++){
            EditText editText = new EditText(getContext());
            //Set the necessary attributes
            editText.addTextChangedListener(new GenericTextWatcher(i));
            mEditTexts.add(editText);
            addView(editText);
        }

        requestLayout();
        if(mEditTexts.size() > 0) {
            mEditTexts.get(0).requestFocus();
        }

    }

    /**
     * similar to setText of an edit text, but
     * set one digit each to the edit text
     * @param s - string for the edit text
     */
    public void setText(String s){
        if(s.length() > mDigitNumber){
            s = s.substring(0, mDigitNumber);
        }
        int i;
        for(i = 0; i < s.length(); i++){
            mEditTexts.get(i).setText(s.charAt(i));
        }
        for(; i < mEditTexts.size(); i++){
            mEditTexts.get(i).setText("");
        }
    }

    /**
     * Similar to the getText of an edit text,
     * concatenates the text from each edit text
     * @return - concatenated string from each edit text
     */
    public String getText() {
        String text = "";
        if(!Utils.isEmptyList(mEditTexts)) {
            for (EditText editText : mEditTexts){
                text +=  editText.getText().toString();
            }
        }
        return text;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event){
        return true;
    }

    /**
     * Called whenever onClick of the View is called. Simulates the click event of
     * the required edit text.
     */
    public void doClick() {
        if(!Utils.isEmptyList(mEditTexts)){
            for(EditText editText : mEditTexts){
                if(editText.getText().toString().equals("")){
                    editText.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
                            SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN , 0, 0, 0));
                    editText.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
                            SystemClock.uptimeMillis(), MotionEvent.ACTION_UP , 0, 0, 0));
                    return;
                }
            }
            mEditTexts.get(mEditTexts.size()-1).dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
                    SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN ,
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mDigitSize,
                            getResources().getDisplayMetrics()), 0, 0));
            mEditTexts.get(mEditTexts.size()-1).dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
                    SystemClock.uptimeMillis(), MotionEvent.ACTION_UP ,
                    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, mDigitSize,
                            getResources().getDisplayMetrics()), 0, 0));
        }
    }

    public interface OnCompleteListener {
        void onComplete();
    }

    // Generic edit text watcher
    public class GenericTextWatcher implements TextWatcher {
        private int index;

        public GenericTextWatcher(int index){
            this.index = index;
        }

        @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.toString().length() >= 1){
                if(index +1 < mEditTexts.size()){
                    mEditTexts.get(index + 1).requestFocus();
                } else if(index == mEditTexts.size() - 1 && mCompleteListener != null){
                    mCompleteListener.onComplete();
                }
            }
        }
    }

}
Abdul Aziz
  • 21
  • 1
  • 5

2 Answers2

1
    edOtp1.addTextChangedListener(new OtpTextWatcher(edOtp1));
    edOtp2.addTextChangedListener(new OtpTextWatcher(edOtp2));
    edOtp3.addTextChangedListener(new OtpTextWatcher(edOtp3));
    edOtp4.addTextChangedListener(new OtpTextWatcher(edOtp4));

create this class that handle text on addition or deletion.

    private class OtpTextWatcher implements TextWatcher 
        {
                private View view;
                OtpTextWatcher(View view) {
                    this.view = view;
                }
                @Override
                public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                String text = charSequence.toString();
                switch (view.getId()) {
                    case R.id.edOtp1:
                        if (text.length() == 1)
                            edOtp2.requestFocus();
edOtp2.setSelection(edOtp2.getText().length());
                        else{
                            edOtp1.requestFocus();
}
                        break;
                    case R.id.edOtp2:
                        if (text.length() == 0) {
                            edOtp1.requestFocus();
                            edOtp1.setSelection(edOtp1.getText().length());
                        }
                        break;
                    case R.id.edOtp3:
                        if (text.length() == 0) {
                            edOtp2.requestFocus();
                            edOtp2.setSelection(edOtp2.getText().length());
                        }
                        break;
                    case R.id.edOtp4:
                        if (text.length() == 0) {
                            edOtp3.requestFocus();
                            edOtp3.setSelection(edOtp3.getText().length());
                        }
                        break;
                    default:
                        break;
                }

                }

                @Override
                public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
                }

                @Override
                public void afterTextChanged(Editable editable) {
                    String text = editable.toString();
                    switch (view.getId()) {
                        case R.id.edOtp1:
                            if (text.length() == 1)
                                edOtp2.requestFocus();
                            else
                                edOtp1.setSelection(edOtp1.getText().length());
                            break;
                        case R.id.edOtp2:
                            if (text.length() == 1)
                                edOtp3.requestFocus();
                            else if (text.length() == 0) {
                                edOtp1.requestFocus();
                                edOtp1.setSelection(edOtp1.getText().length());
                            }
                            break;
                        case R.id.edOtp3:
                            if (text.length() == 1)
                                edOtp4.requestFocus();
                            else if (text.length() == 0) {
                                edOtp2.requestFocus();
                                edOtp2.setSelection(edOtp2.getText().length());
                            }
                            break;
                        case R.id.edOtp4:
                            if (text.length() == 0) {
                                edOtp3.requestFocus();
                                edOtp3.setSelection(edOtp3.getText().length());
                            }
                            break;
                        default:
                            break;
                    }
                }

            }
Rahul
  • 3,293
  • 2
  • 31
  • 43
  • The `afterTextChanged` method won't be called on backspace/delete if the edit text was empty before the keypress. For eg: first enter a number in the first edit text, the focus will shift to second edit text. Now if backspace/delete is pressed, the method won't be called since it was empty before. – Abdul Aziz Feb 08 '17 at 07:30
  • please check it and let me know if any issue found – Rahul Feb 20 '17 at 17:48
  • 2
    I tried the code. Same, issue. TextWatcher won't be called on backpress if the edit text is empty before. Also on typing the last edit text the focus shifts to the third one. – Abdul Aziz Feb 22 '17 at 08:19
1

I created a gist here https://gist.github.com/ShivamPokhriyal/8d0cf4aef062e6c59d00c04c53e03158 which you can simply copy paste in your project.

It creates a custom OTPEditText class which handles shifting the focus to next or previous edittext when user types in or deletes and also handles the paste event when user long presses and pastes the otp in the editText. All this can be done in the xml only. No need to pollute your activity with these stuff.

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;
    }
}

The gist also includes the xml and java code that you can directly add to your activity.

Shivam Pokhriyal
  • 1,044
  • 11
  • 26