Search code examples
androidandroid-recyclerviewadaptertextwatcher

Use of different TextWatcher-implementations inside RecyclerView.Adapter


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!


Solution

  • 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 !!!
        }
    }