Como salvar a posição de rolagem do RecyclerView usando RecyclerView.State?


Respostas:


37

Como você planeja salvar a última posição salva com RecyclerView.State?

Você sempre pode contar com o bom estado de salvamento. Estenda RecyclerViewe substitua onSaveInstanceState () e 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
agora eu tenho outra pergunta, como, não, o que o RecyclerView.State pode fazer?
陈文涛

4
isso realmente funciona? Eu tentei isso, mas continuo recebendo uma exceção de tempo de execução como esta: MyRecyclerView $ SavedState não pode ser convertido em android.support.v7.widget.RecyclerView $ SavedState
Daivid

2
No estado da instância de restauração, precisamos passar o superState para sua superclasse (a visão recicladora) assim: super.onRestoreInstanceState (((ScrollPositionSavingRecyclerView.SavedState) state) .getSuperState ());
Micnoy

5
Isso não funciona se você estender o RecyclerView, você obterá BadParcelException.
EpicPandaForce

1
Nada disso é necessário: se você usar algo como LinearLayoutManager / GridLayoutManager, a "posição de rolagem" já será salva automaticamente para você. (É o mAnchorPosition em LinearLayoutManager.SavedState). Se você não está vendo isso, algo está errado com a forma como você está reaplicando os dados.
Brian Yencho

229

Atualizar

A partir do recyclerview:1.2.0-alpha02lançamento StateRestorationPolicyfoi introduzido. Pode ser uma abordagem melhor para o problema em questão.

Este tópico foi abordado no artigo médio de desenvolvedores Android .

Além disso, @ rubén-viguera compartilhou mais detalhes na resposta abaixo. https://stackoverflow.com/a/61609823/892500

Resposta antiga

Se você estiver usando LinearLayoutManager, ele vem com api linearLayoutManagerInstance.onSaveInstanceState () e restore api linearLayoutManagerInstance.onRestoreInstanceState (...)

Com isso, você pode salvar o pacote devolvido em seu outState. por exemplo,

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

e restaure a posição de restauração com o estado que você salvou. por exemplo,

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

Para encerrar, seu código final será semelhante a

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

Editar: Você também pode usar as mesmas apis com o GridLayoutManager, pois é uma subclasse de LinearLayoutManager. Obrigado @wegsehen pela sugestão.

Edit: Lembre-se, se você também estiver carregando dados em um thread de segundo plano, você precisará fazer uma chamada para onRestoreInstanceState dentro de seu método onPostExecute / onLoadFinished para que a posição seja restaurada após a mudança de orientação, por exemplo

@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
Esta é claramente a melhor resposta, por que não é aceita? Observe que qualquer LayoutManager tem isso, não apenas LinearLayoutManager, também GridLayoutManager e, se não, é uma implementação falsa.
Paul Woitaschek

linearLayoutManager.onInstanceState () sempre retorna nulo -> verifique o código-fonte. Infelizmente não está funcionando.
luckyhandler

1
O RecyclerView não salva o estado de seu LayoutManager em seu próprio onSaveInstanceState? Olhando para v24 da biblioteca de suporte ...
androidguy

3
Isso funciona em um único, RecyclerViewmas não se você tiver um ViewPagercom um RecycerViewem cada página.
Kris B de

1
@Ivan, acho que funciona em um fragmento (apenas verificado), mas não em onCreateView, mas após o carregamento dos dados. Veja a resposta de @Farmaker aqui.
CoolMind

81

Loja

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

Restaurar

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

e se isso não funcionar, tente

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

Armazenar onPause() e restauraronResume()


Isso funciona, mas você pode precisar redefinir o lastFirstVisiblePositionpara 0após restaurá-lo. Este caso é útil quando você tem um fragmento / atividade que carrega dados diferentes em um RecyclerView, por exemplo, um aplicativo explorador de arquivos.
blueware de

Excelente! Isso satisfaz tanto voltando de uma atividade de detalhe para a lista, quanto salvando o estado na rotação da tela
Javi

o que fazer se meu recyclerView estiver dentro SwipeRefreshLayout?
Morozov

2
Por que a primeira opção de restauração não funcionaria, mas a segunda funcionaria?
lucidbrot

1
@MehranJalili parece ser umRecyclerView
Vlad

22

Eu queria salvar a posição de rolagem do Recycler View ao navegar para fora da minha atividade de lista e clicar no botão Voltar para navegar de volta. Muitas das soluções fornecidas para esse problema eram muito mais complicadas do que o necessário ou não funcionavam para minha configuração, então pensei em compartilhar minha solução.

Primeiro salve o estado da instância em onPause, como muitos mostraram. Acho que vale a pena enfatizar aqui que esta versão de onSaveInstanceState é um método da classe RecyclerView.LayoutManager.

private LinearLayoutManager mLayoutManager;
Parcelable state;

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

A chave para fazer isso funcionar corretamente é certificar-se de chamar onRestoreInstanceState depois de conectar seu adaptador, como alguns indicaram em outros threads. No entanto, a chamada do método real é muito mais simples do que muitos indicaram.

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

1
Essa é a solução exata. : D
Afjalur Rahman Rana

1
para mim, onSaveInstanceState não é chamado, portanto, salvar o estado em onPause parece uma opção melhor. estou também abrindo a pilha de retorno para retornar ao fragmento.
filthy_wizard

para a carga no estado. que faço em onViewCreated. só parece funcionar quando estou no modo de depuração e uso pontos de interrupção? ou se eu adicionar um atraso e colocar a execução em uma co-rotina.
filthy_wizard

21

I Defina as variáveis ​​em onCreate (), salve a posição de rolagem em onPause () e defina a posição de rolagem em 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);
    }
}

Mantenha o deslocamento do item também. Agradável. Obrigado!
até


14

Todos os gerenciadores de layout incluídos na biblioteca de suporte já sabem como salvar e restaurar a posição de rolagem.

A posição de rolagem é restaurada na primeira passagem de layout, o que acontece quando as seguintes condições são atendidas:

  • um gerenciador de layout é anexado ao RecyclerView
  • um adaptador é conectado ao RecyclerView

Normalmente você define o gerenciador de layout em XML, então tudo que você precisa fazer agora é

  1. Carregue o adaptador com dados
  2. Em seguida, conecte o adaptador aoRecyclerView

Você pode fazer isso a qualquer momento, não está restrito a Fragment.onCreateViewou Activity.onCreate.

Exemplo: certifique-se de que o adaptador esteja conectado sempre que os dados forem atualizados.

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

A posição de rolagem salva é calculada a partir dos seguintes dados:

  • Posição do adaptador do primeiro item na tela
  • Deslocamento de pixel da parte superior do item a partir do topo da lista (para layout vertical)

Portanto, você pode esperar uma pequena incompatibilidade, por exemplo, se seus itens tiverem dimensões diferentes na orientação retrato e paisagem.


O RecyclerView/ ScrollView/ qualquer coisa precisa ter um android:idpara que seu estado seja salvo.


Isso foi tudo para mim. Eu estava configurando o adaptador inicialmente e depois atualizando seus dados quando disponíveis. Conforme explicado acima, deixá-lo até que os dados estejam disponíveis faz com que ele volte automaticamente para onde estava.
NeilS

Obrigado @Eugen. Existe alguma documentação sobre como salvar e restaurar a posição de rolagem do LayoutManager?
Adam Hurwitz

@AdamHurwitz Que tipo de documentação você esperaria? O que descrevi é um detalhe de implementação do LinearLayoutManager, então leia seu código-fonte.
Eugen Pechanec

1
Aqui está um link para LinearLayoutManager.SavedState: cs.android.com/androidx/platform/frameworks/support/+/…
Eugen Pechanec

Obrigado, @EugenPechanec. Isso funcionou para mim para rotações de dispositivos. Também estou usando uma visualização de navegação inferior e, quando alterno para outra guia inferior e volto, a lista começa do início novamente. Alguma ideia do porquê disso?
schv09

4

Aqui está minha abordagem para salvá-los e manter o estado de visualização de reciclagem em um fragmento. Primeiro, adicione mudanças de configuração em sua atividade pai conforme o código abaixo.

android:configChanges="orientation|screenSize"

Então, em seu Fragment, declare esses métodos de instância

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

Adicione o código abaixo em seu método @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);
}

Adicione o método onConfigartionChanged como abaixo.

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

Esta abordagem resolverá seu problema. se você tiver um gerenciador de layout diferente, então apenas substitua o gerenciador de layout superior.


Você pode não precisar de um Handler para dados persistentes. Os métodos de ciclo de vida serão suficientes o suficiente para salvar / carregar.
Mahi

@sayaMahi Você pode descrever de uma maneira adequada. Também vi que onConfigurationChanged evita chamar o método destroy. portanto, não é uma solução adequada.
Pawan kumar sharma

3

É assim que eu restauro a posição do RecyclerView com GridLayoutManager após a rotação quando você precisa recarregar dados da Internet com AsyncTaskLoader.

Faça uma variável global de Parcelable e GridLayoutManager e uma string final estática:

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

Salvar o estado do gridLayoutManager em onSaveInstance ()

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

Restaurar em 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);
        }
    }

Então, quando o carregador obtém dados da Internet, você restaura a posição de reciclagem de visualização em onLoadFinished ()

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

.. claro que você tem que instanciar o gridLayoutManager dentro de onCreate. Felicidades


3

Eu tive o mesmo requisito, mas as soluções aqui não me ajudaram muito, devido à fonte de dados para o recyclerView.

Eu estava extraindo o estado LinearLayoutManager de RecyclerViews em onPause ()

private Parcelable state;

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

Parcelable stateé salvo em onSaveInstanceState(Bundle outState)e extraído novamente em onCreate(Bundle savedInstanceState), quando savedInstanceState != null.

No entanto, o adaptador recyclerView é preenchido e atualizado por um objeto ViewModel LiveData retornado por uma chamada DAO para um banco de dados 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);
    }
});

Por fim, descobri que restaurar o estado da instância diretamente após os dados serem definidos no adaptador manteria o estado de rolagem nas rotações.

Suspeito que isso seja porque o LinearLayoutManagerestado que eu restaurei estava sendo sobrescrito quando os dados foram retornados pela chamada do banco de dados ou o estado restaurado não fazia sentido em um LinearLayoutManager 'vazio'.

Se os dados do adaptador estiverem disponíveis diretamente (ou seja, não contigentes em uma chamada de banco de dados), a restauração do estado da instância no LinearLayoutManager poderá ser feita após o adaptador ser configurado no recyclerView.

A distinção entre os dois cenários me sustentou por muito tempo.


3

No Android API Nível 28, simplesmente garanto que configurei meu LinearLayoutManagere RecyclerView.Adapterno meu Fragment#onCreateViewmétodo, e tudo Just Worked ™ ️. Eu não precisava fazer nada onSaveInstanceStateou onRestoreInstanceStatetrabalhar.

A resposta de Eugen Pechanec explica por que isso funciona.


2

A partir da versão 1.2.0-alpha02 da biblioteca androidx recyclerView, agora é gerenciado automaticamente. Basta adicioná-lo com:

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

E use:

adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY

O enum StateRestorationPolicy tem 3 opções:

  • ALLOW - o estado padrão, que restaura o estado RecyclerView imediatamente, na próxima passagem de layout
  • PREVENT_WHEN_EMPTY - restaura o estado RecyclerView somente quando o adaptador não está vazio (adapter.getItemCount ()> 0). Se os seus dados forem carregados de forma assíncrona, o RecyclerView espera até que os dados sejam carregados e só então o estado é restaurado. Se você tiver itens padrão, como cabeçalhos ou indicadores de progresso de carregamento como parte do seu adaptador, você deve usar a opção PREVENT , a menos que os itens padrão sejam adicionados usando MergeAdapter . MergeAdapter espera que todos os seus adaptadores estejam prontos e só então restaura o estado.
  • PREVENT - toda restauração de estado é adiada até que você defina ALLOW ou PREVENT_WHEN_EMPTY.

Observe que, no momento desta resposta, a biblioteca recyclerView ainda está em alpha03, mas a fase alpha não é adequada para fins de produção .


Muito excesso de trabalho economizado. Além disso, tivemos que salvar o índice de cliques da lista, restaurar elementos, impedir o redesenho em onCreateView, aplicar scrollToPosition (ou fazer como outras respostas) e ainda assim o usuário notou a diferença após voltar
Rahul Kahale

Além disso, se eu quiser salvar a posição do RecyclerView após navegar pelo aplicativo ... há alguma ferramenta para isso?
StayCool

@RubenViguera Por que alfa não é para fins de produção? Quer dizer, isso é realmente ruim?
StayCool

1

Para mim, o problema era que eu configurava um novo gerenciador de layout toda vez que trocava meu adaptador, perdendo a posição de rolagem no recyclerView.


1

Gostaria apenas de compartilhar a situação recente que encontro com o RecyclerView. Espero que qualquer pessoa com o mesmo problema se beneficie.

Meu requisito de projeto: Portanto, tenho um RecyclerView que lista alguns itens clicáveis ​​em minha atividade principal (Atividade-A). Quando o item é clicado, uma nova atividade é exibida com os detalhes do item (Atividade-B).

Eu implementei no Manifesto arquivo que a Activity-A é pai da Activity-B, dessa forma, tenho um botão voltar ou home na ActionBar da Activity-B

Problema: toda vez que pressiono o botão Voltar ou Início na ActionBar da Activity-B, a Activity-A entra em todo o ciclo de vida da atividade começando em onCreate ()

Embora eu tenha implementado um onSaveInstanceState () salvando a Lista do Adaptador do RecyclerView, quando a Activity-A inicia seu ciclo de vida, o saveInstanceState é sempre nulo no método onCreate ().

Pesquisando mais na internet, me deparei com o mesmo problema, mas a pessoa percebeu que no botão Voltar ou Início abaixo do dispositivo Anroid (botão Voltar / Início padrão), a Atividade-A não entra no Ciclo de Vida da Atividade.

Solução:

  1. Removi a tag da atividade pai no manifesto da atividade B
  2. Eu habilitei o botão home ou voltar na Activity-B

    No método onCreate (), adicione esta linha supportActionBar? .SetDisplayHomeAsUpEnabled (true)

  3. No método overide fun onOptionsItemSelected (), verifiquei o item.ItemId no qual o item foi clicado com base no id. O Id para o botão Voltar é

    android.R.id.home

    Em seguida, implemente uma chamada de função finish () dentro de android.R.id.home

    Isso encerrará a Atividade-B e trará o Acitivy-A sem passar por todo o ciclo de vida.

Para minhas necessidades, esta é a melhor solução até agora.

     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

Eu tive o mesmo problema com uma pequena diferença, eu estava usando Databinding , e estava configurando o adaptador recyclerView e o layoutManager nos métodos de binding.

O problema era que a vinculação de dados estava acontecendo depois de onResume, então estava redefinindo meu layoutManager depois de onResume e isso significa que estava rolando de volta para o lugar original todas as vezes. Portanto, quando parei de definir layoutManager nos métodos do adaptador de ligação de dados, ele voltou a funcionar bem.


como você conseguiu isso?
Kedar Sukerkar 01 de

0

No meu caso, eu estava configurando o RecyclerView layoutManagertanto em XML quanto em onViewCreated. Remover a atribuição no onViewCreatedconsertou.

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

    }   

Por que estou usando o OnSaveInstanceState()in onPause()significa, embora a mudança para outra atividade onPause seja chamada. Isso salvará a posição do scroll e restaurará a posição quando voltarmos de outra atividade.


2
O campo estático Público não é bom. Dados globais e singletons não são bons.
Weston

@weston eu entendi agora. Mudei o savingstate para onpause para remover global.
Steve
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.