UWP如何使用IncrementalLoadingCollection实现ListView双向滚动增量加载
解决方案
原生IncrementalLoadingCollection仅支持单向向下滚动加载,设计逻辑是仅往集合尾部追加数据,无法直接支持向上滚动往头部插入数据的需求,你可以通过自定义双向增量加载集合+监听滚动位置的方式实现需求,以下是完整实现方案:
1. 自定义双向增量加载集合
public interface IBiDirectionalIncrementalSource<T> { // 加载更旧的数据(向上滚动时加载,插入到集合头部) Task<IEnumerable<T>> GetPreviousItemsAsync(int pageSize); // 加载更新的数据(向下滚动时加载,追加到集合尾部) Task<IEnumerable<T>> GetNextItemsAsync(int pageSize); // 是否还有更早的数据可以加载 bool HasPreviousItems { get; } // 是否还有更新的数据可以加载 bool HasNextItems { get; } } public class BiDirectionalIncrementalLoadingCollection<T> : ObservableCollection<T> { private readonly IBiDirectionalIncrementalSource<T> _source; private readonly int _pageSize; private bool _isLoading; public BiDirectionalIncrementalLoadingCollection(IBiDirectionalIncrementalSource<T> source, int pageSize = 20) { _source = source; _pageSize = pageSize; } // 初始加载中间页数据 public async Task LoadInitialPageAsync(IEnumerable<T> initialItems) { foreach (var item in initialItems) { Add(item); } } // 加载前一页(向上滚动触发) public async Task LoadPreviousPageAsync() { if (_isLoading || !_source.HasPreviousItems) return; _isLoading = true; var items = await _source.GetPreviousItemsAsync(_pageSize); // 插入到集合头部 for (int i = items.Count() - 1; i >= 0; i--) { Insert(0, items.ElementAt(i)); } _isLoading = false; } // 加载后一页(向下滚动触发) public async Task LoadNextPageAsync() { if (_isLoading || !_source.HasNextItems) return; _isLoading = true; var items = await _source.GetNextItemsAsync(_pageSize); foreach (var item in items) { Add(item); } _isLoading = false; } }
2. 实现数据源
public class PeopleSource : IBiDirectionalIncrementalSource<Person> { private readonly List<Person> _allPeople; // 当前加载到的最旧数据的索引 private int _currentMinIndex; // 当前加载到的最新数据的索引 private int _currentMaxIndex; public bool HasPreviousItems => _currentMinIndex > 0; public bool HasNextItems => _currentMaxIndex < _allPeople.Count - 1; public PeopleSource() { // 模拟全量数据,实际场景可从接口拉取 _allPeople = Enumerable.Range(0, 1000).Select(x => new Person { Name = $"用户{x}" }).ToList(); // 初始中间页索引,比如总共有1000条,中间是500的位置 _currentMinIndex = 490; _currentMaxIndex = 509; } // 获取初始中间页数据 public IEnumerable<Person> GetInitialItems() { return _allPeople.Skip(_currentMinIndex).Take(20); } public async Task<IEnumerable<Person>> GetPreviousItemsAsync(int pageSize) { await Task.Delay(100); // 模拟网络请求延迟 var startIndex = Math.Max(0, _currentMinIndex - pageSize); var count = _currentMinIndex - startIndex; var items = _allPeople.Skip(startIndex).Take(count).ToList(); _currentMinIndex = startIndex; return items; } public async Task<IEnumerable<Person>> GetNextItemsAsync(int pageSize) { await Task.Delay(100); // 模拟网络请求延迟 var startIndex = _currentMaxIndex + 1; var count = Math.Min(pageSize, _allPeople.Count - startIndex); var items = _allPeople.Skip(startIndex).Take(count).ToList(); _currentMaxIndex = startIndex + count - 1; return items; } }
3. XAML 增加滚动监听
<ListView x:Name="PeopleListView" ScrollViewer.VerticalScrollBarVisibility="Visible"> <ListView.ItemTemplate> <DataTemplate x:DataType="local:Person"> <TextBlock Text="{x:Bind Name}" /> </DataTemplate> </ListView.ItemTemplate> </ListView>
4. 后台逻辑绑定+滚动事件处理
private BiDirectionalIncrementalLoadingCollection<Person> _collection; private PeopleSource _source; private ScrollViewer _listScrollViewer; protected override async void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); _source = new PeopleSource(); _collection = new BiDirectionalIncrementalLoadingCollection<Person>(_source); // 加载初始中间页 await _collection.LoadInitialPageAsync(_source.GetInitialItems()); PeopleListView.ItemsSource = _collection; // 获取ListView内部的ScrollViewer,可通过VisualTreeHelper实现查找 _listScrollViewer = PeopleListView.FindDescendant<ScrollViewer>(); if (_listScrollViewer != null) { _listScrollViewer.ViewChanged += OnScrollViewChanged; // 初始滚动到中间位置,因为初始加载的是中间20条,滚动到第10条的位置就是视觉中间 PeopleListView.ScrollIntoView(_collection[10]); } } private async void OnScrollViewChanged(object sender, ScrollViewerViewChangedEventArgs e) { if (e.IsIntermediate) return; // 滚动未结束不处理 // 滚动到接近顶部,触发向上加载 if (_listScrollViewer.VerticalOffset < 10 && _source.HasPreviousItems) { var previousCount = _collection.Count; await _collection.LoadPreviousPageAsync(); // 加载完成后调整滚动位置,避免跳变 var addedCount = _collection.Count - previousCount; if (addedCount > 0) { // 40是单条Item高度,可根据实际布局调整 _listScrollViewer.ChangeView(null, _listScrollViewer.VerticalOffset + addedCount * 40, null); } } // 滚动到接近底部,触发向下加载 else if (_listScrollViewer.VerticalOffset > _listScrollViewer.ScrollableHeight - 10 && _source.HasNextItems) { await _collection.LoadNextPageAsync(); } }
注意点
- 单条Item高度固定的情况下,加载前一页后的滚动偏移计算会更准确,如果是动态高度,可以通过获取新增Item的总高度来调整偏移,避免加载后视觉跳变。
- 加载过程中加锁避免重复触发加载请求。
内容的提问来源于stack exchange,提问作者Bruno
相关产品推荐
相关产品推荐

