背水一戰 Windows 10 (57) - 控件(集合類): ListViewBase - 增量加載, 分步繪制
作者:webabcd
介紹
背水一戰 Windows 10 之 控件(集合類 - ListViewBase)
- 增量加載
- 分步繪制(大數據量流暢滾動)
示例
1、ListViewBase 的增量加載
Controls/CollectionControl/ListViewBaseDemo/MyIncrementalLoading.cs
/* * 演示如何實現 ISupportIncrementalLoading 接口,以便為 ListViewBase 的增量加載提供數據 * * * ISupportIncrementalLoading - 用於支持增量加載 * HasMoreItems - 是否還有更多的數據 * IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) - 異步加載指定數量的數據(增量加載) * * LoadMoreItemsResult - 增量加載的結果 * Count - 實際已加載的數據量 */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Windows.Foundation; using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Data; namespace Windows10.Controls.CollectionControl.ListViewBaseDemo { public class MyIncrementalLoading<T> : ObservableCollection<T>, ISupportIncrementalLoading { // 是否正在異步加載中 private bool _isBusy = false; // 提供數據的 Func // 第一個參數:增量加載的起始索引;第二個參數:需要獲取的數據量;第三個參數:獲取到的數據集合 private Func<int, int, List<T>> _funcGetData; // 最大可顯示的數據量 private uint _totalCount = 0; /// <summary> /// 構造函數 /// </summary> /// <param name="totalCount">最大可顯示的數據量</param> /// <param name="getDataFunc">提供數據的 Func</param> public MyIncrementalLoading(uint totalCount, Func<int, int, List<T>> getDataFunc) { _funcGetData = getDataFunc; _totalCount = totalCount; } /// <summary> /// 是否還有更多的數據 /// </summary> public bool HasMoreItems { get { return this.Count < _totalCount; } } /// <summary> /// 異步加載數據(增量加載) /// </summary> /// <param name="count">需要加載的數據量</param> /// <returns></returns> public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) { if (_isBusy) { throw new InvalidOperationException("忙着呢,先不搭理你"); } _isBusy = true; var dispatcher = Window.Current.Dispatcher; return AsyncInfo.Run ( (token) => Task.Run<LoadMoreItemsResult> ( async () => { try { // 模擬長時任務 await Task.Delay(1000); // 增量加載的起始索引 var startIndex = this.Count; await dispatcher.RunAsync ( CoreDispatcherPriority.Normal, () => { // 通過 Func 獲取增量數據 var items = _funcGetData(startIndex, (int)count); foreach (var item in items) { this.Add(item); } } ); // Count - 實際已加載的數據量 return new LoadMoreItemsResult { Count = (uint)this.Count }; } finally { _isBusy = false; } }, token ) ); } } }
Controls/CollectionControl/ListViewBaseDemo/ListViewBaseDemo3.xaml
<Page x:Class="Windows10.Controls.CollectionControl.ListViewBaseDemo.ListViewBaseDemo3" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Windows10.Controls.CollectionControl.ListViewBaseDemo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="Transparent" Margin="10 0 10 10"> <TextBlock Name="lblMsg" /> <ListView x:Name="listView" Width="300" Height="300" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0 30 0 0"> <ListView.ItemTemplate> <DataTemplate> <Border Background="Blue" Width="200" CornerRadius="3" HorizontalAlignment="Left"> <TextBlock Text="{Binding Name}" /> </Border> </DataTemplate> </ListView.ItemTemplate> </ListView> <TextBlock Name="lblLog" Margin="0 350 0 0" /> </Grid> </Page>
Controls/CollectionControl/ListViewBaseDemo/ListViewBaseDemo3.xaml.cs
/* * ListViewBase(基類) - 列表控件基類(繼承自 Selector, 請參見 /Controls/SelectionControl/SelectorDemo.xaml) * IncrementalLoadingTrigger - 增量加載的觸發器 * Edge - 允許觸發增量加載,默認值 * None - 禁止觸發增量加載 * DataFetchSize - 預提數據的大小,默認值 3.0 * 本例將此值設置為 4.0 ,其效果為(注:本例中的 ListView 每頁可顯示的數據量為 6 條或 7 條,以下計算需基於此) * 1、先獲取 1 條數據,為的是盡量快地顯示數據 * 2、再獲取 4.0 * 1 條數據 * 3、再獲取 4.0 * (6 或 7,如果 ListView 當前顯示了 6 條數據則為 6,如果 ListView 當前顯示了 7 條數據則為 7) 條數據 * 4、以后每次到達閾值后,均增量加載 4.0 * (6 或 7,如果 ListView 當前顯示了 6 條數據則為 6,如果 ListView 當前顯示了 7 條數據則為 7) 條數據 * IncrementalLoadingThreshold - 增量加載的閾值,默認值 0.0 * 本例將此值設置為 2.0 ,其效果為(注:本例中的 ListView 每頁可顯示的數據量為 6 條或 7 條) * 1、滾動中,如果已准備好的數據少於 2.0 * (6 或 7,如果 ListView 當前顯示了 6 條數據則為 6,如果 ListView 當前顯示了 7 條數據則為 7) 條數據,則開始增量加載 * * * 本例用於演示如何實現 ListViewBase 的增量加載(數據源需要實現 ISupportIncrementalLoading 接口,詳見:MyIncrementalLoading.cs) */ using Windows.UI.Xaml.Controls; using System.Linq; using System.Collections.Specialized; using System; using Windows10.Common; using Windows.UI.Xaml; namespace Windows10.Controls.CollectionControl.ListViewBaseDemo { public sealed partial class ListViewBaseDemo3 : Page { // 實現了增量加載的數據源 private MyIncrementalLoading<Employee> _employees; public ListViewBaseDemo3() { this.InitializeComponent(); this.Loaded += ListViewBaseDemo3_Loaded; } private void ListViewBaseDemo3_Loaded(object sender, RoutedEventArgs e) { listView.IncrementalLoadingTrigger = IncrementalLoadingTrigger.Edge; listView.DataFetchSize = 4.0; listView.IncrementalLoadingThreshold = 2.0; _employees = new MyIncrementalLoading<Employee>(1000, (startIndex, count) => { lblLog.Text += string.Format("從索引 {0} 處開始獲取 {1} 條數據", startIndex, count); lblLog.Text += Environment.NewLine; return TestData.GetEmployees().Skip(startIndex).Take(count).ToList(); }); _employees.CollectionChanged += _employees_CollectionChanged; listView.ItemsSource = _employees; } void _employees_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { lblMsg.Text = "已獲取的數據量:" + _employees.Count.ToString(); } } }
2、ListViewBase 的分步繪制
Controls/CollectionControl/ListViewBaseDemo/ListViewBaseDemo4.xaml
<Page x:Class="Windows10.Controls.CollectionControl.ListViewBaseDemo.ListViewBaseDemo4" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Windows10.Controls.CollectionControl.ListViewBaseDemo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="Transparent"> <!-- ListViewBase(基類) - 列表控件基類 ContainerContentChanging - 數據虛擬化時,項容器的內容發生變化時觸發的事件(僅 ItemsStackPanel 和 ItemsWrapGrid 有效) --> <GridView x:Name="gridView" Margin="10 0 10 10" ContainerContentChanging="gridView_ContainerContentChanging"> <GridView.ItemTemplate> <DataTemplate> <StackPanel Width="80" Height="80" Background="Blue"> <Rectangle x:Name="placeholderRectangle" Fill="Red" Height="10" Opacity="0" /> <TextBlock x:Name="lblName" Text="{Binding Name}" Foreground="Yellow" /> <TextBlock x:Name="lblAge" Text="{Binding Age}" Foreground="Aqua" /> <TextBlock x:Name="lblIsMale" Text="{Binding IsMale}" Foreground="Gray" /> </StackPanel> </DataTemplate> </GridView.ItemTemplate> </GridView> </Grid> </Page>
Controls/CollectionControl/ListViewBaseDemo/ListViewBaseDemo4.xaml.cs
/* * ListViewBase(基類) - 列表控件基類(繼承自 Selector, 請參見 /Controls/SelectionControl/SelectorDemo.xaml) * ContainerContentChanging - 數據虛擬化時,項容器的內容發生變化時觸發的事件(僅 ItemsStackPanel 和 ItemsWrapGrid 有效) * * * 當 ListViewBase 的一屏需要顯示的數據量極大時(一屏的 item 多,且每個 item 中的 element 也多),由於每次滾動時需要繪制當前屏的每個 element,這需要占用大量的 ui 資源,所以就會有一些卡頓 * 為了解決這個問題 uwp 給出了兩種解決方案 * 1、設置 ListViewBase 的 ShowsScrollingPlaceholders 屬性為 true(默認值),每次顯示 item 時先顯示占位符(尚不清楚怎么修改這個占位符的背景色),然后再繪制內容 * 相關演示請參見:/Controls/CollectionControl/ListViewBaseDemo/ListViewBaseDemo1.xaml * 2、通過 ListViewBase 的 ContainerContentChanging 事件,分步繪制 item 中的 element * 本例即介紹這種方法。注意在 uwp 中已經不用這么麻煩了,可以通過 x:Bind 和 x:Phase 來實現,請參見:/Bind/PhaseDemo.xaml * * * 本例用於演示如何實現 ListViewBase 的分步繪制(大數據量流暢滾動) * * * 注: * 虛擬化布局控件用於減少創建的 item 數量 * 分步繪制用於在繪制 item 時,分階段繪制 item 上的元素 */ using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Navigation; using Windows.UI.Xaml.Shapes; using Windows10.Common; namespace Windows10.Controls.CollectionControl.ListViewBaseDemo { public sealed partial class ListViewBaseDemo4 : Page { public ListViewBaseDemo4() { this.InitializeComponent(); } protected override void OnNavigatedTo(NavigationEventArgs e) { gridView.ItemsSource = TestData.GetEmployees(1000); // 默認值是 true,即為了保證流暢,每次顯示 item 時先會顯示占位符(application 級的背景色塊),然后再繪制內容 // 本例演示 ContainerContentChanging 事件的使用,所以不會用到這個 gridView.ShowsScrollingPlaceholders = false; } private void gridView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) { // 交由我處理吧(不用系統再處理了) args.Handled = true; // 第 1 階段繪制 // args.Phase.ToString(); // 0 StackPanel templateRoot = (StackPanel)args.ItemContainer.ContentTemplateRoot; Rectangle placeholderRectangle = (Rectangle)templateRoot.FindName("placeholderRectangle"); TextBlock lblName = (TextBlock)templateRoot.FindName("lblName"); TextBlock lblAge = (TextBlock)templateRoot.FindName("lblAge"); TextBlock lblIsMale = (TextBlock)templateRoot.FindName("lblIsMale"); // 顯示自定義占位符(也可以不用這個,而是直接顯示 item 的背景) placeholderRectangle.Opacity = 1; // 除了占位符外,所有 item 全部暫時不繪制 lblName.Opacity = 0; lblAge.Opacity = 0; lblIsMale.Opacity = 0; // 開始下一階段的繪制 args.RegisterUpdateCallback(ShowName); } private void ShowName(ListViewBase sender, ContainerContentChangingEventArgs args) { // 第 2 階段繪制 // args.Phase.ToString(); // 1 Employee employee = (Employee)args.Item; SelectorItem itemContainer = (SelectorItem)args.ItemContainer; StackPanel templateRoot = (StackPanel)itemContainer.ContentTemplateRoot; TextBlock lblName = (TextBlock)templateRoot.FindName("lblName"); // 繪制第 2 階段的內容 lblName.Text = employee.Name; lblName.Opacity = 1; // 開始下一階段的繪制 args.RegisterUpdateCallback(ShowAge); } private void ShowAge(ListViewBase sender, ContainerContentChangingEventArgs args) { // 第 3 階段繪制 // args.Phase.ToString(); // 2 Employee employee = (Employee)args.Item; SelectorItem itemContainer = (SelectorItem)args.ItemContainer; StackPanel templateRoot = (StackPanel)itemContainer.ContentTemplateRoot; TextBlock lblAge = (TextBlock)templateRoot.FindName("lblAge"); // 繪制第 3 階段的內容 lblAge.Text = employee.Age.ToString(); lblAge.Opacity = 1; // 開始下一階段的繪制 args.RegisterUpdateCallback(ShowIsMale); } private void ShowIsMale(ListViewBase sender, ContainerContentChangingEventArgs args) { // 第 4 階段繪制 // args.Phase.ToString(); // 3 Employee employee = (Employee)args.Item; SelectorItem itemContainer = (SelectorItem)args.ItemContainer; StackPanel templateRoot = (StackPanel)itemContainer.ContentTemplateRoot; Rectangle placeholderRectangle = (Rectangle)templateRoot.FindName("placeholderRectangle"); TextBlock lblIsMale = (TextBlock)templateRoot.FindName("lblIsMale"); // 繪制第 4 階段的內容 lblIsMale.Text = employee.IsMale.ToString(); lblIsMale.Opacity = 1; // 隱藏自定義占位符 placeholderRectangle.Opacity = 0; } } }
OK
[源碼下載]