From 141031632d19b0ef626e635dac6de53da12282c5 Mon Sep 17 00:00:00 2001 From: xiaoy312 Date: Wed, 16 Feb 2022 17:55:17 -0500 Subject: [PATCH] feat(ListView): incremental loading support for skia/wasm --- src/Uno.Foundation/IndexPath.cs | 2 + .../Given_ListViewBase.cs | 146 +++++++++++++++++- .../ListViewBase/ListViewBase.managed.cs | 26 +++- .../VirtualizingPanelLayout.managed.cs | 8 +- 4 files changed, 176 insertions(+), 6 deletions(-) diff --git a/src/Uno.Foundation/IndexPath.cs b/src/Uno.Foundation/IndexPath.cs index e8d121107202..4e4e8c6e54bf 100644 --- a/src/Uno.Foundation/IndexPath.cs +++ b/src/Uno.Foundation/IndexPath.cs @@ -32,6 +32,8 @@ int IComparable.CompareTo(IndexPath other) public static IndexPath Zero { get; } = new IndexPath(); + public static IndexPath NotFound { get; } = new IndexPath(-1, 0); + public static bool operator <(IndexPath indexPath1, IndexPath indexPath2) { return Compare(indexPath1, indexPath2) < 0; diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs index 71acc98247bc..68ce1a1a6a50 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -30,6 +30,7 @@ using System.Runtime.CompilerServices; using Windows.UI.Xaml.Data; using Uno.UI.RuntimeTests.Extensions; +using System.Runtime.InteropServices.WindowsRuntime; namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls { @@ -1766,9 +1767,114 @@ public async Task When_Pool_Aware_View_In_Item_Template() } } - private bool ApproxEquals(double value1, double value2) => Math.Abs(value1 - value2) <= 2; + [TestMethod] + public async Task When_Incremental_Load() + { + const int BatchSize = 25; + + // setup + var container = new Grid { Height = 210, VerticalAlignment = VerticalAlignment.Bottom }; + + var list = new ListView + { + ItemContainerStyle = BasicContainerStyle, + ItemTemplate = FixedSizeItemTemplate // height=29 + }; + container.Children.Add(list); + + var source = new InfiniteSource(async start => + { + await Task.Delay(25); + return Enumerable.Range(start, BatchSize).ToArray(); + }); + list.ItemsSource = source; + + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(list); + await Task.Delay(1000); + var initial = GetCurrenState(); + + // scroll to bottom + ScrollBy(list, 10000); + await Task.Delay(500); + await WindowHelper.WaitForIdle(); + var firstScroll = GetCurrenState(); + + // scroll to bottom + ScrollBy(list, 10000); + await Task.Delay(500); + await WindowHelper.WaitForIdle(); + var secondScroll = GetCurrenState(); + + Assert.AreEqual(BatchSize * 1, initial.LastLoaded, "Should start with first batch loaded."); + Assert.AreEqual(BatchSize * 2, firstScroll.LastLoaded, "Should have 2 batches loaded after first scroll."); + Assert.IsTrue(initial.LastMaterialized < firstScroll.LastMaterialized, "No extra item materialized after first scroll."); + Assert.AreEqual(BatchSize * 3, secondScroll.LastLoaded, "Should have 3 batches loaded after second scroll."); + Assert.IsTrue(firstScroll.LastMaterialized < secondScroll.LastMaterialized, "No extra item materialized after second scroll."); + + (int LastLoaded, int LastMaterialized) GetCurrenState() => + ( + source.LastIndex, + Enumerable.Range(0, source.LastIndex).Reverse().FirstOrDefault(x => list.ContainerFromIndex(x) != null) + ); + } + + [TestMethod] + public async Task When_Incremental_Load_ShouldStop() + { + const int BatchSize = 25; + + // setup + var container = new Grid { Height = 210, VerticalAlignment = VerticalAlignment.Bottom }; + + var list = new ListView + { + ItemContainerStyle = BasicContainerStyle, + ItemTemplate = FixedSizeItemTemplate // height=29 + }; + container.Children.Add(list); + + var source = new InfiniteSource(async start => + { + await Task.Delay(25); + return Enumerable.Range(start, BatchSize).ToArray(); + }); + list.ItemsSource = source; + + WindowHelper.WindowContent = container; + await WindowHelper.WaitForLoaded(list); + await Task.Delay(1000); + var initial = GetCurrenState(); + // scroll to bottom + ScrollBy(list, 10000); + await Task.Delay(500); + await WindowHelper.WaitForIdle(); + var firstScroll = GetCurrenState(); + + // Has'No'MoreItems + source.HasMoreItems = false; + + // scroll to bottom + ScrollBy(list, 10000); + await Task.Delay(500); + await WindowHelper.WaitForIdle(); + var secondScroll = GetCurrenState(); + + Assert.AreEqual(BatchSize * 1, initial.LastLoaded, "Should start with first batch loaded."); + Assert.AreEqual(BatchSize * 2, firstScroll.LastLoaded, "Should have 2 batches loaded after first scroll."); + Assert.IsTrue(initial.LastMaterialized < firstScroll.LastMaterialized, "No extra item materialized after first scroll."); + Assert.AreEqual(BatchSize * 2, secondScroll.LastLoaded, "Should still have 2 batches loaded after first scroll since HasMoreItems was false."); + Assert.AreEqual(BatchSize * 2 - 1, secondScroll.LastMaterialized, "Last materialized item should be the last from 2nd batch (50th/index=49)."); + + (int LastLoaded, int LastMaterialized) GetCurrenState() => + ( + source.LastIndex, + Enumerable.Range(0, source.LastIndex).Reverse().FirstOrDefault(x => list.ContainerFromIndex(x) != null) + ); + } + private bool ApproxEquals(double value1, double value2) => Math.Abs(value1 - value2) <= 2; #region Helper classes private class When_Removed_From_Tree_And_Selection_TwoWay_Bound_DataContext : System.ComponentModel.INotifyPropertyChanged @@ -1852,8 +1958,10 @@ public string Display } } } + #endregion } + #region Helper classes public partial class OnItemsChangedListView : ListView { public Action ItemsChangedAction = null; @@ -2015,5 +2123,39 @@ protected override DataTemplate SelectTemplateCore(object item) } } } + + public class InfiniteSource : ObservableCollection, ISupportIncrementalLoading + { + public delegate Task AsyncFetch(int start); + public delegate T[] Fetch(int start); + + private readonly AsyncFetch _fetchAsync; + private int _start; + + public InfiniteSource(AsyncFetch fetch) + { + _fetchAsync = fetch; + _start = 0; + } + + public IAsyncOperation LoadMoreItemsAsync(uint count) + { + return AsyncInfo.Run(async ct => + { + var items = await _fetchAsync(_start); + foreach (var item in items) + { + Add(item); + } + _start += items.Length; + + return new LoadMoreItemsResult { Count = count }; + }); + } + + public bool HasMoreItems { get; set; } = true; + + public int LastIndex => _start; + } #endregion } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.managed.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.managed.cs index 4bfb397ac842..8b6b15c2fa30 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.managed.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.managed.cs @@ -11,7 +11,26 @@ namespace Windows.UI.Xaml.Controls { public partial class ListViewBase { - private int PageSize => throw new NotImplementedException(); + private int PageSize + { + get + { + if (VirtualizingPanel is null) + { + return 0; + } + + var layouter = VirtualizingPanel.GetLayouter(); + var firstVisibleIndex = layouter.FirstVisibleIndex; + var lastVisibleIndex = layouter.LastVisibleIndex; + if (lastVisibleIndex == -1) + { + return 0; + } + + return lastVisibleIndex - firstVisibleIndex + 1; + } + } private void AddItems(int firstItem, int count, int section) { @@ -56,7 +75,10 @@ private void ReplaceGroup(int groupIndexInView) private void TryLoadMoreItems() { - //TODO: ISupportIncrementalLoading + if (VirtualizingPanel.GetLayouter() is { } layouter) + { + TryLoadMoreItems(layouter.LastVisibleIndex); + } } } } diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.managed.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.managed.cs index 5b57bcc68d94..bb502ecba2e9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.managed.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/VirtualizingPanelLayout.managed.cs @@ -259,6 +259,10 @@ private void OnScrollChanged(object? sender, ScrollViewerViewChangedEventArgs e) unappliedDelta -= scrollIncrement; unappliedDelta = Max(0, unappliedDelta); UpdateLayout(extentAdjustment: sign * -unappliedDelta, isScroll: true); + +#if __WASM__ || __SKIA__ + (ItemsControl as ListViewBase)?.TryLoadMoreItems(LastVisibleIndex); +#endif } ArrangeElements(_availableSize, ViewportSize); UpdateCompleted(); @@ -817,12 +821,12 @@ private void OnOrientationChanged(Orientation newValue) private Uno.UI.IndexPath GetFirstVisibleIndexPath() { - throw new NotImplementedException(); //TODO: FirstVisibleIndex + return GetFirstMaterializedLine()?.FirstItem ?? Uno.UI.IndexPath.NotFound; } private Uno.UI.IndexPath GetLastVisibleIndexPath() { - throw new NotImplementedException(); //TODO: LastVisibleIndex + return GetLastMaterializedLine()?.LastItem ?? Uno.UI.IndexPath.NotFound; } private IEnumerable GetSnapPointsInner(SnapPointsAlignment alignment)