Search code examples
androidandroid-edittexttextwatcher

Textwatcher not working on KitKat for number as inputType


I am trying to use a custom ViewGroup for my app. In that I am using the following xml to be inflated in my ViewGroup class.

 <?xml version="1.0" encoding="utf-8"?>
<!-- This layout is used wherever the pin entering screens used -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal" >

    <TextView
        android:id="@+id/textView1"
        style="?attr/txtNormalStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:drawable/editbox_background_normal"
        android:ems="1"
        android:inputType="numberPassword" />

    <TextView
        android:id="@+id/textView2"
        style="?attr/txtNormalStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:drawable/editbox_background_normal"
        android:ems="1"
        android:inputType="numberPassword" />

    <TextView
        android:id="@+id/textView3"
        style="?attr/txtNormalStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:drawable/editbox_background_normal"
        android:ems="1"
        android:inputType="numberPassword" />

    <TextView
        android:id="@+id/textView4"
        style="?attr/txtNormalStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:drawable/editbox_background_normal"
        android:ems="1"
        android:inputType="numberPassword" />

    <EditText
        android:id="@+id/edtInvisible"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:maxLength="4"
        android:visibility="visible" />
     </LinearLayout>

And following is my extended view group class

 public class View_Pin_Text extends LinearLayout implements
        View.OnClickListener, TextWatcher, View.OnKeyListener {

    private String strPin;
    private TextView txtView1, txtView2, txtView3, txtView4;
    private EditText edtText;
    private boolean isInTextWatcher = false;

    public View_Pin_Text(Context context, AttributeSet attrs) {
        super(context, attrs);

        // inflating the custom layout for the view group
        LayoutInflater mInflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mInflater.inflate(R.layout.view_pin_enter, this, true);

        // 4 text views for showing pin to user
        txtView1 = (TextView) findViewById(R.id.textView1);
        txtView2 = (TextView) findViewById(R.id.textView2);
        txtView3 = (TextView) findViewById(R.id.textView3);
        txtView4 = (TextView) findViewById(R.id.textView4);

        // setting on click listener
        txtView1.setOnClickListener(this);
        txtView2.setOnClickListener(this);
        txtView3.setOnClickListener(this);
        txtView4.setOnClickListener(this);

        // invisible edit text for invoking keyboard
        edtText = (EditText) findViewById(R.id.edtInvisible);

        // text change listener to update the input in text views
         edtText.addTextChangedListener(this);

        // key listener to handle backspace/del keys press
        edtText.setOnKeyListener(this);

    }

    /**
     * @return strPin
     *         <p>
     *         Gives the currently given pin by the user
     *         </p>
     */
    public String getStrPin() {
        return strPin;
    }

    /**
     * @param strPin
     *            <p>
     *            Sets the pin to instance object and updates the proper
     *            characters in all text views
     *            </p>
     */
    public void setStrPin(String strPin) {

        if (strPin != null) {
            int lenght = strPin.length();

            if (lenght <= 4)
                this.strPin = strPin;

            Log.d("text", strPin);

            switch (lenght) {
            case 0:
                txtView1.setText("");
                txtView2.setText("");
                txtView3.setText("");
                txtView4.setText("");
                break;
            case 1:
                txtView1.setText(String.valueOf(strPin.charAt(0)));
                txtView2.setText("");
                txtView3.setText("");
                txtView4.setText("");
                break;
            case 2:
                txtView1.setText(String.valueOf(strPin.charAt(0)));
                txtView2.setText(String.valueOf(strPin.charAt(1)));
                txtView3.setText("");
                txtView4.setText("");
                break;
            case 3:
                txtView1.setText(String.valueOf(strPin.charAt(0)));
                txtView2.setText(String.valueOf(strPin.charAt(1)));
                txtView3.setText(String.valueOf(strPin.charAt(2)));
                txtView4.setText("");
                break;
            case 4:
                txtView1.setText(String.valueOf(strPin.charAt(0)));
                txtView2.setText(String.valueOf(strPin.charAt(1)));
                txtView3.setText(String.valueOf(strPin.charAt(2)));
                txtView4.setText(String.valueOf(strPin.charAt(3)));
                ((InputMethodManager) getContext().getSystemService(
                        Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(
                        edtText.getWindowToken(), 0);
                break;
            }
        } else {
            this.strPin = strPin;
        }
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(getContext(), "OnClick", Toast.LENGTH_SHORT).show();
        ((InputMethodManager) getContext().getSystemService(
                Context.INPUT_METHOD_SERVICE)).showSoftInput(edtText,
                InputMethodManager.SHOW_FORCED);
    }

    @Override
    public void afterTextChanged(Editable s) {

        if (isInTextWatcher)
            return;

        isInTextWatcher = true;

        Log.d("text", "changed-" + s.toString());

        if (getStrPin() == null) {
            setStrPin(s.toString());
        } else {
            setStrPin(getStrPin() + "" + s.toString());
        }
        edtText.setText("");

        isInTextWatcher = false;
    }

    @Override
    public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
            int arg3) {

    }

    @Override
    public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {

        if (keyCode == KeyEvent.KEYCODE_DEL
                && event.getAction() != KeyEvent.ACTION_DOWN) {
            if (getStrPin() != null) {
                int length = getStrPin().length();
                if (length > 0) {
                    setStrPin(getStrPin()
                            .substring(0, getStrPin().length() - 1));
                }
            }
        } 
        return true;
     }

    }

In this class my text watcher afterTextChanged is not triggered when my edit text set as android:inputType="number" and works perfectly for text input in my nexus 5 (Kitkat 4.4.4). But when I tried it with Samsung Core (Jellybean 4.1.2) works fine.. So What is the problem and how should this be solved?


Solution

  • Android's own soft keyboard (LatinIME) deals with number input a little differently from other characters. Here's what happens:

    private void sendKeyCodePoint(final int code) {
        ....
        ....
    
        // TODO: Remove this special handling of digit letters.
        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
        if (code >= '0' && code <= '9') {
            sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0);
            return;
        }
    
        if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
            // a hardware keyboard event on pressing enter or delete. This is bad for many
            // reasons (there are race conditions with commits) but some applications are
            // relying on this behavior so we continue to support it for older apps.
            sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
        } else {
            mConnection.commitText(StringUtils.newSingleCodePointString(code), 1);
        }
    }
    

    So, for '0' - '9' (and ENTER, before JellyBean), a KeyEvent is sent. The problem is - your OnKeyListener consumes every KeyEvent:

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
    
        if (keyCode == KeyEvent.KEYCODE_DEL
                && event.getAction() != KeyEvent.ACTION_DOWN) {
            if (getStrPin() != null) {
                int length = getStrPin().length();
                if (length > 0) {
                    setStrPin(getStrPin()
                            .substring(0, getStrPin().length() - 1));
                }
            }
        } 
    
        // Returning `true` at this point means that you have handled whatever was sent
        return true;
    }
    

    It seems that you wish to handle KeyEvent.KEYCODE_DEL. In this case, your OnKeyListener should look like:

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
    
        if (keyCode == KeyEvent.KEYCODE_DEL
                && event.getAction() != KeyEvent.ACTION_DOWN) {
            if (getStrPin() != null) {
                int length = getStrPin().length();
                if (length > 0) {
                    setStrPin(getStrPin()
                            .substring(0, getStrPin().length() - 1));
    
                    // Handled
                    return true;
                }
            }
        } 
    
        // Let everything other that KEYCODE_DEL be handled elsewhere
        return false;
    }
    

    I am not sure why your code works on Samsung Core, but It might be because Samsung makes quite a lot of changes to AOSP. It could be that they don't send a KeyEvent for 0 - 9.

    Another issue: Your OnKeyListener will only work for API < 16. For API >= 16, KEYCODE_DEL is not sent as a KeyEvent. Look at InputConnectionWrapper (specifically deleteSurroundingText(...)) to support similar functionality on JellyBean & later.