Manter/salvar/restaurar a posição de rolagem ao retornar a uma ListView
-
26-09-2019 - |
Pergunta
Eu tenho um longo ListView
que o usuário possa rolar antes de retornar à tela anterior. Quando o usuário abre isso ListView
Novamente, quero que a lista seja rolada até o mesmo ponto em que era anteriormente. Alguma idéia de como conseguir isso?
Solução
Experimente isso:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
// ...
// restore index and position
mList.setSelectionFromTop(index, top);
Explicação:
ListView.getFirstVisiblePosition()
Retorna o item da lista mais visível. Mas esse item pode ser parcialmente rolar fora de vista e, se você deseja restaurar a posição de rolagem exata da lista, você precisa obter esse deslocamento. Então ListView.getChildAt(0)
retorna o View
para o item da lista superior e depois View.getTop() - mList.getPaddingTop()
retorna seu deslocamento relativo do topo do ListView
. Então, para restaurar o ListView
Posição de rolagem, nós chamamos ListView.setSelectionFromTop()
com o índice do item que queremos e um deslocamento para posicionar sua borda superior do topo do ListView
.
Outras dicas
Parcelable state;
@Override
public void onPause() {
// Save ListView state @ onPause
Log.d(TAG, "saving listview state @ onPause");
state = listView.onSaveInstanceState();
super.onPause();
}
...
@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Set new items
listView.setAdapter(adapter);
...
// Restore previous state (including selected item index and scroll position)
if(state != null) {
Log.d(TAG, "trying to restore listview state..");
listView.onRestoreInstanceState(state);
}
}
Eu adotei a solução sugerida por @(Kirk Woll) e funciona para mim. Também vi no código -fonte do Android para o aplicativo "contatos", que eles usam uma técnica semelhante. Gostaria de adicionar mais alguns detalhes: no topo da minha classe derivada da ListActivity:
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;
Então, alguns métodos substituem:
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
mListState = state.getParcelable(LIST_STATE);
}
@Override
protected void onResume() {
super.onResume();
loadData();
if (mListState != null)
getListView().onRestoreInstanceState(mListState);
mListState = null;
}
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
mListState = getListView().onSaveInstanceState();
state.putParcelable(LIST_STATE, mListState);
}
É claro que "LoadData" é minha função para recuperar dados do banco de dados e colocá -los na lista.
No meu dispositivo Froyo, isso funciona quando você altera a orientação do telefone e quando você edita um item e volta à lista.
Uma maneira muito simples:
/** Save the position **/
int currentPosition = listView.getFirstVisiblePosition();
//Here u should save the currentPosition anywhere
/** Restore the previus saved position **/
listView.setSelection(savedPosition);
O método SetSelection redefinirá a lista para o item fornecido. Se não estiver no modo de toque, o item será realmente selecionado se, no modo de toque, o item será posicionado apenas na tela.
Uma abordagem mais complicada:
listView.setOnScrollListener(this);
//Implements the interface:
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
mCurrentX = view.getScrollX();
mCurrentY = view.getScrollY();
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
//Save anywere the x and the y
/** Restore: **/
listView.scrollTo(savedX, savedY);
Encontrei algo interessante sobre isso.
Eu tentei setSelection e scrolltoxy, mas não funcionou, a lista permaneceu na mesma posição, depois de algumas tentativas e erros, recebi o seguinte código que funciona
final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {
@Override
public void run() {
list.setSelection(0);
}
});
Se, em vez de postar o Runnable, você tenta Runonuithread, ele também não funciona (pelo menos em alguns dispositivos)
Esta é uma solução alternativa muito estranha para algo que deve ser direto.
CUIDADO!! Há um bug no AbslistView que não permite que o OnsAvestate () funcione corretamente se o listView.getFirstVisiblePosition () for 0.
Portanto, se você tem imagens grandes que ocupam a maior parte da tela e você rola para a segunda imagem, mas um pouco do primeiro está mostrando, a posição de rolagem não será salva ...
a partir de AbslistView.java:1650 (Comentários meu)
// this will be false when the firstPosition IS 0
if (haveChildren && mFirstPosition > 0) {
...
} else {
ss.viewTop = 0;
ss.firstId = INVALID_POSITION;
ss.position = 0;
}
Mas, nessa situação, o 'top' no código abaixo será um número negativo que faz com que outros problemas que impeçam o estado a ser restaurado corretamente. Então, quando o 'top' for negativo, pegue o próximo filho
// save index and top position
int index = getFirstVisiblePosition();
View v = getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
if (top < 0 && getChildAt(1) != null) {
index++;
v = getChildAt(1);
top = v.getTop();
}
// parcel the index and top
// when restoring, unparcel index and top
listView.setSelectionFromTop(index, top);
private Parcelable state;
@Override
public void onPause() {
state = mAlbumListView.onSaveInstanceState();
super.onPause();
}
@Override
public void onResume() {
super.onResume();
if (getAdapter() != null) {
mAlbumListView.setAdapter(getAdapter());
if (state != null){
mAlbumListView.requestFocus();
mAlbumListView.onRestoreInstanceState(state);
}
}
}
É o bastante
Para alguns que procuram uma solução para esse problema, a raiz do problema pode ser onde você está definindo o adaptador de visualizações da sua lista. Depois de definir o adaptador no ListView, ele redefine a posição de rolagem. Apenas algo a considerar. Mudei de definir o adaptador no meu OnCreateView depois de pegar a referência ao ListView e ele resolveu o problema para mim. =)
Estou postando isso porque estou surpreso que ninguém tenha mencionado isso.
Depois que o usuário clica no botão Voltar, ele retornará ao ListView no mesmo estado que saiu dele.
Esse código substituirá o botão "Up" para se comportar da mesma maneira que o botão Voltar, portanto, no caso de ListView -> Detalhes -> Voltar para ListView (e nenhuma outra opção) Este é o código mais simples para manter a rolagem e o conteúdo no ListView.
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return(true);
}
return(super.onOptionsItemSelected(item)); }
CUIDADO: Se você puder ir a outra atividade da atividade de detalhes, o botão UP o devolverá de volta a essa atividade, para que você tenha que manipular a história do butão para que isso funcione.
Não é simplesmente android:saveEnabled="true"
Na Declaração ListView XML o suficiente?
A melhor solução é:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
// ...
// restore index and position
mList.post(new Runnable() {
@Override
public void run() {
mList.setSelectionFromTop(index, top);
}
});
Você deve ligar no post e no tópico!
Você pode manter o estado de rolagem após uma recarga se salvar o estado antes de recarregar e restaurá -lo depois. No meu caso, fiz uma solicitação de rede assíncrona e recarreguei a lista em um retorno de chamada após a conclusão. É aqui que eu restauro o estado. O exemplo de código é Kotlin.
val state = myList.layoutManager.onSaveInstanceState()
getNewThings() { newThings: List<Thing> ->
myList.adapter.things = newThings
myList.layoutManager.onRestoreInstanceState(state)
}
Se você estiver usando fragmentos hospedados em uma atividade, você pode fazer algo assim:
public abstract class BaseFragment extends Fragment {
private boolean mSaveView = false;
private SoftReference<View> mViewReference;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (mSaveView) {
if (mViewReference != null) {
final View savedView = mViewReference.get();
if (savedView != null) {
if (savedView.getParent() != null) {
((ViewGroup) savedView.getParent()).removeView(savedView);
return savedView;
}
}
}
}
final View view = inflater.inflate(getFragmentResource(), container, false);
mViewReference = new SoftReference<View>(view);
return view;
}
protected void setSaveView(boolean value) {
mSaveView = value;
}
}
public class MyFragment extends BaseFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setSaveView(true);
final View view = super.onCreateView(inflater, container, savedInstanceState);
ListView placesList = (ListView) view.findViewById(R.id.places_list);
if (placesList.getAdapter() == null) {
placesList.setAdapter(createAdapter());
}
}
}
Se você estiver salvando/restaurando a posição de rolagem de ListView
Você está essencialmente duplicando a funcionalidade já implementada na estrutura do Android. o ListView
Restaura a posição fina de rolagem bem por conta própria, exceto uma ressalva: como @Aaronvargas mencionou que há um bug em AbsListView
Isso não permitirá restaurar a posição de rolagem fina para o primeiro item da lista. No entanto, a melhor maneira de restaurar a posição de rolagem não é restaurá -la. O Android Framework fará isso melhor para você. Apenas certifique -se de ter atendido as seguintes condições:
- Certifique -se de não ligar
setSaveEnabled(false)
método e não definidoandroid:saveEnabled="false"
atributo para a lista no arquivo de layout XML - por
ExpandableListView
sobreporlong getCombinedChildId(long groupId, long childId)
método para que ele retorne um número longo positivo (implementação padrão na aulaBaseExpandableListAdapter
retorna número negativo). Aqui estão exemplos:
.
@Override
public long getChildId(int groupPosition, int childPosition) {
return 0L | groupPosition << 12 | childPosition;
}
@Override
public long getCombinedChildId(long groupId, long childId) {
return groupId << 32 | childId << 1 | 1;
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getCombinedGroupId(long groupId) {
return (groupId & 0x7FFFFFFF) << 32;
}
- E se
ListView
ouExpandableListView
é usado em um fragmento, não recrie o fragmento na recreação da atividade (após a rotação da tela, por exemplo). Obter o fragmento comfindFragmentByTag(String tag)
método. - verifique se o
ListView
tem umandroid:id
E é único.
Para evitar advertência mencionada acima com o item da primeira lista, você pode criar seu adaptador da maneira como ele retorna ListView
na posição 0. Aqui está o exemplo simples do projeto mostra ListView
e ExpandableListView
Restaure suas posições finas de rolagem, enquanto suas posições de rolagem não são explicitamente salvas/restauradas. A posição de rolagem fina é restaurada perfeitamente, mesmo para os cenários complexos, com mudança temporária para algum outro aplicativo, rotação de tela dupla e alternância de volta ao aplicativo de teste. Observe que, se você estiver saindo explicitamente do aplicativo (pressionando o botão traseiro), a posição de rolagem não será salva (assim como todas as outras visualizações não salvarão seu estado).https://github.com/voromto/restorescrollposition/releases
Para uma atividade derivada da ListActivity que implementa carregadorManager.loadCallbacks usando um SimpleCursoradapter, não funcionou para restaurar a posição em OnReset (), porque a atividade quase sempre foi reiniciada e o adaptador foi recarregado quando a visualização dos detalhes foi fechada. O truque era restaurar a posição em OnLoadfinished ():
em onListItemClick ():
// save the selected item position when an item was clicked
// to open the details
index = getListView().getFirstVisiblePosition();
View v = getListView().getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - getListView().getPaddingTop());
em OnLoadfinished ():
// restore the selected item which was saved on item click
// when details are closed and list is shown again
getListView().setSelectionFromTop(index, top);
em OnbackPressed ():
// Show the top item at next start of the app
index = 0;
top = 0;
Nenhuma das soluções oferecidas aqui parecia funcionar para mim. No meu caso, eu tenho um ListView
em um Fragment
que estou substituindo em um FragmentTransaction
, então um novo Fragment
a instância é criada cada vez que o fragmento é mostrado, o que significa que o ListView
Estado não pode ser armazenado como membro do Fragment
.
Em vez disso, acabei armazenando o estado em meu costume Application
classe. O código abaixo deve dar uma idéia de como isso funciona:
public class MyApplication extends Application {
public static HashMap<String, Parcelable> parcelableCache = new HashMap<>();
/* ... code omitted for brevity ... */
}
public class MyFragment extends Fragment{
private ListView mListView = null;
private MyAdapter mAdapter = null;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mAdapter = new MyAdapter(getActivity(), null, 0);
mListView = ((ListView) view.findViewById(R.id.myListView));
Parcelable listViewState = MyApplication.parcelableCache.get("my_listview_state");
if( listViewState != null )
mListView.onRestoreInstanceState(listViewState);
}
@Override
public void onPause() {
MyApplication.parcelableCache.put("my_listview_state", mListView.onSaveInstanceState());
super.onPause();
}
/* ... code omitted for brevity ... */
}
A idéia básica é que você armazene o estado fora da instância do fragmento. Se você não gosta da ideia de ter um campo estático na sua classe de aplicativo, acho que você pode fazê -lo implementando uma interface de fragmento e armazenando o estado em sua atividade.
Outra solução seria armazená -la em SharedPreferences
, mas fica um pouco mais complicado e você precisará limpá -lo no lançamento do aplicativo, a menos que deseje que o estado seja persistido nos lançamentos de aplicativos.
Além disso, para evitar a "posição de rolagem não salva quando o primeiro item é visível", você pode exibir um primeiro item fictício com 0px
altura. Isso pode ser alcançado ao substituir getView()
No seu adaptador, assim:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if( position == 0 ) {
View zeroHeightView = new View(parent.getContext());
zeroHeightView.setLayoutParams(new ViewGroup.LayoutParams(0, 0));
return zeroHeightView;
}
else
return super.getView(position, convertView, parent);
}
Minha resposta é para Firebase e a posição 0 é uma solução alternativa
Parcelable state;
DatabaseReference everybody = db.getReference("Everybody Room List");
everybody.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
state = listView.onSaveInstanceState(); // Save
progressBar.setVisibility(View.GONE);
arrayList.clear();
for (DataSnapshot messageSnapshot : dataSnapshot.getChildren()) {
Messages messagesSpacecraft = messageSnapshot.getValue(Messages.class);
arrayList.add(messagesSpacecraft);
}
listView.setAdapter(convertView);
listView.onRestoreInstanceState(state); // Restore
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
e ConvertView
Posição 0 A Adicionar um item em branco que você não está usando
public class Chat_ConvertView_List_Room extends BaseAdapter {
private ArrayList<Messages> spacecrafts;
private Context context;
@SuppressLint("CommitPrefEdits")
Chat_ConvertView_List_Room(Context context, ArrayList<Messages> spacecrafts) {
this.context = context;
this.spacecrafts = spacecrafts;
}
@Override
public int getCount() {
return spacecrafts.size();
}
@Override
public Object getItem(int position) {
return spacecrafts.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@SuppressLint({"SetTextI18n", "SimpleDateFormat"})
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.message_model_list_room, parent, false);
}
final Messages s = (Messages) this.getItem(position);
if (position == 0) {
convertView.getLayoutParams().height = 1; // 0 does not work
} else {
convertView.getLayoutParams().height = RelativeLayout.LayoutParams.WRAP_CONTENT;
}
return convertView;
}
}
Eu vi esse trabalho temporariamente sem perturbar o usuário, espero que funcione para você
Use este código abaixo:
int index,top;
@Override
protected void onPause() {
super.onPause();
index = mList.getFirstVisiblePosition();
View v = challengeList.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
}
E sempre que você atualiza seus dados, use este código abaixo:
adapter.notifyDataSetChanged();
mList.setSelectionFromTop(index, top);
estou a usar FireBaseListAdapter e não conseguiu que nenhuma das soluções funcione. Acabei fazendo isso. Acho que existem maneiras mais elegantes, mas essa é uma solução completa e funcional.
Antes do OnCreate:
private int reset;
private int top;
private int index;
Dentro do FireBaseListAdapter:
@Override
public void onDataChanged() {
super.onDataChanged();
// Only do this on first change, when starting
// activity or coming back to it.
if(reset == 0) {
mListView.setSelectionFromTop(index, top);
reset++;
}
}
OnStart:
@Override
protected void onStart() {
super.onStart();
if(adapter != null) {
adapter.startListening();
index = 0;
top = 0;
// Get position from SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
top = sharedPref.getInt("TOP_POSITION", 0);
index = sharedPref.getInt("INDEX_POSITION", 0);
// Set reset to 0 to allow change to last position
reset = 0;
}
}
Onstop:
@Override
protected void onStop() {
super.onStop();
if(adapter != null) {
adapter.stopListening();
// Set position
index = mListView.getFirstVisiblePosition();
View v = mListView.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
// Save position to SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
sharedPref.edit().putInt("TOP_POSITION" + "", top).apply();
sharedPref.edit().putInt("INDEX_POSITION" + "", index).apply();
}
}
Já que eu também tive que resolver isso para FireBaseRecycleradapter Estou postando a solução aqui para isso também:
Antes do OnCreate:
private int reset;
private int top;
private int index;
Dentro do FireBaseRecycleradapter:
@Override
public void onDataChanged() {
// Only do this on first change, when starting
// activity or coming back to it.
if(reset == 0) {
linearLayoutManager.scrollToPositionWithOffset(index, top);
reset++;
}
}
OnStart:
@Override
protected void onStart() {
super.onStart();
if(adapter != null) {
adapter.startListening();
index = 0;
top = 0;
// Get position from SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
top = sharedPref.getInt("TOP_POSITION", 0);
index = sharedPref.getInt("INDEX_POSITION", 0);
// Set reset to 0 to allow change to last position
reset = 0;
}
}
Onstop:
@Override
protected void onStop() {
super.onStop();
if(adapter != null) {
adapter.stopListening();
// Set position
index = linearLayoutManager.findFirstVisibleItemPosition();
View v = linearLayoutManager.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - linearLayoutManager.getPaddingTop());
// Save position to SharedPrefs
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
sharedPref.edit().putInt("TOP_POSITION" + "", top).apply();
sharedPref.edit().putInt("INDEX_POSITION" + "", index).apply();
}
}