You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 11:06:02