實現 ListView 下拉刷新與上拉加載更多時,調用 adapter.notifyDataSetChanged() 後,列表自動跳回第一項(位置 0),導致用戶閱讀位置丟失。
notifyDataSetChanged() 會觸發 ListView 重新佈局,默認情況下 ListView 會嘗試恢復第一個可見項的位置。若數據源變化(如清空後重新添加),原有位置信息失效,導致跳轉頂部。
// 刷新前記錄第一個可見項位置及偏移
int firstVisiblePosition = listView.getFirstVisiblePosition();
View firstVisibleView = listView.getChildAt(0);
int topOffset = (firstVisibleView == null) ? 0 : firstVisibleView.getTop();
// 異步任務完成後
adapter.notifyDataSetChanged();
// 恢復位置
listView.setSelectionFromTop(firstVisiblePosition, topOffset);
// 在 onSaveInstanceState 保存
Parcelable state = listView.onSaveInstanceState();
// 刷新數據後恢復
adapter.notifyDataSetChanged();
listView.onRestoreInstanceState(state);
若是上拉加載更多,**不要清空列表重建**,直接在現有數據尾部追加:
// 正確做法:追加
mDataList.addAll(newData);
adapter.notifyDataSetChanged();
// 可選:平滑滾動到新數據起始位置
listView.smoothScrollToPosition(oldSize);
錯誤做法:
// 錯誤:清空重建導致跳頂
mDataList.clear();
mDataList.addAll(allData); // 包含舊數據+新數據
adapter.notifyDataSetChanged();
listView.setOnRefreshListener(() -> {
// 1. 記錄當前滾動狀態
int firstPos = listView.getFirstVisiblePosition();
View firstView = listView.getChildAt(0);
int offset = (firstView != null) ? firstView.getTop() : 0;
new AsyncTask<Void, Void, List<Item>>() {
@Override
protected List<Item> doInBackground(Void... voids) {
return fetchNewData(); // 網絡請求
}
@Override
protected void onPostExecute(List<Item> newData) {
// 2. 更新數據源(替換或合併)
dataList.clear();
dataList.addAll(newData);
adapter.notifyDataSetChanged();
// 3. 恢復滾動位置
listView.setSelectionFromTop(firstPos, offset);
listView.onRefreshComplete();
}
}.execute();
});
RecyclerView + LinearLayoutManager 可通過 onSaveInstanceState() / onRestoreInstanceState() 完美保持滾動位置,且支援動畫、項目飾品等現代特性。建議新項目直接使用 RecyclerView。
getFirstVisiblePosition() 和 getChildAt(0).getTop()。setSelectionFromTop() 恢復。clear() 重建列表。掌握滾動狀態保存與恢復,是列表類交互體驗優化的核心技能。