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


当前回答

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

其他回答

警告! !在AbsListView中有一个错误,如果ListView.getFirstVisiblePosition()为0,则不允许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”将是一个负数,这将导致其他问题,阻止状态被正确恢复。所以当'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);

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

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

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

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

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

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

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

这里提供的解决方案似乎都不适合我。在我的情况下,我有一个ListView在一个片段,我替换在一个FragmentTransaction,所以一个新的片段实例创建每次片段显示,这意味着ListView状态不能存储为片段的成员。

相反,我最终将状态存储在我的自定义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);
}

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

val state = myList.layoutManager.onSaveInstanceState()

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

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