我有一个很长的ListView
,用户可以在返回上一个屏幕之前滚动。当用户再次打开此ListView
时,我希望列表滚动到之前的同一点。关于如何实现这一点的任何想法?
答案 0 :(得分:625)
试试这个:
// 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
的顶部定位。< / p>
答案 1 :(得分:529)
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);
}
}
答案 2 :(得分:52)
我采用了@(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”是我从DB中检索数据并将其放入列表的功能。
在我的Froyo设备上,当您更改手机方向时,以及编辑项目并返回列表时,这都有效。
答案 3 :(得分:27)
一种非常简单的方法:
/** 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);
答案 4 :(得分:17)
我发现了一些有趣的事情。
我尝试了setSelection和scrolltoXY,但它根本不起作用,列表保持在同一位置,经过一些试验和错误后,我得到了以下代码才能正常工作
final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {
@Override
public void run() {
list.setSelection(0);
}
});
如果不是发布Runnable而是尝试runOnUiThread,它也不起作用(至少在某些设备上)
对于应该直截了当的事情,这是一个非常奇怪的解决方法。
答案 5 :(得分:10)
小心!如果ListView.getFirstVisiblePosition()为0,则AbsListView中存在一个错误,该错误不允许onSaveState()正常工作。
因此,如果您有占据屏幕大部分的大图像,并且您滚动到第二个图像,但第一个图像显示,则滚动位置将不会保存...
来自 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;
}
但是在这种情况下,下面代码中的“顶部”将是一个负数,这会导致阻止状态正确恢复的其他问题。因此,当'top'为负数时,请选择下一个孩子
// 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);
答案 6 :(得分:5)
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);
}
}
}
够了
答案 7 :(得分:4)
对于寻找此问题的解决方案的人来说,问题的根源可能是您设置列表视图适配器的位置。在listview上设置适配器后,它会重置滚动位置。只是需要考虑的事情。在我们获取对listview的引用后,我将适配器设置为我的onCreateView,它解决了我的问题。 =)
答案 8 :(得分:1)
我发布这个是因为我很惊讶没有人提到过这个。
用户点击后退按钮后,他将以与退出时相同的状态返回列表视图。
此代码将覆盖“向上”按钮,其行为与后退按钮相同,因此在Listview的情况下 - &gt;详细信息 - &gt;回到Listview(没有其他选项)这是维护列表视图中滚动位置和内容的最简单的代码。
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return(true);
}
return(super.onOptionsItemSelected(item)); }
警告:如果您可以从详细信息活动转到另一个活动,则向上按钮将返回到该活动,因此您必须操纵后退按钮历史记录才能使其生效。
答案 9 :(得分:1)
ListView xml声明中的android:saveEnabled="true"
不够简单吗?
答案 10 :(得分: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.post(new Runnable() {
@Override
public void run() {
mList.setSelectionFromTop(index, top);
}
});
你必须在线路上打电话!
答案 11 :(得分:1)
如果在重新加载之前保存状态并在之后恢复,则可以在重新加载后保持滚动状态。在我的情况下,我做了一个异步网络请求,并在完成后在回调中重新加载列表。这是我恢复状态的地方。代码示例是Kotlin。
val state = myList.layoutManager.onSaveInstanceState()
getNewThings() { newThings: List<Thing> ->
myList.adapter.things = newThings
myList.layoutManager.onRestoreInstanceState(state)
}
答案 12 :(得分:0)
要弄清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的适配器,因此一切都保留了!
答案 13 :(得分:0)
我正在使用 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();
}
}
答案 14 :(得分:0)
使用以下代码:
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);
答案 15 :(得分:0)
我的答案是针对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 a添加一个您不使用的空白项目
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;
}
}
我在不打扰用户的情况下临时看到了此作品,希望它对您有用
答案 16 :(得分:0)
这里提供的解决方案似乎都不适用于我。在我的情况下,我在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);
}
答案 17 :(得分:0)
对于使用SimpleCursorAdapter实现LoaderManager.LoaderCallbacks的ListActivity派生的活动,它无法恢复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;
答案 18 :(得分:0)
如果你自己保存/恢复ListView
的滚动位置,你基本上复制了android框架中已经实现的功能。除了一个警告之外,ListView
还可以很好地恢复精细滚动位置:正如@aaronvargas所提到的,AbsListView
中存在一个错误,它不允许恢复第一个列表项的精细滚动位置。然而,恢复滚动位置的最佳方法是不恢复它。 Android框架将为您做得更好。只要确保您符合以下条件:
setSaveEnabled(false)
方法,并且没有为xml布局文件中的列表设置android:saveEnabled="false"
属性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
并且它是唯一的。为了避免上面提到的第一个列表项的警告,您可以按照它在位置0返回ListView
的特殊虚拟零像素高度视图的方式来制作适配器。
以下是简单示例项目显示ListView
和ExpandableListView
恢复其精细滚动位置,而其滚动位置未明确保存/恢复。即使对于复杂的场景,临时切换到其他应用程序,双屏幕旋转和切换回测试应用程序,精细滚动位置也能完美恢复。请注意,如果您明确退出应用程序(通过按“返回”按钮),则不会保存滚动位置(以及所有其他视图都不会保存其状态)。
https://github.com/voromto/RestoreScrollPosition/releases
答案 19 :(得分: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());
}
}
}