Как сохранить позицию прокрутки RecyclerView с помощью RecyclerView.State?

У меня есть вопрос о Android RecyclerView.State.

Я использую RecyclerView, как я могу использовать и связать его с RecyclerView.State?

Моя цель - сохранить позицию прокрутки в RecyclerView.

20 ответов

Решение

Как вы планируете сохранить последнюю сохраненную позицию с RecyclerView.State?

Вы всегда можете положиться на хорошее состояние сохранения. простираться RecyclerView и переопределить onSaveInstanceState() и onRestoreInstanceState():

@Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        LayoutManager layoutManager = getLayoutManager();
        if(layoutManager != null && layoutManager instanceof LinearLayoutManager){
            mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        }
        SavedState newState = new SavedState(superState);
        newState.mScrollPosition = mScrollPosition;
        return newState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if(state != null && state instanceof SavedState){
            mScrollPosition = ((SavedState) state).mScrollPosition;
            LayoutManager layoutManager = getLayoutManager();
            if(layoutManager != null){
              int count = layoutManager.getChildCount();
              if(mScrollPosition != RecyclerView.NO_POSITION && mScrollPosition < count){
                  layoutManager.scrollToPosition(mScrollPosition);
              }
            }
        }
    }

    static class SavedState extends android.view.View.BaseSavedState {
        public int mScrollPosition;
        SavedState(Parcel in) {
            super(in);
            mScrollPosition = in.readInt();
        }
        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(mScrollPosition);
        }
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

Если вы используете LinearLayoutManager, он поставляется с предварительно созданным API сохранения linearLayoutManagerInstance.onSaveInstanceState() и восстановления API linearLayoutManagerInstance.onRestoreInstanceState (...)

Таким образом, вы можете сохранить возвращенный товар в вашем outState. например,

outState.putParcelable("KeyForLayoutManagerState", linearLayoutManagerInstance.onSaveInstanceState());

и восстановите позицию восстановления с сохраненным вами состоянием. например,

Parcelable state = savedInstanceState.getParcelable("KeyForLayoutManagerState");
linearLayoutManagerInstance.onRestoreInstanceState(state);

В итоге ваш окончательный код будет выглядеть примерно так:

private static final String BUNDLE_RECYCLER_LAYOUT = "classname.recycler.layout";

/**
 * This is a method for Fragment. 
 * You can do the same in onCreate or onRestoreInstanceState
 */
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);

    if(savedInstanceState != null)
    {
        Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
}

Изменить: Вы также можете использовать тот же API-интерфейс с GridLayoutManager, так как это подкласс LinearLayoutManager. Спасибо @wegsehen за предложение.

Редактировать: Помните, что если вы также загружаете данные в фоновом потоке, вам нужно будет вызвать onRestoreInstanceState в вашем методе onPostExecute/onLoadFinished для позиции, которая будет восстановлена ​​после изменения ориентации, например

@Override
protected void onPostExecute(ArrayList<Movie> movies) {
    mLoadingIndicator.setVisibility(View.INVISIBLE);
    if (movies != null) {
        showMoviePosterDataView();
        mDataAdapter.setMovies(movies);
      mRecyclerView.getLayoutManager().onRestoreInstanceState(mSavedRecyclerLayoutState);
        } else {
            showErrorMessage();
        }
    }

Хранить

lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();

Восстановить

((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);

и если это не сработает, попробуйте

((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)

Положить магазин в onPause()и восстановить в onResume()

Я хотел сохранить положение прокрутки в Recycler View, когда отошел от своего списка активности, а затем нажал кнопку "Назад", чтобы вернуться назад. Многие решения, предоставленные для этой проблемы, были либо намного сложнее, чем нужно, либо не работали для моей конфигурации, поэтому я решил поделиться своим решением.

Сначала сохраните свое состояние экземпляра в onPause, как показали многие. Я думаю, что здесь стоит подчеркнуть, что эта версия onSaveInstanceState является методом из класса RecyclerView.LayoutManager.

private LinearLayoutManager mLayoutManager;
Parcelable state;

        @Override
        public void onPause() {
            super.onPause();
            state = mLayoutManager.onSaveInstanceState();
        }

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

private void someMethod() {
    mVenueRecyclerView.setAdapter(mVenueAdapter);
    mLayoutManager.onRestoreInstanceState(state);
}

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

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

  • менеджер макета прикреплен к RecyclerView
  • adpater прикреплен к RecyclerView

Обычно вы устанавливаете менеджер макета в XML, поэтому все, что вам нужно сделать сейчас, это

  1. Загрузите адаптер с данными
  2. Затем прикрепите адаптер к RecyclerView

Вы можете сделать это в любое время, это не обязательно Fragment.onCreateView или же Activity.onCreate,

Пример. Убедитесь, что адаптер подключен при каждом обновлении данных.

viewModel.liveData.observe(this) {
    // Load adapter.
    adapter.data = it

    if (list.adapter != adapter) {
        // Only set the adapter if we didn't do it already.
        list.adapter = adapter
    }
}

Сохраненная позиция прокрутки рассчитывается по следующим данным:

  • Положение адаптера первого элемента на экране
  • Пиксельное смещение верхней части элемента от верхней части списка (для вертикального расположения)

Поэтому вы можете ожидать небольшое несоответствие, например, если ваши предметы имеют разные размеры в портретной и альбомной ориентации.

Я устанавливаю переменные в onCreate(), сохраняю позицию прокрутки в onPause() и устанавливаю позицию прокрутки в onResume()

public static int index = -1;
public static int top = -1;
LinearLayoutManager mLayoutManager;

@Override
public void onCreate(Bundle savedInstanceState)
{
    //Set Variables
    super.onCreate(savedInstanceState);
    cRecyclerView = ( RecyclerView )findViewById(R.id.conv_recycler);
    mLayoutManager = new LinearLayoutManager(this);
    cRecyclerView.setHasFixedSize(true);
    cRecyclerView.setLayoutManager(mLayoutManager);

}

@Override
public void onPause()
{
    super.onPause();
    //read current recyclerview position
    index = mLayoutManager.findFirstVisibleItemPosition();
    View v = cRecyclerView.getChildAt(0);
    top = (v == null) ? 0 : (v.getTop() - cRecyclerView.getPaddingTop());
}

@Override
public void onResume()
{
    super.onResume();
    //set recyclerview position
    if(index != -1)
    {
        mLayoutManager.scrollToPositionWithOffset( index, top);
    }
}

Начиная с версии 1.2.0-alpha02 библиотеки androidx recyclerView, она теперь управляется автоматически. Просто добавьте его с помощью:

implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"

И используйте:

adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY

Перечисление StateRestorationPolicy имеет 3 варианта:

  • РАЗРЕШИТЬ - состояние по умолчанию, которое восстанавливает состояние RecyclerView немедленно, на следующем проходе макета.
  • PREVENT_WHEN_EMPTY - восстанавливает состояние RecyclerView, только когда адаптер не пуст (adapter.getItemCount() > 0). Если ваши данные загружаются асинхронно, RecyclerView ждет, пока данные будут загружены, и только после этого состояние восстанавливается. Если у вас есть элементы по умолчанию, такие как заголовки или индикаторы выполнения загрузки, как часть вашего адаптера, вам следует использовать параметр PREVENT, если элементы по умолчанию не добавлены с помощью MergeAdapter. MergeAdapter ожидает готовности всех своих адаптеров и только после этого восстанавливает состояние.
  • PREVENT - восстановление любого состояния откладывается до тех пор, пока вы не установите ALLOW или PREVENT_WHEN_EMPTY.

Обратите внимание, что на момент этого ответа библиотека recyclerView все еще находится в alpha03, но альфа-фаза не подходит для производственных целей.

Вам больше не нужно спасать и восстанавливать состояние самостоятельно. Если вы установите уникальный идентификатор в xml и recyclerView.setSaveEnabled(true) (по умолчанию true), система сделает это автоматически. Вот больше об этом: http://trickyandroid.com/saving-android-view-state-correctly/

Поскольку я считаю, что большинство параметров слишком длинные или сложные, вот мой короткий вариант Kotlin с viewBinding и viewModel для достижения этой цели:

Если вы хотите, чтобы ваш жизненный цикл состояния RecyclerView был осведомлен, поместите этот код в свою ViewModel, в противном случае вы можете использовать базовый репозиторий и работать оттуда:

      private lateinit var state: Parcelable
fun saveRecyclerViewState(parcelable: Parcelable) { state = parcelable }
fun restoreRecyclerViewState() : Parcelable = state
fun stateInitialized() : Boolean = ::state.isInitialized

и это внутри вашего onPause()

      binding.recyclerView.layoutManager?.onSaveInstanceState()?.let { viewModel.saveRecyclerViewState(it) }

и, наконец, это в вашем onResume ()

      if (viewModel.stateInitialized()) {
        binding.recyclerView.layoutManager?.onRestoreInstanceState(
            viewModel.restoreRecyclerViewState()
        )
    }

Наслаждаться :)

У меня было такое же требование, но решения здесь не совсем помогли мне, из-за источника данных для recyclerView.

Я извлекал состояние LinearLayoutManager из RecyclerViews в onPause ()

private Parcelable state;

Public void onPause() {
    state = mLinearLayoutManager.onSaveInstanceState();
}

Parcelable state сохраняется в onSaveInstanceState(Bundle outState)и извлекли снова в onCreate(Bundle savedInstanceState), когда savedInstanceState != null,

Однако адаптер recyclerView заполняется и обновляется объектом ViewModel LiveData, возвращаемым вызовом DAO для базы данных Room.

mViewModel.getAllDataToDisplay(mSelectedData).observe(this, new Observer<List<String>>() {
    @Override
    public void onChanged(@Nullable final List<String> displayStrings) {
        mAdapter.swapData(displayStrings);
        mLinearLayoutManager.onRestoreInstanceState(state);
    }
});

В конце концов я обнаружил, что восстановление состояния экземпляра непосредственно после установки данных в адаптере сохранит состояние прокрутки при поворотах.

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

Если данные адаптера доступны напрямую (т.е. не являются конфликтующими при вызове базы данных), то восстановление состояния экземпляра в LinearLayoutManager может быть выполнено после того, как адаптер установлен в recyclerView.

Различие между этими двумя сценариями держало меня целую вечность.

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

android:configChanges="orientation|screenSize"

Затем в вашем фрагменте объявите эти методы экземпляра

private  Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;
private LinearLayoutManager mLinearLayoutManager;

Добавьте приведенный ниже код в ваш метод @onPause

@Override
public void onPause() {
    super.onPause();
    //This used to store the state of recycler view
    mBundleRecyclerViewState = new Bundle();
    mListState =mTrailersRecyclerView.getLayoutManager().onSaveInstanceState();
    mBundleRecyclerViewState.putParcelable(getResources().getString(R.string.recycler_scroll_position_key), mListState);
}

Добавьте метод onConfigartionChanged, как показано ниже.

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    //When orientation is changed then grid column count is also changed so get every time
    Log.e(TAG, "onConfigurationChanged: " );
    if (mBundleRecyclerViewState != null) {
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                mListState = mBundleRecyclerViewState.getParcelable(getResources().getString(R.string.recycler_scroll_position_key));
                mTrailersRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);

            }
        }, 50);
    }
    mTrailersRecyclerView.setLayoutManager(mLinearLayoutManager);
}

Этот подход решит вашу проблему. если у вас есть другой менеджер компоновки, просто замените верхний менеджер макета

На Android API Level 28 я просто проверяю, LinearLayoutManager а также RecyclerView.Adapter в моем Fragment#onCreateView метод, и все просто работал ™️. Мне не нужно было ничего делать onSaveInstanceState или же onRestoreInstanceState Работа.

Ответ Евгена Печанца объясняет, почему это работает.

Вот как я восстанавливаю позицию RecyclerView с помощью GridLayoutManager после поворота, когда вам нужно перезагрузить данные из Интернета с помощью AsyncTaskLoader.

Создайте глобальную переменную Parcelable и GridLayoutManager и статическую итоговую строку:

private Parcelable savedRecyclerLayoutState;
private GridLayoutManager mGridLayoutManager;
private static final String BUNDLE_RECYCLER_LAYOUT = "recycler_layout";

Сохранить состояние gridLayoutManager в onSaveInstance()

 @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, 
            mGridLayoutManager.onSaveInstanceState());
        }

Восстановить в onRestoreInstanceState

@Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);

        //restore recycler view at same position
        if (savedInstanceState != null) {
            savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        }
    }

Затем, когда загрузчик извлекает данные из интернета, вы восстанавливаете позицию повторного просмотра в onLoadFinished ()

if(savedRecyclerLayoutState!=null){
                mGridLayoutManager.onRestoreInstanceState(savedRecyclerLayoutState);
            }

... конечно, вы должны создать экземпляр gridLayoutManager внутри onCreate. ура

Вы можете использовать adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY который вводится в recyclerview:1.2.0-alpha02

https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334

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

Или вы можете использовать ViewModel с участием SavedStateHandleкоторый работает для внутренних RecyclerViews, поворота экрана и смерти процесса.

Создать ViewModel с участием saveStateHandle

val scrollState=
    savedStateHandle.getLiveData<Parcelable?>(KEY_LAYOUT_MANAGER_STATE)

использовать Parcelable scrollState для сохранения и восстановления состояния, как указано в других сообщениях, или путем добавления прослушивателя прокрутки в RecyclerView и

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
               scrollState.value = mLayoutManager.onSaveInstanceState()
            }
        }

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

У меня была та же проблема с небольшим отличием, я использовал привязку данных и настраивал адаптер recyclerView и layoutManager в методах привязки.

Проблема заключалась в том, что привязка данных происходила после onResume, поэтому он сбрасывал мой layoutManager после onResume, и это означает, что он каждый раз прокручивал обратно в исходное место. Поэтому, когда я прекратил устанавливать layoutManager в методах адаптера Databinding, он снова работает нормально.

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

Требования к моему проекту: у меня есть RecyclerView, в котором перечислены некоторые интерактивные элементы в моей основной деятельности (Activity-A). При щелчке по элементу отображается новое действие с деталями элемента (действие-B).

Я реализовал в файле манифеста, что Activity-A является родителем Activity-B, таким образом, у меня есть кнопка "Назад" или "Домой" на ActionBar Activity-B

Проблема: Каждый раз, когда я нажимал кнопку "Назад" или "Домой" на панели действий Activity-B, Activity-A переходит в полный жизненный цикл активности, начиная с onCreate()

Несмотря на то, что я реализовал onSaveInstanceState(), сохраняющий список адаптера RecyclerView, когда Activity-A запускает свой жизненный цикл, saveInstanceState всегда имеет значение null в методе onCreate().

Продолжая копаться в интернете, я столкнулся с той же проблемой, но человек заметил, что кнопка "Назад" или "Домой" под устройством Anroid (кнопка "Назад / Домой по умолчанию"), Activity-A не входит в жизненный цикл Activity.

Решение:

  1. Я удалил тег для родительской активности в манифесте для Activity-B
  2. Я включил кнопку домой или назад на Activity-B

    Под методом onCreate() добавьте эту строку supportActionBar?.SetDisplayHomeAsUpEnabled(true)

  3. В методе overide fun onOptionsItemSelected() я проверил для item.ItemId, по которому элемент нажимается на основе идентификатора. Идентификатор кнопки возврата

    android.R.id.home

    Затем реализуйте вызов функции finish() внутри android.R.id.home.

    Это завершит деятельность-B и принесет Acitivy-A, не пройдя весь жизненный цикл.

По моим требованиям это пока лучшее решение.

     override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_show_project)

    supportActionBar?.title = projectName
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

}


 override fun onOptionsItemSelected(item: MenuItem?): Boolean {

    when(item?.itemId){

        android.R.id.home -> {
            finish()
        }
    }

    return super.onOptionsItemSelected(item)
}

Альтернативный способ смотреть это ,

Прежде всего, определите переменные,

      private LinearLayoutManager mLayoutManager;
int lastPosition;

public static final String MyPREFERENCES__DISPLAY = "MyPrefs_display" ;
SharedPreferences sharedpreferences_display;
SharedPreferences.Editor editor_display;

тогда,

      mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);

recyclerView.scrollToPosition(sharedpreferences_display.getInt("last_saved_position", 0));

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        lastPosition = mLayoutManager.findFirstVisibleItemPosition();
    }
});

если вы хотите сохранить позицию по кнопке,

      btn.setOnClickListener(v -> {
    editor_display.putInt("last_saved_position", lastPosition).apply();
    Toast.makeText(Arama_Fav_Activity.this, "Saved", Toast.LENGTH_SHORT).show();
});

В моем случае я устанавливал RecyclerView's layoutManager как в XML, так и в onViewCreated. Удаление задания вonViewCreated починил это.

with(_binding.list) {
//  layoutManager =  LinearLayoutManager(context)
    adapter = MyAdapter().apply {
        listViewModel.data.observe(viewLifecycleOwner,
            Observer {
                it?.let { setItems(it) }
            })
    }
}

Activity.java:

public RecyclerView.LayoutManager mLayoutManager;

Parcelable state;

    mLayoutManager = new LinearLayoutManager(this);


// Inside `onCreate()` lifecycle method, put the below code :

    if(state != null) {
    mLayoutManager.onRestoreInstanceState(state);

    } 

    @Override
    protected void onResume() {
        super.onResume();

        if (state != null) {
            mLayoutManager.onRestoreInstanceState(state);
        }
    }   

   @Override
    protected void onPause() {
        super.onPause();

        state = mLayoutManager.onSaveInstanceState();

    }   

Почему я использую OnSaveInstanceState() в onPause() означает, что при переключении на другое действие будет вызываться onPause. Оно сохранит эту позицию прокрутки и восстановит положение, когда мы возвращаемся из другого действия.

Другие вопросы по тегам