当返回ListView时,保持/保存/恢复滚动位置

我有一个很长的ListView,用户可以在返回上一个屏幕之前滚动它。当用户再次打开这个ListView时,我想让列表滚动到之前的位置。关于如何实现这一点,你有什么想法吗?

218238 次浏览

一个非常简单的方法:

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

试试这个:

// 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);
< p >解释:< br > < br > 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");
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);
}
}
我采用了@(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设备上,当你改变手机方向时,当你编辑一个项目并返回列表时,这都是有效的。

我发现了一些有趣的事情。

我尝试了setSelection和scrolltoXY,但它根本不起作用,列表仍然在相同的位置,经过一些尝试和错误,我得到了以下代码,确实工作

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

如果不是发布Runnable,你尝试runOnUiThread,它也不工作(至少在一些设备上)

这是一个非常奇怪的变通方法,应该是直截了当的。

警告! !在AbsListView中有一个错误,如果ListView.getFirstVisiblePosition()为0,则不允许onSaveState()正确工作。

所以,如果你有大图像,占据了屏幕的大部分,你滚动到第二张图像,但第一张图像的一部分正在显示,滚动位置将不会被保存…

from 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”将是一个负数,这将导致其他问题,阻止状态被正确恢复。所以当'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);

我发布这篇文章是因为我很惊讶没有人提到这一点。

当用户单击返回按钮后,他将返回到列表视图,在相同的状态,因为他离开它。

这段代码将覆盖“向上”按钮的行为与后退按钮相同,所以在Listview ->细节->回到Listview(没有其他选项)的情况下,这是最简单的代码来维护滚动位置和Listview中的内容。

 public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return(true);
}
return(super.onOptionsItemSelected(item)); }

注意:如果你可以从细节活动转到另一个活动,向上按钮将返回到该活动,所以你必须操作后退按钮历史,以使其工作。

难道ListView xml声明中的android:saveEnabled="true"还不够吗?

如果你在一个活动上使用片段,你可以这样做:

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

这就够了

如果你自己保存/恢复ListView的滚动位置,你基本上复制了android框架中已经实现的功能。ListView本身就可以很好地恢复滚动位置,除了一个警告:正如@aaronvargas所提到的,AbsListView中有一个错误,不允许恢复第一个列表项的滚动位置。然而,恢复滚动位置的最好方法是不恢复它。Android框架会做得更好。只要确保你符合以下条件:

  • 确保你没有调用setSaveEnabled(false)方法,也没有在xml布局文件中为列表设置android:saveEnabled="false"属性
  • for 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;
}
  • 如果在片段中使用了ListViewExpandableListView,则不要在活动重新创建时重新创建片段(例如在屏幕旋转后)。使用findFragmentByTag(String tag)方法获取片段。
  • 确保ListView有一个android:id并且它是唯一的。
为了避免前面提到的第一个列表项的警告,你可以制作你的适配器,它在位置0处为ListView返回特殊的虚拟零像素高度视图。 下面是一个简单的例子,项目显示ListViewExpandableListView恢复了它们的滚动位置,而它们的滚动位置没有显式地保存/恢复。精细的滚动位置完全恢复,即使是临时切换到一些其他应用程序的复杂场景,双屏幕旋转和切换回测试应用程序。请注意,如果您显式地退出应用程序(通过按后退按钮),滚动位置将不会保存(以及所有其他视图将不会保存它们的状态)。 https://github.com/voromto/RestoreScrollPosition/releases < / p >

最好的解决方案是:

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

你必须在邮件和线程中调用!

用于从实现LoaderManager的ListActivity派生的活动。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;

对于一些正在寻找此问题解决方案的人来说,问题的根源可能在于您设置列表视图适配器的位置。在列表视图上设置适配器后,它将重置滚动位置。只是需要考虑一下。我移动设置适配器到我的onCreateView后,我们抓取引用到列表视图,它解决了我的问题。=)

如果在重新加载前保存状态,并在重新加载后恢复状态,则可以在重新加载后保持滚动状态。在我的情况下,我做了一个异步网络请求,并在它完成后在回调中重新加载列表。这是我恢复状态的地方。代码示例是Kotlin。

val state = myList.layoutManager.onSaveInstanceState()


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


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

这里提供的解决方案似乎都不适合我。在我的例子中,我在Fragment中有一个ListView,我正在替换FragmentTransaction中的ListView,所以每次显示片段时都会创建一个新的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 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;
}
}

我已经看到这个工作暂时不打扰用户,我希望它为您工作

使用下面的代码:

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

我使用FirebaseListAdapter,不能得到任何解决方案的工作。我最后做了这个。我猜有更优雅的方式,但这是一个完整的和有效的解决方案。

在onCreate之前:

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

原:

@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的这个问题,所以我在这里也发布了它的解决方案:

在onCreate之前:

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

原:

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

为了澄清瑞安·纽森的优秀答案,并针对片段和通常情况进行调整,我们希望从“主”ListView片段导航到“details”片段,然后再返回“主”

    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的适配器,所以一切都保持不变!