Сохранять/сохранять/восстанавливать позицию прокрутки при возврате в ListView

У меня есть длинный ListView, который пользователь может прокрутить, прежде чем вернуться к предыдущему экрану. Когда пользователь снова откроет этот ListView, я хочу, чтобы список был прокручен до той же точки, что и раньше. Любые идеи о том, как достичь этого?

Ответ 1

Попробуйте следующее:

// 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.

Ответ 2

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

Ответ 3

Я принял решение, предложенное @(Kirk Woll), и оно работает для меня. Я также видел в исходном коде 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 это работает как при изменении ориентации телефона, так и при редактировании элемента и возврате его в список.

Ответ 4

Очень простой способ:

/** Save the position **/
int currentPosition = listView.getFirstVisiblePosition();

//Here u should save the currentPosition anywhere

/** Restore the previus saved position **/
listView.setSelection(savedPosition);

Метод setSelection будет reset список поставляемого элемента. Если не в режиме касания, элемент будет выбран, если в касательном режиме элемент будет отображаться только на экране.

Более сложный подход:

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

Ответ 5

Я нашел в этом что-то интересное.

Я попробовал setSelection и scrolltoXY, но он вообще не работал, список остался в той же позиции, после некоторых проб и ошибок я получил следующий код, который работает

final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {            
    @Override
    public void run() {
        list.setSelection(0);
    }
});

Если вместо публикации Runnable вы пытаетесь выполнить runOnUiThread, он не работает (по крайней мере, на некоторых устройствах)

Это очень странное решение для чего-то, что должно быть прямым.

Ответ 6

ВНИМАНИЕ!! В 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);

Ответ 7

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

Достаточно

Ответ 8

Для некоторых, кто ищет решение этой проблемы, корень проблемы может быть там, где вы устанавливаете адаптер списка видов. После того, как вы установите адаптер в списке, он сбрасывает положение прокрутки. Просто подумать. Я переместил адаптер в мой onCreateView после того, как мы захватили ссылку на список, и он решил проблему для меня. =)

Ответ 9

Я публикую это, потому что я удивлен, что никто не упомянул об этом.

После того, как пользователь нажмет кнопку "Назад", он вернется к списку в том же состоянии, в котором он вышел.

Этот код будет переопределять кнопку "вверх", чтобы вести себя так же, как кнопка "назад", поэтому в случае 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)); }

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

Ответ 10

Не просто android:saveEnabled="true" в объявлении xml ListView достаточно?

Ответ 11

ЛУЧШЕЕ РЕШЕНИЕ:

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

ВЫ ДОЛЖНЫ ПРИЗЫВАТЬ В ПОЧТУ И В РЕЗЬБЕ!

Ответ 12

Вы можете сохранить состояние прокрутки после перезагрузки, если вы сохраните состояние до перезагрузки и восстановления после него. В моем случае я сделал асинхронный сетевой запрос и перезагрузил список в обратном вызове после его завершения. Здесь я восстанавливаю состояние. Образец кода - это Kotlin.

val state = myList.layoutManager.onSaveInstanceState()

getNewThings() { newThings: List<Thing> ->

    myList.adapter.things = newThings
    myList.layoutManager.onRestoreInstanceState(state)
}

Ответ 13

Если вы используете фрагменты, размещенные в действии, вы можете сделать что-то вроде этого:

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

Ответ 14

Если вы сохраняете/восстанавливаете положение прокрутки ListView самостоятельно, вы по существу дублируете функциональность, уже внедренную в инфраструктуру Android. ListView восстанавливает точное положение прокрутки только по своему усмотрению, за исключением одного оговорки: поскольку @aaronvargas упоминает, что есть ошибка в AbsListView, которая не позволит восстановить точную позицию прокрутки для первого элемента списка. Тем не менее, лучший способ восстановить положение прокрутки - не восстановить его. Рамка для Android сделает это лучше для вас. Просто убедитесь, что вы выполнили следующие условия:

  • убедитесь, что вы не вызывали метод 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

Ответ 15

Для действия, полученного из 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;

Ответ 16

Похоже, ни одно из предложенных здесь решений не сработало для меня. В моем случае у меня есть ListView во Fragment который я заменяю в FragmentTransaction, поэтому каждый раз, когда отображается 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);
}

Ответ 17

Мой ответ для 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;
}
}

Я видел эту работу временно, не мешая пользователю, я надеюсь, что она работает для вас

Ответ 18

используйте этот код ниже:

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

Ответ 19

Я использую 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();
    }
}

Ответ 20

Чтобы уточнить отличный ответ Райана Ньюсома и настроить его для фрагментов и для обычного случая, когда мы хотим перейти от "основного" фрагмента 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, поэтому все остается, как мы его оставили!