javaandroidandroid-edittextandroid-input-filter

Android EditText Symbol must remain at initial position


I have a text field where the start symbol is $ (could be euro or pound depending on an application setting). I need to make it so that if the user clicks before the symbol nothing will happen. In other words, the selection must remain after the symbol. I tried doing something like this but it seems wrong and it gave me an error:

billAmount.addTextChangedListener(new TextWatcher() {

    //other methods 

    @Override
    public void afterTextChanged(Editable s) {
        billAmount.setText(currencySymbol + billAmount.getText().toString());
    }
});

I was thinking of using an inputFilter but nothing I tried worked. I'm also not allowed to use a TextView right before the EditText.


Solution

  • First, in your code sample, the reason you are getting an error is because, as others have said, you are calling the setText method inside the afterTextChanged method. Calling setText is obviously changing the text which causes afterTextChanged to be called again. This results in the afterTextChanged being called continuously until there is eventually a stack overflow.

    You have two issues: 1) You want to always keep the cursor positioned after the currency symbol, and 2) You want to make sure the currency symbol is never somehow removed.

    The easiest way to solve #1 is to create a subclass of EditText and override the onSelectionChanged method.

    public class MyEditText extends EditText {
    
        // ...
    
        @Override
        public void onSelectionChanged(int selStart, int selEnd) {
            super.onSelectionChanged(selStart, selEnd);
    
            // Make sure the text's length is greater than zero.
            // Then, if the cursor is at position zero...
            if (getText().length() > 0 && selStart == 0) {
                // ...move it over to position one.
                setSelection(1, selEnd);
            }
        }
    }
    

    This will force the cursor to always go after the currency symbol even if the user attempts to move it before it. The check getText().length() > 0 is to ensure that the EditText contains at least one character, otherwise attempting to move the cursor will result in an Exception.

    As for #2, there are a couple ways to go at it. You can attempt to use some instance variables inside your TextWatcher to keep track of when the text needs to be formatted, but that won't prevent the unnecessary method calls from actually happening, and it adds some unneeded complexity. I think it would be easier to simply use an InputFilter, which you can specify in your extended EditText's constructor.

    public class MyEditText extends EditText {
    
        public MyEditText(Context context) {
            super(context);
    
            // Set the EditText's input filter.
            setFilters(new InputFilter[] { new InputFilter {
                @Override
                public CharSequence filter(CharSequence source, int start, int end,
                                            Spanned dest, int dstart, int dend) {
                    // If the currency symbol is about to be replaced...
                    if (dstart == 0)
                        // Add the currency symbol to the front of the source.
                        return currencySymbol + source;
                    // else
                        // Return null to indicate that the change is okay.
                        return null;
                } 
            }});
        }
    
        // ...
    }
    

    In the filter method, the dest parameter represents the EditText's text, and the dstart and dend parameters represent the start and end positions of the portion of the text that is about to be replaced. Since the currency symbol should always be the first character, we know it is about to be replaced if dstart is zero, in which case we simply return the source (which represents the replacement text) with the currency symbol placed in front. Otherwise, we indicate that the change is okay by returning null.

    I tested it, and it seems to work for what you need.

    On a side note, although I understand that you're not "allowed" to use a TextView, I think it's worth reiterating that using one would provide a much better solution to this problem. One particularly useful solution being to have a hidden EditText contain the raw input from the user, and having the TextView on top of the EditText. You would use a TextWatcher to update the TextView with the properly formatted input from the EditText.