Сохранение содержимого EditText в RecyclerView

У меня есть элемент списка с EditText в нем я не знаю, сколько предметов будет. У меня проблема при вводе текста в EditText, а затем прокрутите вниз RecyclerView, после того, как я снова прокрутите вверх, в моем первом тексте нет EditText,

Мне интересно, что и где я должен написать код, чтобы, пока пользователь печатал или закончил печатать (я думал сделать это с TextWatcher) в EditText текст сохраняется в файл (я сохраню его в файле.txt во внешнем хранилище)

Должен ли я сделать это в onCreate метод действия или в классе адаптера или в другом месте?

Вот код

Основной код деятельности

 public class MainActivity extends Activity {

    RecyclerView mRecyclerView;
    MyAdapter mAdapter;
    String[] mDataSet= new String[20];
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // generating text for editText Views
        for (int i = 0; i<=19; i++){
        mDataSet[i]= "EditText n: "+i;

    }
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mAdapter = new MyAdapter(mDataSet);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setHasFixedSize(true);
    }

Мой код адаптера

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

private String[] mDataset;


public static class ViewHolder extends RecyclerView.ViewHolder {
    // each data item is just a string in this case
    public EditText mEditText;

    public ViewHolder(View v) {
        super(v);

        mEditText = (EditText) v.findViewById(R.id.list_item_edittext);
    }
}

public MyAdapter(String[] myDataset) {
    mDataset = myDataset;
}

@Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                 int viewType) {

    View v = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.list_item, parent, false);

    ViewHolder vh = new ViewHolder(v);
    return vh;
}

@Override
public void onBindViewHolder(ViewHolder holder,  final int position) {
    holder.mEditText.setText(mDataset[position]);

    //without this addtextChangedListener my code works fine ovbiusly
    // not saving the content of the edit Text when scrolled
    // If i add this code then when i scroll all textView that go of screen
    // and than come back in have messed up content
    holder.mEditText.addTextChangedListener(new TextWatcher() {

        @Override
        public void onTextChanged(CharSequence s, int start,
                                  int before, int count) {
           //setting data to array, when changed
           // this is a semplified example in the actual app i save the text
           // in  a .txt in the external storage
           mDataset[position] = s.toString();
        }

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

        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });

}

@Override
public int getItemCount() {
    return mDataset.length;
}

без этого "addtextChangedListener" мой код работает нормально, конечно, не сохраняя содержимое редактируемого текста при прокрутке. Если я добавляю этот код, когда я прокручиваю все представления editText, которые уходят с экрана и затем возвращаются, испортили содержимое.

18 ответов

Решение

Основной проблемой вашего решения является выделение и назначение TextWatcher в onBindViewHolder, что является дорогостоящей операцией, которая приводит к задержкам при быстрой прокрутке, а также, по-видимому, мешает определить, какую позицию обновлять в mAdapter.

Выполнение всех операций в onCreateViewHolder является более предпочтительным вариантом. Вот полное испытанное рабочее решение:

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

    private String[] mDataset;

    public MyAdapter(String[] myDataset) {
        mDataset = myDataset;
    }

    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_edittext, parent, false);
        // pass MyCustomEditTextListener to viewholder in onCreateViewHolder
        // so that we don't have to do this expensive allocation in onBindViewHolder
        ViewHolder vh = new ViewHolder(v, new MyCustomEditTextListener());

        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        // update MyCustomEditTextListener every time we bind a new item
        // so that it knows what item in mDataset to update
        holder.myCustomEditTextListener.updatePosition(holder.getAdapterPosition());
        holder.mEditText.setText(mDataset[holder.getAdapterPosition()]);
    }

    @Override
    public int getItemCount() {
        return mDataset.length;
    }


    public static class ViewHolder extends RecyclerView.ViewHolder {
        // each data item is just a string in this case
        public EditText mEditText;
        public MyCustomEditTextListener myCustomEditTextListener;

        public ViewHolder(View v, MyCustomEditTextListener myCustomEditTextListener) {
            super(v);

            this.mEditText = (EditText) v.findViewById(R.id.editText);
            this.myCustomEditTextListener = myCustomEditTextListener;
            this.mEditText.addTextChangedListener(myCustomEditTextListener);
        }
    }

    // we make TextWatcher to be aware of the position it currently works with
    // this way, once a new item is attached in onBindViewHolder, it will
    // update current position MyCustomEditTextListener, reference to which is kept by ViewHolder
    private class MyCustomEditTextListener implements TextWatcher {
        private int position;

        public void updatePosition(int position) {
            this.position = position;
        }

        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
            // no op
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
            mDataset[position] = charSequence.toString();
        }

        @Override
        public void afterTextChanged(Editable editable) {
            // no op
        }
    }
}

У меня была та же проблема, я добавил следующую строку, и она, кажется, устранила проблему на моей стороне.

mRecyclerview.setItemViewCacheSize(mDataset.size());

Надеюсь, это решит проблему на вашей стороне.

Я реализовал решение @dkarmazi, но оно мне не помогло. Итак, я пошел дальше, и есть действительно рабочее решение.

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

    private String[] mDataset;

    public MyAdapter(String[] myDataset) {
        mDataset = myDataset;
    }

    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                               int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_edittext, parent, false);
        // pass MyCustomEditTextListener to viewholder in onCreateViewHolder
        // so that we don't have to do this expensive allocation in onBindViewHolder
        ViewHolder vh = new ViewHolder(v, new MyCustomEditTextListener());

        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        // update MyCustomEditTextListener every time we bind a new item
        // so that it knows what item in mDataset to update
        holder.myCustomEditTextListener.updatePosition(holder.getAdapterPosition());
        holder.mEditText.setText(mDataset[holder.getAdapterPosition()]);
    }

    @Override
    public int getItemCount() {
        return mDataset.length;
    }

    @Override
    public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
        ((ViewHolder) holder).enableTextWatcher();
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
        ((ViewHolder) holder).disableTextWatcher();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        // each data item is just a string in this case
        public EditText mEditText;
        public MyCustomEditTextListener myCustomEditTextListener;

        public ViewHolder(View v, MyCustomEditTextListener myCustomEditTextListener) {
            super(v);

            this.mEditText = (EditText) v.findViewById(R.id.editText);
            this.myCustomEditTextListener = myCustomEditTextListener;
        }

        void enableTextWatcher() {
            mEditText.addTextChangedListener(myCustomEditTextListener);
        }

        void disableTextWatcher() {
            mEditText.removeTextChangedListener(myCustomEditTextListener);
        }
    }

    // we make TextWatcher to be aware of the position it currently works with
    // this way, once a new item is attached in onBindViewHolder, it will
    // update current position MyCustomEditTextListener, reference to which is kept by ViewHolder
    private class MyCustomEditTextListener implements TextWatcher {
        private int position;

        public void updatePosition(int position) {
            this.position = position;
        }

        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
            // no op
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
            mDataset[position] = charSequence.toString();
        }

        @Override
        public void afterTextChanged(Editable editable) {
            // no op
        }
    }
}

Основная проблема заключалась в том, что применяемый TextWatcher продолжал работать во время утилизации предметов.

Я пытался отключить его перед утилизацией, но нет никаких методов "beforeRecycle". Так что я использовал onViewDetachedFromWindow метод, и это сработало хорошо.

Есть только одна проблема, после добавления в EditText пользовательский Editable.Factory список снова тормозит. Я не знаю почему, может быть, я тоже найду решение этой проблемы

Я хотел бы создать интерфейс и передать текущую позицию адаптера для обработки события изменения текста

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

    private String[] mDataset;

    public MyAdapter(String[] myDataset) {
        mDataset = myDataset;
    }

    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                   int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_edittext, parent, false);
        ViewHolder vh = new ViewHolder(v, new ViewHolder.ITextWatcher() {
            @Override
            public void beforeTextChanged(int position, CharSequence s, int start, int count, int after) {
                // do something
            }

            @Override
            public void onTextChanged(int position, CharSequence s, int start, int before, int count) {
                mDataset[position] = s.toString();
            }

            @Override
            public void afterTextChanged(int position, Editable s) {
                // do something
            }
        });

        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        holder.mEditText.setText(mDataset[position]);
    }

    @Override
    public int getItemCount() {
        return mDataset.length;
    }


    public static class ViewHolder extends RecyclerView.ViewHolder {

        public EditText mEditText;
        private ITextWatcher mTextWatcher;

        public interface ITextWatcher {
            // you can add/remove methods as you please, maybe you dont need this much
            void beforeTextChanged(int position, CharSequence s, int start, int count, int after);

            void onTextChanged(int position, CharSequence s, int start, int before, int count);

            void afterTextChanged(int position, Editable s);
        }

        public ViewHolder(View v, ITextWatcher textWatcher) {
            super(v);

            this.mEditText = (EditText) v.findViewById(R.id.editText);

            this.mTextWatcher = textWatcher;

            this.mEditText.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    mTextWatcher.beforeTextChanged(getAdapterPosition(), s, start, count, after);
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    mTextWatcher.onTextChanged(getAdapterPosition(), s, start, before, count);
                }

                @Override
                public void afterTextChanged(Editable s) {
                    mTextWatcher.afterTextChanged(getAdapterPosition(), s);
                }
            });
        }
    }

}

Создайте массив String с размером данных вашего адаптера.

Например: String[] texts = new String[dataSize];

в методе onBindViewHolder внутри адаптера добавьте TextChangedListener в Textview.

Например: -

@Override
    public void onBindViewHolder(Viewholder holder, int position) {

//binding data from array 
   holder.yourEditText.setText(texts [position]);
   holder.yourEditText.addTextChangedListener(new TextWatcher() {

            @Override
            public void onTextChanged(CharSequence s, int start,
                    int before, int count) {
                //setting data to array, when changed
                texts [position] = s.toString();
            }

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

            @Override
            public void afterTextChanged(Editable s) {
                //blank
            }
        });


}

По моему мнению, это больше оптимизирует ответ @dkarmazi

public class UploadPhotoAdapter extends RecyclerView.Adapter<UploadPhotoAdapter.MyViewHolder> {
        ArrayList<Feed> feeds;
        Activity activity;
        public UploadPhotoAdapter(Activity activity, ArrayList<Feed> feeds) {
            this.feeds = feeds;
            this.activity = activity;
        }

        @Override
        public UploadPhotoAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.upload_feeds_recycler, parent, false);
            return new UploadPhotoAdapter.MyViewHolder(itemView);
        }

        @Override
        public void onBindViewHolder(final UploadPhotoAdapter.MyViewHolder holder, int position) {
            Feed feed = feeds.get(holder.getAdapterPosition());
            holder.captionEditText.setText(feed.getDescription());
            holder.captionEditText.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    feeds.get(holder.getAdapterPosition()).setDescription(s.toString());
                }
                @Override
                public void afterTextChanged(Editable s) {}
            });
        }
        @Override
        public int getItemCount() {
            return feeds.size();
        }

        public class MyViewHolder extends RecyclerView.ViewHolder {
            EditText captionEditText;
            public MyViewHolder(View view) {
                super(view);
                captionEditText = (EditText) view.findViewById(R.id.captionEditText);
            }
        }

    }

Переопределите метод onViewRecycled в адаптере RecyclerView следующим образом:

@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
    mDataSet[holder.getAdapterPosition()] = holder.mEditText.getText().toString();
}

EditText#setTextкопирует строку вEditableкоторый можно прочитать, вызвавgetText()Прямо вперед. Таким образом, вы можете создать альтернативный редактируемый набор данных:

      fun onBindViewHolder(…) {
    holder.editText.setText(editables[position] ?: texts[position])
    editables[position] = holder.editText.text
}

Сейчасeditablesвсегда будет содержать актуальные данные.

Вот полный образец , который я написал.

Проблема в том, что вы «добавляете» слушателя, а не «устанавливаете» слушателя. Поэтому, когда представление повторно используется, у некоторых EditText будет более 1 слушателя.

Чтобы решить эту проблему, следует очистить слушателей при повторном просмотре.

Шаг 1

используйте собственную замену EditText.

      public class RecyclerViewEditText extends AppCompatEditText {

    private ArrayList<TextWatcher> mListeners = null;

    public RecyclerViewEditText(@NonNull Context context) {
        super(context);
    }

    public RecyclerViewEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RecyclerViewEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void addTextChangedListener(TextWatcher watcher)
    {
        if (mListeners == null)
        {
            mListeners = new ArrayList<>();
        }
        mListeners.add(watcher);

        super.addTextChangedListener(watcher);
    }

    @Override
    public void removeTextChangedListener(TextWatcher watcher)
    {
        if (mListeners != null)
        {
            int i = mListeners.indexOf(watcher);
            if (i >= 0)
            {
                mListeners.remove(i);
            }
        }

        super.removeTextChangedListener(watcher);
    }

    public void clearTextChangedListeners()
    {
        if(mListeners != null)
        {
            for(TextWatcher watcher : mListeners)
            {
                super.removeTextChangedListener(watcher);
            }

            mListeners.clear();
            mListeners = null;
        }
    }
}

шаг 2

замените EditText на приведенное выше в xml.

          <com.xxxxxxxx.widget.RecyclerViewEditText
        android:id="@+id/et_remark"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:background="@null"
        android:gravity="end"
        android:hint="input hint"
        android:inputType="textPersonName" />

шаг 3

при просмотре переработанного, очистите всех слушателей в вашем адаптере.

      @Override
public void onViewRecycled(@NonNull ViewHolder holder) {
    holder.et_remark.clearTextChangedListeners();
    super.onViewRecycled(holder);
}

см. /questions/42921131/kak-udalit-vseh-slushatelej-dobavlennyih-s-pomoschyu-addtextchangedlistener/42921147#42921147

Привет @mikwee убедитесь, что вы добавляете прослушиватель с измененным текстом в метод ниже, а не добавляете его в onBindViewHolder().

public ViewHolder(View v) {
        super(v);

  yourEditText.addTextChangedListener(new TextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start,
                int before, int count) {
            //setting data to array, when changed
            texts [position] = s.toString();
        }

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

        }

        @Override
        public void afterTextChanged(Editable s) {

        }
    });


}

Я переопределил метод getItemViewType решить для меня

override fun getItemViewType(position: Int): Int {
    return position
}

Для меня вышеуказанные решения не сработали. Для некоторых из них слушатель не вызывал, и когда слушатель был вызван в методе onBindViewHolder, кажется, даже при прокрутке вызываются события слушателя. "Текст" меняется, поэтому я попробовал ключевой слушатель, и он работал нормально. Думаю, во время прокрутки не нажимаются никакие клавиши.

holder.ticketNo.setOnKeyListener(new View.OnKeyListener() {
                @Override
                public boolean onKey(View v, int keyCode, KeyEvent event) {
                    results.get(position).TicketNo = holder.ticketNo.getText().toString();
                    return false;
                }
            });

Код, который работал у меня.

Я думаю, что это самое простое решение. ТолькоsetOnFocusChangeListenerв ваш EditText и добавьте/удалитеTextWatcherсоответственно:

      @Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    // Initialize the TextWatcher when the ViewHolder is bound.
    // You need to make sure you don't add a TextWatcher to an EditText that already has one.

    holder.editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            EditText editText = (EditText) v;

            if (hasFocus) {
                // Add the TextWatcher when the EditText has focus
                TextWatcher textWatcher = new TextWatcher() {
                    @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 void afterTextChanged(Editable s) {
                        // ...
                    }
                };
                
                editText.addTextChangedListener(textWatcher);
                editText.setTag(textWatcher);  // Store the TextWatcher as a tag so you can remove it later.
            } else {
                // Remove the TextWatcher when the EditText loses focus
                TextWatcher textWatcher = (TextWatcher) editText.getTag();
                if (textWatcher != null) {
                    editText.removeTextChangedListener(textWatcher);
                }
            }
        }
    });

}

Я использовал свойство тега, чтобы сохранить прошлый наблюдатель и иметь возможность удалить его:

       editText.tag?.let {
      editText.removeTextChangedListener(it as TextWatcher)
 }

object:TextWatcherAdapter() {
       override fun afterTextChanged(s: Editable) {
               // do something with text
       }
}.also { 
       editText.addTextChangedListener(it)
       editText.tag = it
}
            

Реализует View.OnFocusChangeListener

@Override
public void onBindViewHolder(Viewholder holder, int position) {
        editText.setText(model.getValue());
        editText.setOnFocusChangeListener(this);
        editText.setTag(position);
}

@Override
public void onFocusChange(View v, boolean hasFocus) {
    if (v.getTag() == null)
        return;
    int position = (int) v.getTag();
    if (!hasFocus && v instanceof EditText)
        mValues.get(position).setValue(((EditText) v).getText().toString());
}

Я не очень знаком с объектами RecyclerView, но у меня была та же проблема с ListView. Для них я обычно создаю специальный класс, представляющий значения, вставленные в мои представления (он работает с EditTexts, Checkboxes, RadioButtons...), и через них получаю обновленные данные. Затем я создаю пользовательский ArrayAdapter, состоящий из указанных объектов-контейнеров, извлекая из них значения, которые они помещают в edittexts при каждом обратном вызове getView(), и реализую textwatcher, чтобы поддерживать эти объекты в актуальном состоянии. Опять же, я точно не помню, как работают RecyclerViews, но если они используют адаптеры, это может быть хорошим взломом для вас.

Используйте двустороннюю привязку при работе с привязкой данных

https://medium.com/swlh/how-data-binding-helps-you-when-working-with-edittext-inside-recyclerview-543a1eb5f2cc

Просто переопределите метод ниже, чтобы решить проблему:

@Override
public int getItemViewType(final int position) {
    return position;
}
Другие вопросы по тегам