Обновление данных в RecyclerView и сохранение их положения прокрутки


98

Как обновить данные, отображаемые в RecyclerView(вызывая notifyDataSetChangedего адаптер), и убедиться, что положение прокрутки сброшено точно туда, где оно было?

В случае старого доброго ListViewвсе, что требуется, - это получение getChildAt(0), проверка getTop()и setSelectionFromTopпоследующий вызов с теми же точными данными.

Это кажется невозможным в случае RecyclerView.

Я предполагаю, что я должен использовать его, LayoutManagerкоторый действительно обеспечивает scrollToPositionWithOffset(int position, int offset), но как правильно получить позицию и смещение?

layoutManager.findFirstVisibleItemPosition()и layoutManager.getChildAt(0).getTop()?

Или есть более элегантный способ выполнить работу?


Речь идет о значениях ширины и высоты вашего RecyclerView или LayoutManager. Проверьте это решение: stackoverflow.com/questions/28993640/…
serefakyuz 06

@Konard, в моем случае у меня есть список аудиофайлов с реализацией Seekbar и работающий со следующей проблемой: 1) Для нескольких последних проиндексированных элементов, как только он запустил notifyItemChanged (pos), он автоматически нажимает представление внизу. 2) При сохранении notifyItemChanged (pos) он застрял в списке и не позволяет легко прокручивать. Хотя я задам вопрос, но дайте мне знать, пожалуйста, если у вас есть лучший подход к решению такой проблемы.
CoDe

Ответы:


146

Я использую этот. ^ _ ^

// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();

// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);

Это проще, надеюсь, вам это поможет!


1
На самом деле это правильный ответ. Сложно только из-за асинхронной загрузки данных, но вы можете отложить восстановление.
EpicPandaForce

идеально +2 именно то, что я искал
Адил Тюрк

@BubbleTree paste.ofcode.org/EEdjSLQbLrcQyxXpBpEQ4m Я пытаюсь реализовать это, но не работает, что я делаю не так?
Kaustubh Bhagwat

@KaustubhBhagwat Возможно, вам стоит проверить "recyclerViewState! = Null" перед его использованием.
DawnYu

4
Где именно я должен использовать этот код? в методе onResume?
Lucky_girl

47

У меня очень похожая проблема. И я придумал следующее решение.

Использование notifyDataSetChanged- плохая идея. Вы должны быть более конкретными, тогда RecyclerViewвы сохраните состояние прокрутки.

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

adapter.notifyItemRangeChanged(0, adapter.getItemCount());

2
Я попробовал adapter.notifyItemRangeChanged (0, adapter.getItemCount ());, который тоже работает как .notifyDataSetChanged ()
Mani

4
В моем случае это помогло. Я вставлял несколько элементов в позицию 0, и notifyDataSetChangedпозиция прокрутки изменилась, показывая новые элементы. Однако, если использовать сохраняет состояние прокрутки. notifyItemRangeInserted(0, newItems.size() - 1)RecyclerView
Карлос,

очень, очень хороший ответ, я искал решение этого ответа целый день. большое спасибо ..
Гунду Бандгар

адаптер.notifyItemRangeChanged (0, адаптер.getItemCount ()); следует избегать, как указано в последнем вводе-выводе Google (посмотрите презентацию о включении и выводе recyclerview), было бы лучше (если у вас есть знания) сообщить recyclerview, какие именно элементы были изменены, вместо плоского комплексного обновления всех просмотров.
A. Steenbergen

3
он не работает, recyclerview обновляется вверх, хотя я добавил
Shaahul

21

РЕДАКТИРОВАТЬ: Чтобы восстановить точно такое же видимое положение, как в, чтобы оно выглядело точно так, как оно было, нам нужно сделать что-то немного другое (см. Ниже, как восстановить точное значение scrollY):

Сохраните положение и смещение следующим образом:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
int firstItem = manager.findFirstVisibleItemPosition();
View firstItemView = manager.findViewByPosition(firstItem);
float topOffset = firstItemView.getTop();

outState.putInt(ARGS_SCROLL_POS, firstItem);
outState.putFloat(ARGS_SCROLL_OFFSET, topOffset);

А затем восстановите свиток вот так:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
manager.scrollToPositionWithOffset(mStatePos, (int) mStateOffset);

Это восстанавливает список в его точное видимое положение. Очевидно, потому что он будет выглядеть одинаково для пользователя, но у него не будет такого же значения scrollY (из-за возможных различий в размерах макета альбомной / книжной ориентации).

Обратите внимание, что это работает только с LinearLayoutManager.

--- Ниже показано, как восстановить точный scrollY, который, вероятно, изменит внешний вид списка ---

  1. Примените OnScrollListener следующим образом:

    private int mScrollY;
    private RecyclerView.OnScrollListener mTotalScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            mScrollY += dy;
        }
    };
    

При этом в mScrollY всегда будет сохраняться точное положение прокрутки.

  1. Сохраните эту переменную в своем Bundle и восстановите ее при восстановлении состояния в другую переменную , мы назовем ее mStateScrollY.

  2. После восстановления состояния и после того, как ваш RecyclerView сбросил все свои данные, сбросьте прокрутку следующим образом:

    mRecyclerView.scrollBy(0, mStateScrollY);
    

Вот и все.

Помните, что вы восстанавливаете прокрутку для другой переменной, это важно, потому что OnScrollListener будет вызываться с помощью .scrollBy () и впоследствии установит mScrollY на значение, хранящееся в mStateScrollY. Если вы этого не сделаете, mScrollY будет иметь двойное значение прокрутки (потому что OnScrollListener работает с дельтами, а не с абсолютной прокруткой).

Экономия состояния в действиях может быть достигнута так:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(ARGS_SCROLL_Y, mScrollY);
}

И для восстановления вызовите это в своем onCreate ():

if(savedState != null){
    mStateScrollY = savedState.getInt(ARGS_SCROLL_Y, 0);
}

Сохранение состояния во фрагментах работает аналогичным образом, но фактическое сохранение состояния требует немного дополнительной работы, но есть много статей, посвященных этому, поэтому у вас не должно возникнуть проблем с выяснением того, как, принципы сохранения scrollY и восстановление остается прежним.


я не понимаю, вы можете опубликовать полный пример?

На самом деле это как можно ближе к полному примеру, если вы не хотите, чтобы я публиковал всю свою деятельность
А. Стинберген,

Я чего-то не понимаю. Если в начале списка были добавлены элементы, не будет ли неправильным оставаться в той же позиции прокрутки, поскольку теперь вы действительно будете смотреть на разные элементы, которые заняли эту область?
разработчик Android

Да, в этом случае вам нужно отрегулировать положение при восстановлении, однако это не часть вопроса, поскольку обычно при повороте вы не добавляете и не удаляете элементы. Это было бы странным поведением и, вероятно, не в интересах пользователя, за исключением некоторых особых случаев, которые могут существовать, которые я, честно говоря, не могу представить.
A. Steenbergen

9

Да, вы можете решить эту проблему, создав конструктор адаптера только один раз, здесь я объясняю часть кодирования:

if (appointmentListAdapter == null) {
        appointmentListAdapter = new AppointmentListAdapter(AppointmentsActivity.this);
        appointmentListAdapter.addAppointmentListData(appointmentList);
        appointmentListAdapter.setOnStatusChangeListener(onStatusChangeListener);
        appointmentRecyclerView.setAdapter(appointmentListAdapter);

    } else {
        appointmentListAdapter.addAppointmentListData(appointmentList);
        appointmentListAdapter.notifyDataSetChanged();
    }

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

Если адаптер не равен нулю, то я уверен, что я инициализировал свой адаптер хотя бы один раз.

Поэтому я просто добавлю список в адаптер и вызову notifydatasetchanged.

RecyclerView всегда содержит последнюю прокрученную позицию, поэтому вам не нужно сохранять последнюю позицию, просто вызовите notifydatasetchanged, просмотр recycler всегда обновляет данные, не переходя наверх.

Спасибо, удачного кодирования


Я думаю, что это соответствует принятому ответу @Konrad Morawski
Jithin Jude

6

Сохраняйте позицию прокрутки, используя ответ @DawnYu, чтобы обернуть его notifyDataSetChanged()следующим образом:

val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() 
adapter.notifyDataSetChanged() 
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)

идеально ... лучший ответ
jlandyr

Давно искал этот ответ. Спасибо большое.
Алаа АбуЗарифа,

2

Верхний ответ от @DawnYu работает, но recyclerview сначала прокрутится вверх, а затем вернется к предполагаемой позиции прокрутки, вызывая реакцию «мерцание», что не очень приятно.

Чтобы обновить recyclerView, особенно после выхода из другого действия, без мерцания и сохранения положения прокрутки, вам необходимо сделать следующее.

  1. Убедитесь, что вы обновляете представление ресайклера с помощью DiffUtil. Подробнее об этом читайте здесь: https://www.journaldev.com/20873/android-recyclerview-diffutil.
  2. При возобновлении вашей активности или в момент, когда вы хотите обновить свою активность, загрузите данные в ваш recyclerview. Используя diffUtil, в recyclerview будут производиться только обновления, сохраняя его позицию.

Надеюсь это поможет.


1

Я не использовал Recyclerview, но сделал это в ListView. Пример кода в Recyclerview:

setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        rowPos = mLayoutManager.findFirstVisibleItemPosition();

Это слушатель, когда пользователь прокручивает. Накладные расходы на производительность незначительны. Таким образом, первая видимая позиция будет точной.


Хорошо, findFirstVisibleItemPositionвозвращает «позицию адаптера первого видимого вида», но как насчет смещения.
Конрад Моравски

1
Есть ли аналогичный метод для Recyclerview для setSelection метода ListView для видимой позиции? Это то, что я сделал для ListView.
Оригинальный Android

1
Как насчет использования метода setScrollY с параметром dy, значение, которое вы сохраните? Если сработает, обновлю свой ответ. Я не использовал Recyclerview, но хочу использовать его в своем будущем приложении.
Оригинальный Android,

См. Мой ответ ниже о том, как сохранить позицию прокрутки.
A. Steenbergen

1
Хорошо, я учту это в следующий раз, я не злонамеренно отрицал тебя. Однако я не согласен с короткими и неполными ответами, потому что, когда я начинал, короткие ответы, в которых другие разработчики предполагали, что много справочной информации, мне не очень помогли, потому что они часто были слишком неполными, и мне приходилось задавать много уточняющих вопросов, что вы обычно не может работать со старыми сообщениями stackoverflow. Также обратите внимание на то, что Конрад не принял никакого ответа, что означает, что эти ответы не решили его проблему. Хотя я понимаю вашу общую мысль.
A. Steenbergen

-1
 mMessageAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            mLayoutManager.smoothScrollToPosition(mMessageRecycler, null, mMessageAdapter.getItemCount());
        }
    });

Решение здесь - продолжать прокрутку recyclerview, когда приходит новое сообщение.

Метод onChanged () обнаруживает действие, выполняемое с recyclerview.


-1

У меня это работает в Котлине.

  1. Создайте адаптер и передайте свои данные в конструктор
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>()  { ... }
  1. измените это свойство и вызовите notifyDataSetChanged ()
adapter.currentPole = pole
adapter.notifyDataSetChanged()

Смещение прокрутки не меняется.


-2

У меня была эта проблема со списком элементов, у каждого из которых было время в минутах, пока они не были «выполнены» и нуждались в обновлении. Я бы обновил данные, а затем позвонил

orderAdapter.notifyDataSetChanged();

и каждый раз она прокручивалась вверх. Я заменил это на

 for(int i = 0; i < orderArrayList.size(); i++){
                orderAdapter.notifyItemChanged(i);
            }

и это было нормально. Ни один из других методов в этой теме не помог мне. Однако при использовании этого метода каждый отдельный элемент мигал при его обновлении, поэтому мне также пришлось поместить это в родительский фрагмент onCreateView.

RecyclerView.ItemAnimator animator = orderRecycler.getItemAnimator();
    if (animator instanceof SimpleItemAnimator) {
        ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
    }

-2

Если у вас есть один или несколько EditTexts внутри элементов recyclerview, отключите их автофокус, поместив эту конфигурацию в родительский вид recyclerview:

android:focusable="true"
android:focusableInTouchMode="true"

У меня была эта проблема, когда я начал другое действие, запущенное из элемента recyclerview, когда я вернулся и установил обновление одного поля в одном элементе с помощью notifyItemChanged (position), прокрутка RV перемещается, и я пришел к выводу, что автофокус EditText Элементы, приведенный выше код решил мою проблему.

Лучший.


-3

Просто вернитесь, если oldPosition и position совпадают;

private int oldPosition = -1;

public void notifyItemSetChanged(int position, boolean hasDownloaded) {
    if (oldPosition == position) {
        return;
    }
    oldPosition = position;
    RLog.d(TAG, " notifyItemSetChanged :: " + position);
    DBMessageModel m = mMessages.get(position);
    m.setVideoHasDownloaded(hasDownloaded);
    notifyItemChanged(position, m);
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.