背水一战 Windows 10 (57) - 控件(集合类): ListViewBase - 增量加载, 分步绘制
背水一战 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
[源码下载]