返回ListView时保持/保存/还原滚动位置


397

很长一段时间ListView,用户可以在返回上一个屏幕之前先滚动一下。当用户ListView再次打开它时,我希望列表滚动到以前的相同位置。关于如何实现这一目标的任何想法?


22
我认为Eugene Mymrin / Giorgio Barchiesi提到的解决方案要比公认的答案更好
Mira Weller

Answers:


631

尝试这个:

// 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
不太了解这是如何工作的。我只使用listView.getFirstVisiblePosition()并理解这一点,但不确定这里发生了什么。有机会作简要说明吗?:)
HXCaine 2010年

56
因此,ListView.getFirstVisiblePosition()返回顶部可见的列表项。但是,该项目可能会部分滚动到视线之外,如果要恢复列表的确切滚动位置,则需要获取此偏移量。因此,ListView.getChildAt(0)返回顶部列表项的View,然后View.getTop()返回距ListView顶部的相对偏移量。然后,要恢复ListView的滚动位置,我们调用ListView.setSelectionFromTop(),它具有所需项目的索引和一个偏移量,以将其顶部边缘定位在ListView的顶部。全清?
2011年

15
使用一行保存:,int index = mList.getFirstVisiblePosition();而只需一行恢复:,这可以变得更加简单mList.setSelectionFromTop(index, 0);。很好的答案(+1)!我一直在寻找解决这个问题的优雅方法。
菲尔(Phil)

17
@Phil您的简单解决方案无法恢复精确的滚动位置。
Ixx

2
正如@nbarraille所说,代码应读得更像“ v.getTop()-mList.getPaddingTop()”。否则,您将花费一个小时,就像我试图弄清楚为什么它总是
能使

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

106
我认为这是最好的答案,因为此方法不仅可以还原第一个可见项目,还可以还原确切的滚动位置。
Mira Weller

9
很好的答案,但在动态加载内容并且您想要在onPause()方法中保存状态时不起作用。
Gio 2012年

13
很好的解决方案。只是一个问题。如果项目很大并且可以滚动,但是第一个项目仍然可见,则列表会跳回到顶部。如果您滚动到第二个项目或其他任何地方,则一切正常
passsy 2013年

4
@passsy您是否找到了一个解决方案,使列表重新回到顶部?
2014年

2
作为aaronvargas说,当时ListView.getFirstVisiblePosition()为0,这可能无法正常工作
VinceStyling

54

我采用了@(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设备上,当您更改手机方向以及编辑项目并返回列表时,此方法均适用。


1
这个解决方案对我有用。其他人没有。关于在ListView中删除和插入项目,保存/还原ListView实例状态的行为如何?
布莱恩

2
仅供参考,如果您在一个片段中使用此片段(我正在使用列表片段)并导航到其他片段,这将崩溃,因为在mListState = getListView().onSaveInstanceState().主要通过设备旋转调用时未创建内容视图。
Mgamerz 2014年

2
onRestoreInstanceState从来没有被称为:(
dVaffection 2015年

1
@GiorgioBarchiesi谁是@(柯克·伍尔),其解决方案适合您?用户显然改变了他/她的名字。
Piovezan

1
是的,这是官方宗教。但就我而言,数据是在本地加载的,长度非常有限,并且在一秒钟内加载,所以我很操心:-)
Giorgio Barchiesi 2015年

26

一个非常简单的方法:

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

2
您的答案有误。该方法称为setSelection
Janusz 2010年

这个想法很好用,但是有一个伪问题。第一个可见位置可能只显示了一点(可能只是该行的一小部分),而setSelection()将该行设置为在还原时完全可见制作。
rantravee 2010年

请看看我的第二个选择。我刚刚把它添加到我的第一个答案
弗朗西斯Laurita

27
view.getScrollX()和view.getScrollY()始终返回0!
rantravee

3
看看我上面使用View.getChildAt()和View.getTop()的解决方案。您不能在ListView上使用View.getScrollY(),因为ListView在内部维护其滚动。
2011年

17

我发现了一些有趣的东西。

我尝试了setSelection和scrolltoXY,但是它根本不起作用,列表保持在同一位置,经过反复试验,我得到了下面的代码

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

如果您尝试运行runOnUiThread而不是发布Runnable,则它也不起作用(至少在某些设备上)

对于某些应该直接解决的问题,这是一个非常奇怪的解决方法。


1
我遇到了同样的问题!而且setSelectionFromTop()不起作用。
ofavre '02

+1。listnew.post()在某些设备上不起作用,在某些其他设备上它在某些情况下不起作用,但是runOnUIThread可以正常工作。
奥马尔·雷曼

@Omar,能否举一些无法使用的设备和操作系统的示例?我尝试了HTC G2(2.3.4)和Galaxy Nexus(4.1.2),并且两者均可工作。此线程中的其他答案均不适用于我的任何设备。
Dan J

2
listview.post在4.0.4的Galaxy Note上可以正常运行,但在2.3.6的Nexus One上不能正常运行。但是,onOnUIThread(action)在两个设备上都能正常工作。
奥马尔·雷曼

11

警告!!如果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);

它就像一种魅力,但我不完全理解。如果第一项仍然可见,那么得到下一个孩子有什么意义?setSelectionFromTop与新索引不应该导致列表从第二个孩子开始显示吗?我怎么仍会看到第一个?
tafi

@tafi,第一项可能是“部分”可见的。因此,该项目的顶部将在屏幕上方,因此将为负数。(这会引起其他我不记得的问题...)因此,我们使用第二个项目的“顶部”进行放置,该位置应始终可用(?),否则第一位置将不会滚动!
aaronvargas

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

够了


嗨,您上面的代码能为我提供一个RecyclerView列表吗,其中活动之间没有保存instancestate?我在这里发布了以下问题:stackoverflow.com/questions/35413495/…
AJW

6

对于某些寻求解决此问题的方法的人,问题的根源可能是您设置列表视图适配器的位置。在列表视图上设置适配器后,它将重置滚动位置。只是要考虑的事情。在获取对列表视图的引用之后,我将适配器的设置移到了onCreateView中,它为我解决了这个问题。=)


1

正在发布此消息,因为我很惊讶没有人提及此消息。

用户单击后退按钮后,他将以退出状态相同的状态返回到列表视图。

该代码将覆盖“上”按钮,使其行为与后退按钮相同,因此对于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)); }

警告:如果您可以从明细活动中转到另一个活动,则向上按钮将使您返回该活动,因此您必须操纵后退按钮历史才能使其起作用。



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

您必须打电话通知后才知道!


1

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

val state = myList.layoutManager.onSaveInstanceState()

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

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

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

0

如果您要保存/恢复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处的特殊虚拟零像素高度视图。这是简单的示例项目,显示ListViewExpandableListView恢复其精细滚动位置,但未明确保存其滚动位置/已恢复。通过临时切换到其他应用程序,双屏旋转并切换回测试应用程序,即使在复杂的情况下,也可以完美地恢复精细滚动位置。请注意,如果您明确退出该应用程序(按“后退”按钮),则不会保存滚动位置(所有其他视图也不会保存其状态)。 https://github.com/voromto/RestoreScrollPosition/releases


0

对于从ListActivity派生的,使用SimpleCursorAdapter实现LoaderManager.LoaderCallbacks的活动,它无法恢复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;

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

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

我暂时看到了这项工作,而没有打扰到用户,希望它对您有用


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

0

我正在使用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;
    }
}

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解决此问题此处发布了解决方案:

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

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

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的适配器,因此一切都保留了!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.