0

Currently I use a RecyclerView to represent a dynamically configuration list form.

Every configuration item (entry at RecyclerView list) contains one EditText item. To avoid wrong user input (some fields allow only integer, others only one digit after comma), I've implemented two different TextWatcher-filters which correct illegal input ("DecimalFilterDigitsAfterComma" and "DecimalFilterInteger"). My RecyclerView has 16 configuration items in total, but can only display maximum 8 at one time.

My problem is that the TextWatchers are assigned to specific Items (Integers and Decimal-Point TextEdit). But when I'm scrolling a bit, they change their order, so that Decimal- and Integer-Filters get swapped.

The TextWatcher items will be created inside the ConfigurationAdapter which is a RecyclerView.Adapter. I've event managed that the TextWatcher is only created once for each entry by using the mListConfigInit which is a boolean flag list for the items.

ConfigurationAdapter.java:

public class ConfigurationAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    /*
    ...
    */

    private List<ConfigItem> mConfiguration = new ArrayList<>();

    // make sure that DecimalFilter is only created once for each item
    private List<Boolean> mListConfigInit = new ArrayList<>();
    public ConfigurationAdapter() {
    }


    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.listitem_configuration,
                parent,
                false);

        final ConfigurationViewHolder vh = new ConfigurationViewHolder(v);


        /*
        ...
        */

        return vh;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        final ConfigurationViewHolder vh = (ConfigurationViewHolder) holder;
        ConfigItem config = mConfiguration.get(position);

    if(config.ShowValueAsFloat()) {
        vh.SetTextWatcherType(ConfigurationViewHolder.TextWatcherType.type_FloatActive);
    } else {
        vh.SetTextWatcherType(ConfigurationViewHolder.TextWatcherType.type_IntActive);
    }


        // set name and unit
        vh.mName.setText(config.mName);
        vh.mUnit.setText(config.mUnit);

        /*
        ...
        */
    }

    @Override
    public int getItemCount() {
        return mConfiguration.size();
    }

    public void addConfigItem(ConfigItem item) {
        mConfiguration.add(item);
        mListConfigInit.add(new Boolean(false));
        notifyItemInserted(mConfiguration.size() - 1);
        //notifyDataSetChanged();
    }

    /*
    ...
    */


}

ConfigurationViewHolder.java (changed according to pskink-comments):

public final class ConfigurationViewHolder extends RecyclerView.ViewHolder implements TextWatcher {
    public TextView mName;
    public CheckBox mCheckbox;
    public SeekBar mSeekbar;
    public EditText mValueEditText;
    public TextView mUnit;


    private List<TextWatcher> mListTextWatchers = new ArrayList<>();

    public enum TextWatcherType {
        type_FloatActive(0),
        type_IntActive(1);

        private int mValue;

        TextWatcherType(int value) {
            mValue = value;
        }

        int val() { return mValue; }
    }

    private TextWatcherType mTextWatcherType = TextWatcherType.type_FloatActive;

    public ConfigurationViewHolder(View itemView) {
        super(itemView);

        mName = (TextView) itemView.findViewById(R.id.textView_configuration_name);
        mValueEditText = (EditText) itemView.findViewById(R.id.editText_configuration_value);
        mUnit = (TextView) itemView.findViewById(R.id.textView_configuration_unit);
        mCheckbox = (CheckBox) itemView.findViewById(R.id.checkbox_configuration);
        mSeekbar = (SeekBar) itemView.findViewById(R.id.seekBar_configuration);

        mListTextWatchers.add(0, new DecimalFilterDigitsAfterComma(mValueEditText, 1));
        mListTextWatchers.add(1, new DecimalFilterInteger(mValueEditText));
        mValueEditText.addTextChangedListener(this);
    }

    public void SetTextWatcherType(TextWatcherType textWatcherType) {
        mTextWatcherType = textWatcherType;
    }

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

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

    @Override
    public void afterTextChanged(Editable editable) {
        mListTextWatchers.get(mTextWatcherType.val()).afterTextChanged(editable);
    }
}

DecimalFilterInteger.java

public class DecimalFilterInteger implements TextWatcher {
    private final static String TAG = ConfigurationAdapter.class.getSimpleName();
    private final EditText mEditText;
    private String mLastTextValue = new String("");

    public DecimalFilterInteger(EditText editText) {
        this.mEditText = editText;
    }

    @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 synchronized void afterTextChanged(final Editable text) {
        String strInput = text.toString().trim();
        if(strInput.isEmpty()) {
            return;
        }

        if(strInput.equals(mLastTextValue)) {   // return when same value as last time to avoid endless loop
            return;
        }

        if ((strInput.charAt(0) == '.')) {  // handle dot at beginning
            strInput = "";
        }

        if(strInput.contains(".")){         // cut trailing comma
            String numberBeforeDecimal = strInput.split("\\.")[0];
            strInput = numberBeforeDecimal;
        }
        mEditText.removeTextChangedListener(this);

        mEditText.getText().clear();    // do not use setText here to avoid changing the keyboard
        mEditText.append(strInput);     // back to default (e. g. from 123-mode to abc-mode),
                                        // see: http://stackoverflow.com/questions/26365808/edittext-settext-changes-the-keyboard-type-to-default-from-123-to-abc
        mLastTextValue = mEditText.getText().toString();

        mEditText.setSelection(mEditText.getText().toString().trim().length());
        mEditText.addTextChangedListener(this);
    }
}

Many thanks in advance for your help!

  • You should _really_ consider using the `viewType`. One for decimal, one for float. – David Medenjak Nov 29 '16 at 15:36
  • You mean the `ViewType` like in this post: [http://stackoverflow.com/questions/26245139/how-to-create-recyclerview-with-multiple-view-type](http://stackoverflow.com/questions/26245139/how-to-create-recyclerview-with-multiple-view-type) I'm not sure because my RecyclerView-entries are almost the same except the different input of the EditText-field. Wouldn't a custom TextWatcher (for input filtering) be the better solution? – Philipp Doublehammer Nov 29 '16 at 15:43
  • 1
    you have to call `addTextChangedListener` each time `onBindViewHolder` is called (of course after removing the old one) – pskink Nov 29 '16 at 15:59
  • 1
    or it is better to implement `TextWatcher` in your `ConfigurationViewHolder` and call one of two watchers based on the item type (integer / float with one digit after comma) – pskink Nov 29 '16 at 16:15
  • @pskink: I've implemented your first comment. Unfortunately it behaved like expected: The RecyclerView get stuck when scrolling and the app crashes. I used a Map to save the TextWatcher-items in combination with their position. – Philipp Doublehammer Nov 29 '16 at 16:20
  • 1
    so implement `TextWatcher` in your `ConfigurationViewHolder`, you dont need any `Map`, just change "type" inside `onBindViewHolder` – pskink Nov 29 '16 at 16:21
  • @pskink: Now I implemented the TextWatcher inside the ConfigurationViewHolder by creating one Member for each TextWatcher-type and using a method to add/removeTextChangedListener in combination with a current-state-variable. Unfortunately the app freezes when I'm scrolling too fast and the swapping-behaviour is still there. Many thanks for your help! – Philipp Doublehammer Nov 29 '16 at 16:49
  • 1
    so now you `addTextChangedListener` once inside `ConfigurationViewHolder` and inside `TextWatcher` just call current watcher based on current-state-variable. – pskink Nov 29 '16 at 16:53
  • Sorry, I've implemented that already, just forgot to update the code of 'ConfigurationAdapter.java' here. I've updated it now. I only do update the state already, as you can see, or do you mean anything else? – Philipp Doublehammer Nov 29 '16 at 16:58
  • no no, over-complicated, now there is no need to add / remove, see this http://pastebin.com/RQQdW2Xx – pskink Nov 29 '16 at 17:09
  • 1
    BTW instead of `TextWatcher`s shouldn't you use `android.text.InputFilter`? the docs: `InputFilters can be attached to Editables to constrain the changes that can be made to them.` – pskink Nov 29 '16 at 17:16
  • Yes, that would be an option but I prefer the TextWatcher in foresight for future changes at the validation. Now I've implemented your very elegant solution with the ConfigurationViewHolder which implements the TextWatcher but it hangs in an endless loop ('afterTextChanged' of the outer class is recalled, when the inner implementation 'DecimalFilterInteger' does the EditText.getText().clear(). I think because only the specialized TextWatcher is removed at 'afterTextChanged' before I edit the input field. – Philipp Doublehammer Nov 29 '16 at 18:12
  • 1
    http://stackoverflow.com/a/34282813/2252830 but honestly `InputFilter` is better for that imho – pskink Nov 29 '16 at 18:36
  • I've added already such kind of flag (updated the code up there). I just checked the input-value against the former one to avoid repeating calls. Now the app does not crash anymore, but the behaviour that the TextWatcher filters swap (currently everything gets an DecimalIntegerFilter) is still there. I'm debugging, but can't find an error. But I still see endless calls of the 'afterTextChanged'-methods but they return early, so they won't cause a crash, but it's not beautiful. – Philipp Doublehammer Nov 29 '16 at 18:41
  • 1
    `but the behaviour that the TextWatcher filters swap`, swap? what you mean? – pskink Nov 29 '16 at 18:47
  • I mean that at list-fields where a floating-point-filter should be, a Integer-filter appears (no dot-sign input possible). When I set the breakpoint inside the 'DecimalFilterDigitsAfterComma' it gets triggered but after it, also a 'DecimalFilterInteger'. At the startup of the fragment every filter is working, but after some scrolling, only Integer-Filters are at every field, therefore no comma-input will be possible. – Philipp Doublehammer Nov 29 '16 at 18:50
  • 1
    this is because you are adding watchers multiple times, just do it once in view holder ctor, check where you are calling `addTextChangedListener` – pskink Nov 29 '16 at 18:51
  • It was the calling of 'removeTextChangedListener' and 'addTextChangedListener' inside the 'afterTextChanged' of the specialized TextWatchers. Thanks a lot, without your help I wouldn't have found it! (When you in the near of Vienna, I owe you a bottle of wine!) – Philipp Doublehammer Nov 29 '16 at 19:02
  • i take your word: only Heuriger ;-) – pskink Nov 29 '16 at 19:09

1 Answers1

0

The cause of the swap/switching behaviour of the two different TextWatcher-implementations inside the RecyclerView was that I called removeTextChangedListenerand addTextChangedListenerinside their afterTextChanged-methods to avoid retriggering of the afterTextChanged-method.

The best way to avoid retriggering is a simple check if the text changed since the last call:

public class DecimalFilterInteger implements TextWatcher {
    private final static String TAG = ConfigurationAdapter.class.getSimpleName();
    private final EditText mEditText;
    private String mLastTextValue = new String("");

    // ...

    @Override
    public synchronized void afterTextChanged(final Editable text) {
        String strInput = text.toString().trim();
        if(strInput.isEmpty()) {
            return;
        }

        if(strInput.equals(mLastTextValue)) {   // return when same value as last time to avoid endless loop
            return;
        }

        if ((strInput.charAt(0) == '.')) {  // handle dot at beginning
            strInput = "";
        }

        if(strInput.contains(".")){         // cut trailing comma
            String numberBeforeDecimal = strInput.split("\\.")[0];
            strInput = numberBeforeDecimal;
        }
        //mEditText.removeTextChangedListener(this);    // CAUSE OF SWAP-ERROR !!!

        mEditText.getText().clear();    // do not use setText here to avoid changing the keyboard
        mEditText.append(strInput);     // back to default (e. g. from 123-mode to abc-mode),
                                        // see: http://stackoverflow.com/questions/26365808/edittext-settext-changes-the-keyboard-type-to-default-from-123-to-abc
        mLastTextValue = mEditText.getText().toString();

        mEditText.setSelection(mEditText.getText().toString().trim().length());
        //mEditText.addTextChangedListener(this);       // CAUSE OF SWAP-ERROR !!!
    }
}