Сохранить / сохранить / восстановить позицию прокрутки при возврате в ListView
Я долго ListView
что пользователь может прокрутить, прежде чем вернуться к предыдущему экрану. Когда пользователь открывает это ListView
опять же, я хочу, чтобы список был прокручен до той же точки, что и ранее. Есть идеи, как этого добиться?
17 ответов
Попробуй это:
// 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);
Объяснение:ListView.getFirstVisiblePosition()
возвращает наиболее заметный элемент списка. Но этот элемент может быть частично прокручен вне поля зрения, и если вы хотите восстановить точную позицию прокрутки в списке, вам нужно получить это смещение. Так ListView.getChildAt(0)
возвращает View
для верхнего элемента списка, а затем View.getTop() - mList.getPaddingTop()
возвращает относительное смещение от вершины ListView
, Затем, чтобы восстановить ListView
прокручиваем позицию, мы называем ListView.setSelectionFromTop()
с индексом элемента, который мы хотим, и смещением, чтобы расположить его верхний край от вершины ListView
,
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);
}
}
Я принял решение, предложенное @(Кирк Волл), и оно работает для меня. Я также видел в исходном коде Android для приложения "Контакты", что они используют похожую технику. Я хотел бы добавить еще несколько деталей: в верхней части моего класса, производного от ListActivity:
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;
Затем некоторые методы переопределяют:
@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);
}
Конечно, "loadData" - это моя функция для извлечения данных из БД и помещения их в список.
На моем устройстве Froyo это работает как при изменении ориентации телефона, так и при редактировании элемента и возвращении к списку.
Очень простой способ:
/** Save the position **/
int currentPosition = listView.getFirstVisiblePosition();
//Here u should save the currentPosition anywhere
/** Restore the previus saved position **/
listView.setSelection(savedPosition);
Метод setSelection сбрасывает список до предоставленного элемента. Если не в сенсорном режиме, элемент будет фактически выбран, если в сенсорном режиме он будет размещен только на экране.
Более сложный подход:
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);
Я нашел что-то интересное по этому поводу.
Я попробовал setSelection и scrolltoXY, но он не работал вообще, список остался в том же положении, после некоторых проб и ошибок я получил следующий код, который работает
final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {
@Override
public void run() {
list.setSelection(0);
}
});
Если вместо публикации Runnable вы пытаетесь запустить runOnUiThread, он также не работает (по крайней мере, на некоторых устройствах)
Это очень странный обходной путь для чего-то, что должно быть прямым.
ВНИМАНИЕ!! В AbsListView есть ошибка, которая не позволяет onSaveState() работать правильно, если ListView.getFirstVisiblePosition() равен 0.
Так что если у вас есть большие изображения, которые занимают большую часть экрана, и вы прокручиваете до второго изображения, но показывает немного первого, положение прокрутки не будет сохранено...
от AbsListView.java:1650 (комментарии мои)
// this will be false when the firstPosition IS 0
if (haveChildren && mFirstPosition > 0) {
...
} else {
ss.viewTop = 0;
ss.firstId = INVALID_POSITION;
ss.position = 0;
}
Но в этой ситуации "верх" в приведенном ниже коде будет отрицательным числом, что вызывает другие проблемы, препятствующие правильному восстановлению состояния. Таким образом, когда "верх" отрицательный, получить следующий ребенок
// 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);
Для тех, кто ищет решение этой проблемы, корень проблемы может быть в том, где вы настраиваете свой адаптер представления списка. После того, как вы установите адаптер в списке, он сбрасывает позицию прокрутки. Просто кое-что рассмотреть. Я переместил установку адаптера в свой onCreateView после того, как мы получили ссылку на просмотр списка, и это решило проблему для меня. знак равно
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);
}
}
}
Достаточно
ЛУЧШЕЕ РЕШЕНИЕ:
// 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);
}
});
ВЫ ДОЛЖНЫ ЗВОНИТЬ В ПОЧТУ И В РЕЗЬБЕ!
Вы можете сохранить состояние прокрутки после перезагрузки, если сохраните состояние до перезагрузки и восстановите его после. В моем случае я сделал асинхронный сетевой запрос и перезагрузил список в обратном вызове после его завершения. Здесь я восстанавливаю состояние. Пример кода - Котлин.
val state = myList.layoutManager.onSaveInstanceState()
getNewThings() { newThings: List<Thing> ->
myList.adapter.things = newThings
myList.layoutManager.onRestoreInstanceState(state)
}
Отправляю это, потому что я удивлен, что никто не упомянул это.
После того, как пользователь нажмет кнопку "Назад", он вернется к списку в том же состоянии, в котором он вышел из него.
Этот код переопределяет кнопку "вверх", чтобы вести себя так же, как кнопка "назад", поэтому в случае Listview -> Details -> Back to Listview (и без других опций) это самый простой код для поддержания положения прокрутки и содержимого. в списке.
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return(true);
}
return(super.onOptionsItemSelected(item)); }
Предостережение: если вы можете перейти к другому действию из подробного действия, кнопка вверх вернет вас к этому заданию, поэтому вам придется манипулировать историей кнопок, чтобы это работало.
Не просто android:saveEnabled="true"
в объявлении ListView xml достаточно?
Похоже, ни одно из предложенных здесь решений не сработало для меня. В моем случае у меня есть ListView
в Fragment
который я заменяю в FragmentTransaction
итак новый Fragment
экземпляр создается каждый раз при отображении фрагмента, что означает, что ListView
состояние не может быть сохранено как член Fragment
,
Вместо этого я закончил хранить состояние в своем обычае Application
учебный класс. Код ниже должен дать вам представление о том, как это работает:
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 ... */
}
Основная идея заключается в том, что вы сохраняете состояние вне экземпляра фрагмента. Если вам не нравится идея иметь статическое поле в вашем классе приложения, я думаю, вы могли бы сделать это, реализовав фрагментный интерфейс и сохранив состояние в вашей активности.
Другое решение будет хранить его в SharedPreferences
, но это становится немного сложнее, и вам нужно будет обязательно очистить его при запуске приложения, если вы не хотите, чтобы состояние сохранялось при запуске приложения.
Кроме того, чтобы избежать "позиции прокрутки, которая не сохраняется, когда виден первый элемент", вы можете отобразить фиктивный первый элемент с помощью 0px
рост. Это может быть достигнуто путем переопределения getView()
в вашем адаптере, вот так:
@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);
}
Мой ответ для Firebase, а позиция 0 - это обходной путь
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) {
}
});
и convertView
позиция 0 добавить пустой элемент, который вы не используете
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;
}
}
Я видел эту работу временно, не мешая пользователю, я надеюсь, что она работает для вас
Я использую FirebaseListAdapter и не смог заставить работать ни одно из решений. Я закончил тем, что делал это. Я предполагаю, что есть более элегантные способы, но это полное и рабочее решение.
Перед созданием:
private int reset;
private int top;
private int index;
Внутри 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();
}
}
Так как я также должен был решить эту проблему для FirebaseRecyclerAdapter, я также выкладываю здесь решение для этого:
Перед созданием:
private int reset;
private int top;
private int index;
Внутри 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();
}
}
Используйте этот код ниже:
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());
}
и всякий раз, когда вы обновляете свои данные, используйте следующий код:
adapter.notifyDataSetChanged();
mList.setSelectionFromTop(index, top);
Если вы сохраняете / восстанавливаете положение прокрутки ListView
Вы сами дублируете функциональность, уже реализованную в платформе Android. ListView
восстанавливает позицию точной прокрутки самостоятельно, за исключением одного предупреждения: как уже упоминалось @aaronvargas, есть ошибка в AbsListView
это не позволит восстановить точную позицию прокрутки для первого элемента списка. Тем не менее, лучший способ восстановить положение прокрутки - не восстанавливать его. Android Framework сделает это лучше для вас. Просто убедитесь, что вы выполнили следующие условия:
- убедитесь, что вы не звонили
setSaveEnabled(false)
метод и не установленandroid:saveEnabled="false"
атрибут для списка в файле макета XML - за
ExpandableListView
переопределениеlong getCombinedChildId(long groupId, long childId)
метод, так что он возвращает положительное длинное число (реализация по умолчанию в классеBaseExpandableListAdapter
возвращает отрицательное число). Вот примеры:
,
@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;
}
- если
ListView
или жеExpandableListView
используется во фрагменте, не воссоздайте фрагмент при восстановлении активности (например, после поворота экрана). Получить фрагмент сfindFragmentByTag(String tag)
метод. - убедитесь, что
ListView
имеетandroid:id
и это уникально.
Чтобы избежать вышеупомянутого предостережения с первым элементом списка, вы можете создать свой адаптер так, как он возвращает специальный фиктивный вид с нулевой высотой пикселей для ListView
в позиции 0. Вот простой пример проекта показывает ListView
а также ExpandableListView
восстановить их точные позиции прокрутки, тогда как их позиции прокрутки не сохраняются / не восстанавливаются явно. Точная позиция прокрутки восстанавливается идеально даже для сложных сценариев с временным переключением на какое-либо другое приложение, двойным поворотом экрана и переключением обратно на тестовое приложение. Обратите внимание, что если вы явно выходите из приложения (нажав кнопку "Назад"), позиция прокрутки не будет сохранена (как и все другие виды не сохранят свое состояние). https://github.com/voromto/RestoreScrollPosition/releases
Для действия, производного от ListActivity, который реализует LoaderManager.LoaderCallbacks с использованием SimpleCursorAdapter, не удалось восстановить позицию в onReset (), поскольку действие почти всегда перезапускалось, и адаптер перезагружался при закрытии представления сведений. Хитрость заключалась в том, чтобы восстановить позицию в onLoadFinished():
в 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());
в onLoadFinished():
// restore the selected item which was saved on item click
// when details are closed and list is shown again
getListView().setSelectionFromTop(index, top);
в onBackPressed():
// Show the top item at next start of the app
index = 0;
top = 0;
Если вы используете фрагменты, размещенные в действии, вы можете сделать что-то вроде этого:
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());
}
}
}
Чтобы прояснить отличный ответ Ryan Newsom и настроить его для фрагментов и для обычного случая, когда мы хотим перейти от "основного" фрагмента ListView к фрагменту "подробностей", а затем обратно к "мастеру"
private View root;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
if(root == null){
root = inflater.inflate(R.layout.myfragmentid,container,false);
InitializeView();
}
return root;
}
public void InitializeView()
{
ListView listView = (ListView)root.findViewById(R.id.listviewid);
BaseAdapter adapter = CreateAdapter();//Create your adapter here
listView.setAdpater(adapter);
//other initialization code
}
"Магия" здесь в том, что когда мы переходим обратно от фрагмента сведений к фрагменту ListView, представление не воссоздается, мы не устанавливаем адаптер ListView, поэтому все остается в том виде, в каком мы его оставили!