Tutorial: PIN input view in Android

Hi, guys!

Today’s post will be about PIN-like view in Android. I’m sure that many Android developers would like to have it by default, me included. And so I wanted to show you how I handled the problem caused by lack of such view. But first, how should it look like? Well, it should like an input field where every character has it’s own box and while typing, characters should automatically populate those boxes. Sounds pretty easy, isn’t it? But later, when you think about all of the aspects, you will realize it’s not that simple at all…

First, I wanted to mention that this particular example is for text passwords (or text pin). The code for just numerical PIN view would be much easier. But it wasn’t the case with the app I was writing and I definitely needed the default keyboard. Because I could change my soft keyboard from text to numerical at any point of typing, I couldn’t have a collection of EditTexts because switching from one to another would have changed a keyboard either to text or numerical and I didn’t want that.

So, I had to think of something more flexible. And what I’ve done is created a collection of EditTexts AND another one which user wouldn’t normally see but which will actually take a focus and basically do the job for us.

OK, lets see some code!

Here’s the main.xml layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pin_content_layout" 
  android:orientation="vertical" 
  android:layout_width="match_parent" 
  android:layout_height="match_parent" 
  android:padding="@dimen/pin_content_layout_padding" > 

    <TextView android:layout_width="wrap_content" 
      android:layout_height="wrap_content" 
      android:text="@string/pin_label_text" 
      android:textStyle="bold" 
      android:gravity="center_horizontal" 
      android:layout_gravity="center_horizontal" 
      android:layout_marginBottom="@dimen/pin_label_margin" /> 

    <LinearLayout android:id="@+id/pin_layout" 
      android:layout_width="match_parent" 
      android:layout_height="wrap_content" 
      android:orientation="horizontal" 
      android:gravity="center_horizontal" 
      android:layout_gravity="center_horizontal"> 

        <EditText android:id="@+id/pin_first_edittext" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:contentDescription="@string/pin_content_desc" 
          style="@style/pin_edittext_style" /> 

        <EditText android:id="@+id/pin_second_edittext" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:contentDescription="@string/pin_content_desc" 
          style="@style/pin_edittext_style" /> 

        <EditText android:id="@+id/pin_third_edittext" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:contentDescription="@string/pin_content_desc" 
          style="@style/pin_edittext_style" /> 

        <EditText android:id="@+id/pin_forth_edittext" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:contentDescription="@string/pin_content_desc" 
          style="@style/pin_edittext_style" /> 

        <EditText android:id="@+id/pin_fifth_edittext" 
          android:layout_width="wrap_content" 
          android:layout_height="wrap_content" 
          android:contentDescription="@string/pin_content_desc" 
          style="@style/pin_edittext_style" />
    </LinearLayout> 

    <EditText android:id="@+id/pin_hidden_edittext" 
      android:layout_width="1dp" 
      android:layout_height="1dp" 
      android:gravity="center_horizontal" 
      android:layout_gravity="center_horizontal" 
      android:background="@null" 
      android:cursorVisible="false" 
      android:password="true" 
      android:maxLength="5" 
      android:textColor="#00000000" 
      android:contentDescription="@string/pin_content_desc" />
</LinearLayout> 

What you can see here is just a LinearLayout with some text label, five edit texts and one hidden below.
It is not even hidden, just a transparent 1dp width and height EditText. Edit texts for pin have its own style.

styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="pin_edittext_style">
       <item name="android:gravity">center</item>
       <item name="android:cursorVisible">false</item>
       <item name="android:maxLength">1</item>
       <item name="android:minEms">2</item>
       <item name="android:inputType">textPassword</item>
       <item name="android:focusable">true</item>
       <item name="android:focusableInTouchMode">true</item>
    </style>
</resources>

We want our characters aligned to center, cursor won’t be visible as we won’t edit one character separately but the whole PIN view at once. Max length each of those PIN inputs should be 1. minEms with value of 2 ensures the width of input won’t shrink or expand depending on character width.

Strings for this example project:

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
       <string name="app_name">PinProject</string>
       <string name="pin_content_desc">PIN input field</string>
       <string name="pin_label_text">PIN</string>
</resources>

Some dimentions:

dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
       <dimen name="pin_content_layout_padding">40dp</dimen>
       <dimen name="pin_label_margin">10dp</dimen>
       <dimen name="pin_min_width">32dp</dimen>
</resources>

And AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.lomza.pinproject"
     android:versionCode="1"
     android:versionName="1.0">

       <uses-sdk
           android:minSdkVersion="14"
           android:targetSdkVersion="18"/>
       <application
           android:label="@string/app_name"
           android:icon="@drawable/ic_launcher"
           android:theme="@android:style/Theme.Holo.Light">
       <activity
           android:name="MainActivity"
           android:label="@string/app_name">
              <intent-filter>
                  <action android:name="android.intent.action.MAIN"/>
                  <category android:name="android.intent.category.LAUNCHER"/>
              </intent-filter>
       </activity>
       </application>
</manifest>

This project wasn’t tested on Android 2.x.x, so I just put a min SDK version to 14 (which is Android 4.0). You can try to run it on lower versions.

I have also used Theme.Holo.Light. It could have been any other theme, it’s just that I need EditText focusable and default backgrounds and they were adjusted for the Light theme. Why bother about those backgrounds? Because we will set it programmatically depending on focus and on characters typed in our PIN view. You will see when you will actually run this project 🙂 Just remeber, you need to have some gfx files 😉

OK, so the most interesting part is the code!

Here’s MainActivity.java


/**
 * This activity holds view with a custom 5-digit PIN EditText.
 */
public class MainActivity extends Activity implements View.OnFocusChangeListener, View.OnKeyListener, TextWatcher {
    private EditText mPinFirstDigitEditText;
    private EditText mPinSecondDigitEditText;
    private EditText mPinThirdDigitEditText;
    private EditText mPinForthDigitEditText;
    private EditText mPinFifthDigitEditText;
    private EditText mPinHiddenEditText;

    @Override
    public void afterTextChanged(Editable s) {
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    /**
     * Hides soft keyboard.
     *
     * @param editText EditText which has focus
     */
    public void hideSoftKeyboard(EditText editText) {
        if (editText == null)
            return;

        InputMethodManager imm = (InputMethodManager) getSystemService(Service.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
    }

    /**
     * Initialize EditText fields.
     */
    private void init() {
        mPinFirstDigitEditText = (EditText) findViewById(R.id.pin_first_edittext);
        mPinSecondDigitEditText = (EditText) findViewById(R.id.pin_second_edittext);
        mPinThirdDigitEditText = (EditText) findViewById(R.id.pin_third_edittext);
        mPinForthDigitEditText = (EditText) findViewById(R.id.pin_forth_edittext);
        mPinFifthDigitEditText = (EditText) findViewById(R.id.pin_fifth_edittext);
        mPinHiddenEditText = (EditText) findViewById(R.id.pin_hidden_edittext);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MainLayout(this, null));

        init();
        setPINListeners();
    }

    @Override
    public void onFocusChange(View v, boolean hasFocus) {
        final int id = v.getId();
        switch (id) {
            case R.id.pin_first_edittext:
                if (hasFocus) {
                    setFocus(mPinHiddenEditText);
                    showSoftKeyboard(mPinHiddenEditText);
                }
                break;

            case R.id.pin_second_edittext:
                if (hasFocus) {
                    setFocus(mPinHiddenEditText);
                    showSoftKeyboard(mPinHiddenEditText);
                }
                break;

            case R.id.pin_third_edittext:
                if (hasFocus) {
                    setFocus(mPinHiddenEditText);
                    showSoftKeyboard(mPinHiddenEditText);
                }
                break;

            case R.id.pin_forth_edittext:
                if (hasFocus) {
                    setFocus(mPinHiddenEditText);
                    showSoftKeyboard(mPinHiddenEditText);
                }
                break;

            case R.id.pin_fifth_edittext:
                if (hasFocus) {
                    setFocus(mPinHiddenEditText);
                    showSoftKeyboard(mPinHiddenEditText);
                }
                break;
            default:
                break;
        }
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            final int id = v.getId();
            switch (id) {
                case R.id.pin_hidden_edittext:
                    if (keyCode == KeyEvent.KEYCODE_DEL) {
                        if (mPinHiddenEditText.getText().length() == 5)
                            mPinFifthDigitEditText.setText("");
                        else if (mPinHiddenEditText.getText().length() == 4)
                            mPinForthDigitEditText.setText("");
                        else if (mPinHiddenEditText.getText().length() == 3)
                            mPinThirdDigitEditText.setText("");
                        else if (mPinHiddenEditText.getText().length() == 2)
                            mPinSecondDigitEditText.setText("");
                        else if (mPinHiddenEditText.getText().length() == 1)
                            mPinFirstDigitEditText.setText("");

                        if (mPinHiddenEditText.length() > 0)
                            mPinHiddenEditText.setText(mPinHiddenEditText.getText().subSequence(0, mPinHiddenEditText.length() - 1));

                        return true;
                    }

                    break;

                default:
                    return false;
            }
        }

        return false;
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        setDefaultPinBackground(mPinFirstDigitEditText);
        setDefaultPinBackground(mPinSecondDigitEditText);
        setDefaultPinBackground(mPinThirdDigitEditText);
        setDefaultPinBackground(mPinForthDigitEditText);
        setDefaultPinBackground(mPinFifthDigitEditText);

        if (s.length() == 0) {
            setFocusedPinBackground(mPinFirstDigitEditText);
            mPinFirstDigitEditText.setText("");
        } else if (s.length() == 1) {
            setFocusedPinBackground(mPinSecondDigitEditText);
            mPinFirstDigitEditText.setText(s.charAt(0) + "");
            mPinSecondDigitEditText.setText("");
            mPinThirdDigitEditText.setText("");
            mPinForthDigitEditText.setText("");
            mPinFifthDigitEditText.setText("");
        } else if (s.length() == 2) {
            setFocusedPinBackground(mPinThirdDigitEditText);
            mPinSecondDigitEditText.setText(s.charAt(1) + "");
            mPinThirdDigitEditText.setText("");
            mPinForthDigitEditText.setText("");
            mPinFifthDigitEditText.setText("");
        } else if (s.length() == 3) {
            setFocusedPinBackground(mPinForthDigitEditText);
            mPinThirdDigitEditText.setText(s.charAt(2) + "");
            mPinForthDigitEditText.setText("");
            mPinFifthDigitEditText.setText("");
        } else if (s.length() == 4) {
            setFocusedPinBackground(mPinFifthDigitEditText);
            mPinForthDigitEditText.setText(s.charAt(3) + "");
            mPinFifthDigitEditText.setText("");
        } else if (s.length() == 5) {
            setDefaultPinBackground(mPinFifthDigitEditText);
            mPinFifthDigitEditText.setText(s.charAt(4) + "");

            hideSoftKeyboard(mPinFifthDigitEditText);
        }
    }

    /**
     * Sets default PIN background.
     *
     * @param editText edit text to change
     */
    private void setDefaultPinBackground(EditText editText) {
        setViewBackground(editText, getResources().getDrawable(R.drawable.textfield_default_holo_light));
    }

    /**
     * Sets focus on a specific EditText field.
     *
     * @param editText EditText to set focus on
     */
    public static void setFocus(EditText editText) {
        if (editText == null)
            return;

        editText.setFocusable(true);
        editText.setFocusableInTouchMode(true);
        editText.requestFocus();
    }

    /**
     * Sets focused PIN field background.
     *
     * @param editText edit text to change
     */
    private void setFocusedPinBackground(EditText editText) {
        setViewBackground(editText, getResources().getDrawable(R.drawable.textfield_focused_holo_light));
    }

    /**
     * Sets listeners for EditText fields.
     */
    private void setPINListeners() {
        mPinHiddenEditText.addTextChangedListener(this);

        mPinFirstDigitEditText.setOnFocusChangeListener(this);
        mPinSecondDigitEditText.setOnFocusChangeListener(this);
        mPinThirdDigitEditText.setOnFocusChangeListener(this);
        mPinForthDigitEditText.setOnFocusChangeListener(this);
        mPinFifthDigitEditText.setOnFocusChangeListener(this);

        mPinFirstDigitEditText.setOnKeyListener(this);
        mPinSecondDigitEditText.setOnKeyListener(this);
        mPinThirdDigitEditText.setOnKeyListener(this);
        mPinForthDigitEditText.setOnKeyListener(this);
        mPinFifthDigitEditText.setOnKeyListener(this);
        mPinHiddenEditText.setOnKeyListener(this);
    }

    /**
     * Sets background of the view.
     * This method varies in implementation depending on Android SDK version.
     *
     * @param view       View to which set background
     * @param background Background to set to view
     */
    @SuppressWarnings("deprecation")
    public void setViewBackground(View view, Drawable background) {
        if (view == null || background == null)
            return;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            view.setBackground(background);
        } else {
            view.setBackgroundDrawable(background);
        }
    }

    /**
     * Shows soft keyboard.
     *
     * @param editText EditText which has focus
     */
    public void showSoftKeyboard(EditText editText) {
        if (editText == null)
            return;

        InputMethodManager imm = (InputMethodManager) getSystemService(Service.INPUT_METHOD_SERVICE);
        imm.showSoftInput(editText, 0);
    }

    /**
     * Custom LinearLayout with overridden onMeasure() method
     * for handling software keyboard show and hide events.
     */
    public class MainLayout extends LinearLayout {

        public MainLayout(Context context, AttributeSet attributeSet) {
            super(context, attributeSet);
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            inflater.inflate(R.layout.main, this);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            final int proposedHeight = MeasureSpec.getSize(heightMeasureSpec);
            final int actualHeight = getHeight();

            Log.d("TAG", "proposed: " + proposedHeight + ", actual: " + actualHeight);

            if (actualHeight >= proposedHeight) {
                // Keyboard is shown
                if (mPinHiddenEditText.length() == 0)
                    setFocusedPinBackground(mPinFirstDigitEditText);
                else
                    setDefaultPinBackground(mPinFirstDigitEditText);
            }

            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
}

Wow, a lot of code you might say! And yet, there’s is just enough for correct functioning 😉

We have init() method in which our edit texts are initialized, then we have onFocusChange() method. If user touches any of our pin edit texts, we should catch the focus of our hidden EditText and show soft keyboard. onKey() method checks if DEL key was pressed, then checks how many characters were typed and based on that sets our edit texts to empty. onTextChanged() method sets correct background based on text length and also sets characters we typed.

The last interesting thing here is MainLayout which is just a LinearLayout with overridden onMeasure() method. Why do you need this? Because when the keyboard is shown, we need to highlight the first PIN box (if it is not empty). This is just for a better UX, nothing more I guess…

PIN view

 

And that’s all! A lot of code but the idea is quite clear. We have one EditText, which catches the focus and characters we type, everything else is just a programmatic trick 😉

Thanks for reading!

Like and share:

Published by

Tonia Tkachuk

I’m an Android Developer, writing code for living and for fun. Love beautiful apps with a clean code inside. Enjoy travelling and reading.