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


108

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

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

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

Ответы:


37

Как вы планируете сохранять последнюю сохраненную позицию с помощью 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.getItemCount();
              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];
            }
        };
    }

1
теперь у меня другой вопрос, как, нет, что умеет RecyclerView.State?
陈文涛

4
это действительно работает? Я пробовал это, но получаю такое исключение во время выполнения: MyRecyclerView $ SavedState не может быть преобразован в android.support.v7.widget.RecyclerView $ SavedState
Daivid

2
В состоянии экземпляра восстановления нам нужно передать superState его суперклассу (представлению recytcler) следующим образом: super.onRestoreInstanceState (((ScrollPositionSavingRecyclerView.SavedState) state) .getSuperState ());
micnoy

5
Это не сработает, если вы расширите RecyclerView, вы получите BadParcelException.
EpicPandaForce

1
В этом нет необходимости: если вы используете что-то вроде LinearLayoutManager / GridLayoutManager, "позиция прокрутки" уже автоматически сохраняется для вас. (Это mAnchorPosition в LinearLayoutManager.SavedState). Если вы этого не видите, значит, что-то не так с тем, как вы повторно применяете данные.
Брайан Йенчо

230

Обновить

Введено начиная с recyclerview:1.2.0-alpha02релиза StateRestorationPolicy. Это мог бы быть лучший подход к данной проблеме.

Эта тема была рассмотрена в средней статье разработчиков Android .

Кроме того, @ rubén-viguera поделился более подробной информацией в ответе ниже. https://stackoverflow.com/a/61609823/892500

Старый ответ

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

При этом вы можете сохранить возвращенную посылку в своем другом штате. например,

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();
        }
    }

21
Очевидно, это лучший ответ, почему он не принимается? Обратите внимание, что это есть в любом LayoutManager, а не только в LinearLayoutManager, но и в GridLayoutManager, а если и не в его ложной реализации.
Пол Войташек

linearLayoutManager.onInstanceState () всегда возвращает null -> проверьте исходный код. К сожалению, не работает.
luckyhandler 08

1
Разве RecyclerView не сохраняет состояние своего LayoutManager в собственном onSaveInstanceState? Глядя на
версию

3
Это работает на один , RecyclerViewно не тогда , когда у вас есть ViewPagerс RecycerViewна каждой странице.
Kris B

1
@Ivan, думаю работает во фрагменте (только что проверил), но не в onCreateView, а после загрузки данных. См. Ответ @Farmaker здесь.
CoolMind

81

хранить

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

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

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

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

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

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


Это работает, но вам может потребоваться сбросить его lastFirstVisiblePositionдо 0после восстановления. Этот случай полезен, когда у вас есть фрагмент / действие, которое загружает разные данные в один RecyclerView, например, приложение проводника файлов.
blueware

Превосходно! Он удовлетворяет как возвращение из подробного действия в список, так и сохранение состояния при повороте экрана
Javi

что делать, если мой recyclerView находится внутри SwipeRefreshLayout?
Морозов

2
Почему первый вариант восстановления не работает, а второй работает?
lucidbrot

1
@MehranJalili, похоже, этоRecyclerView
Влад

22

Я хотел сохранить позицию прокрутки 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);
}

1
Это точное решение. : D
Афджалур Рахман Рана

1
для меня onSaveInstanceState не называется, поэтому сохранение состояния в onPause кажется лучшим вариантом. Я также вставляю backstack, чтобы вернуться к фрагменту.
filthy_wizard

для загрузки в гос. что я делаю в onViewCreated. это работает только тогда, когда я в режиме отладки и использую точки останова? или если я добавлю задержку и помещу выполнение в сопрограмму.
filthy_wizard

21

Я устанавливаю переменные в 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);
    }
}

Также сохраните смещение элемента. Ницца. Спасибо!
t0m

14

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


14

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

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

  • менеджер по расположению прикреплен к RecyclerView
  • адаптер прилагается к 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
    }
}

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

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

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


RecyclerView/ ScrollView/ Что нужно иметь android:idдля своего государства , чтобы спастись.


Это было для меня. Я изначально настраивал адаптер, а затем обновлял его данные, когда они доступны. Как объяснялось выше, если оставить его до тех пор, пока не будут доступны данные, он автоматически вернется туда, где он был.
NeilS

Спасибо @Eugen. Есть ли какая-либо документация по сохранению и восстановлению положения прокрутки LayoutManager?
Адам Гурвиц

@AdamHurwitz Какую документацию вы ожидаете? Я описал детали реализации LinearLayoutManager, поэтому прочтите его исходный код.
Eugen Pechanec

1
Вот ссылка на LinearLayoutManager.SavedState: cs.android.com/androidx/platform/frameworks/support/+/…
Eugen Pechanec

Спасибо, @EugenPechanec. Это сработало для меня при вращении устройства. Я также использую нижний вид навигации, и когда я переключаюсь на другую нижнюю вкладку и возвращаюсь, список снова начинается с начала. Есть идеи, почему это так?
schv09

4

Вот мой подход к их сохранению и поддержанию состояния 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);
}

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


Вам может не понадобиться обработчик для сохранения данных. Методы жизненного цикла будет достаточно для сохранения / загрузки.
Mahi

@sayaMahi Вы можете описать как следует. Я также видел, что onConfigurationChanged предотвращает вызов метода уничтожения. так что это неправильное решение.
Паван Кумар Шарма

3

Вот как я восстанавливаю позицию 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);
        }
    }

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

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

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


3

У меня было такое же требование, но решения здесь не совсем помогли мне перейти линию из-за источника данных для 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.

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


3

На уровне 28 Android API я просто проверяю, что настроил свой метод LinearLayoutManagerand RecyclerView.Adapterin my Fragment#onCreateView, и все работает Just Worked ™ ️. Мне не нужно было ничего делать onSaveInstanceStateили onRestoreInstanceStateработать.

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


2

Начиная с версии 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, но альфа-фаза не подходит для производственных целей .


Сэкономлено столько переутомления. В противном случае нам пришлось сохранить индекс кликов в списке, восстановить элементы, предотвратить перерисовку в onCreateView, применить scrollToPosition (или сделать другие ответы), и все же пользователь мог заметить разницу после возвращения
Рахул Кахале

Кроме того, если я хочу сохранить позицию RecyclerView после навигации по приложению ... есть ли для этого какой-нибудь инструмент?
StayCool,

@RubenViguera Почему альфа-версия не предназначена для производственных целей? Я имею в виду, это действительно плохо?
StayCool,

1

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


1

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

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

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

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

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

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

Решение:

  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

    Это завершит Activity-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)
}

1

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

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


как ты этого добился?
Кедар Сукеркар,

0

В моем случае я устанавливал RecyclerView layoutManagerкак в XML, так и в формате onViewCreated. Снятие присвоения onViewCreatedисправлено.

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

-1

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 будет вызываться. Он сохранит эту позицию прокрутки и восстановит позицию, когда мы вернемся из другого действия.


2
Статическое поле Public не подходит. Глобальные данные и синглтоны не годятся.
Уэстон,

@weston Теперь я понял. Я изменил savestate на onpause, чтобы удалить global.
Стив
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.