基于GridView LoadMoreAsync 设计的横向双向瀑布流 组件 源码分享
原理:
实现了ISupportIncrementalLoading 接口
完成了增量加载,
针对于本地对象无法释放的情况 增加了 相关的Func
同时 通过VisualTree 拿到了GridView中 HorizontalBar 来对滚动条的位置进行捕捉 与计算
通过计算后 来执行 虚化操作
如需 转载请声明博客作者
以下是源码:
IncrementalLoadingCollection 对象:
public class IncrementalLoadingCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading { // 是否正在异步加载中 private bool _isBusy = false; // 提供数据的 Func // 第一个参数:增量加载的起始索引;第二个参数:需要获取的数据量;第三个参数:获取到的数据集合 private Func<int, int, List<T>> _funcGetData; // 最大可显示的数据量 private uint _totalCount = 0; private Func<T, T> _actDisposeData; public int PageIndex = 0; public uint perCount = 0; /// <summary> /// 构造函数 /// </summary> /// <param name="totalCount">最大可显示的数据量</param> /// <param name="getDataFunc">提供数据的 Func</param> public IncrementalLoadingCollection(uint totalCount, Func<int, int, List<T>> getDataFunc, Func<T, T> actDisposeData) { _funcGetData = getDataFunc; _totalCount = totalCount; _actDisposeData = actDisposeData; } /// <summary> /// 是否还有更多的数据 /// </summary> public bool HasMoreItems { get { return this.Count < _totalCount; } } /// <summary> /// 异步加载数据(增量加载) /// </summary> /// <param name="count">需要加载的数据量</param> /// <returns></returns> public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) { perCount = count; if (_isBusy) { return AsyncInfo.Run((token) => Task.Run<LoadMoreItemsResult>(() => { return new LoadMoreItemsResult { Count = (uint)this.Count }; }, token)); } _isBusy = true; var dispatcher = Window.Current.Dispatcher; return AsyncInfo.Run( (token) => Task.Run<LoadMoreItemsResult>( async () => { try { //// 模拟长时任务 await Task.Delay(100); // 增量加载的起始索引 var startIndex = this.Count; await dispatcher.RunAsync( CoreDispatcherPriority.Normal, () => { PageIndex++; // 通过 Func 获取增量数据 var items = _funcGetData(startIndex, (int)count); if (items != null) foreach (var item in items) { this.Add(item); } }); // Count - 实际已加载的数据量 return new LoadMoreItemsResult { Count = (uint)this.Count }; } finally { _isBusy = false; } }, token)); } public void DisposeItemByStartAndEnd(long start, long end) { for (long i = start; i < end; i++) { _actDisposeData(this.Items[(int)i]); } } public void RemoveItemByRange(long start, long end) { for (long i = start; i < end; i++) { if (this.Items.Count > i) { this.RemoveItem((int)i); } } } public void Reset() { this.OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset)); } }
IncrementalLoadingGridView:
public class IncrementalLoadingGridView : GridView { #region Member Variables private ScrollBar _HorizontalScrollBar; private Dictionary<int, int> _pageOffsetDict = new Dictionary<int, int>(); private Dictionary<int, bool> _pageVirtualizingDict = new Dictionary<int, bool>(); private dynamic _filelist; #endregion #region Constants const string HORIZONTALSCROLLBAR_PARTNAME = "HorizontalScrollBar"; #endregion public IncrementalLoadingGridView() { this.Loaded += ((sender, e) => { this.OnApplyTemplate(); }); this.Unloaded += ((sender, e) => { _HorizontalScrollBar = Pollute.FindVisualChildByName<ScrollBar>(this, HORIZONTALSCROLLBAR_PARTNAME); if (null != _HorizontalScrollBar) _HorizontalScrollBar.ValueChanged -= ValueChanged; }); } protected override async void OnApplyTemplate() { base.OnApplyTemplate(); await this.WaitForLayoutUpdateAsync(); _HorizontalScrollBar = Pollute.FindVisualChildByName<ScrollBar>(this, HORIZONTALSCROLLBAR_PARTNAME); if (null != _HorizontalScrollBar) { _HorizontalScrollBar.ValueChanged += ValueChanged; } } protected override async void OnItemsChanged(object e) { if (null != this.ItemsSource) { InitPositionByValue(); } base.OnItemsChanged(e); if (null == this.ItemsSource) { await this.WaitForLoadedAsync(); InitPositionByValue(); } } private void InitPositionByValue() { _filelist = this.ItemsSource; CompositionTarget.Rendering += ((obj, args) => { var newValue = Convert.ToInt32(_HorizontalScrollBar.Value); int currentPageIndex = _filelist.PageIndex - 2; if (!_pageOffsetDict.ContainsKey(currentPageIndex)) { _pageOffsetDict[currentPageIndex] = newValue; _pageVirtualizingDict[currentPageIndex] = false; } }); } private int preIndex = 0; private int maxPageIndex = 0; private int minOffset; private async void ValueChanged(object sender, RangeBaseValueChangedEventArgs e) { if (null == this.ItemsSource) return; var newValue = Convert.ToInt32(e.NewValue); var oldValue = Convert.ToInt32(e.OldValue); if (newValue == oldValue) return; Debug.WriteLine("坐标:" + newValue); int currentPageIndex; if (null == _filelist) _filelist = this.ItemsSource; if (e.NewValue < 1.0) { await Task.Run(async () => { string text = "滑到头了 开始释放 释放前个数:" + _filelist.Count.ToString(); Debug.WriteLine(text); for (int i = 3; i < maxPageIndex; i++) { _pageVirtualizingDict[i] = true; var start = (i - 1) * _filelist.perCount; var end = i * _filelist.perCount - 1; await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => { if (_filelist.Count > end) { _filelist.DisposeItemByStartAndEnd(start, end); _filelist.RemoveItemByRange(start, end); } await Task.Delay(500); _filelist.Reset(); }); } _filelist.PageIndex = 2; text = "滑到头了 释放完毕 释放后个数:" + _filelist.Count; Debug.WriteLine(text); }); } else await Task.Run(() => { if (newValue > oldValue) { //lock (_pageOffsetDict) //{ var horiOffset = newValue - oldValue; if (minOffset > horiOffset) minOffset = horiOffset; currentPageIndex = _filelist.PageIndex - 2; //_pageOffsetDict[currentPageIndex] = newValue; maxPageIndex = Convert.ToInt32(_filelist.Count) / Convert.ToInt32(_filelist.perCount); //} if (preIndex != currentPageIndex && _pageOffsetDict.ContainsValue(newValue)) { Debug.WriteLine("坐标:" + newValue + " 上一页:" + preIndex + " 当前页:" + currentPageIndex); if (_pageVirtualizingDict.ContainsKey(preIndex)) { _pageVirtualizingDict[preIndex] = false; Debug.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@需要向后虚化:" + preIndex + "@@@@@@@@@@@@@@@@@@@@@@" + _pageVirtualizingDict[preIndex]); if (!_pageVirtualizingDict[preIndex] && preIndex > 3) { int i = preIndex; while (i > 3) { //if (!_pageVirtualizingDict.ContainsKey(i)) //{ // _pageVirtualizingDict[i] = false; // _pageOffsetDict[i] = oldValue -= minOffset; //} if (_pageVirtualizingDict.ContainsKey(i) && !_pageVirtualizingDict[i]) { var start = (i - 3) * _filelist.perCount; var end = (i - 2) * _filelist.perCount - 1; _filelist.DisposeItemByStartAndEnd(start, end); _pageVirtualizingDict[i] = true; Debug.WriteLine("虚化完毕:" + i); } i--; } } } preIndex = currentPageIndex; } _pageVirtualizingDict[currentPageIndex] = false; } else if (newValue < oldValue) { if (_pageOffsetDict.ContainsValue(newValue)) { currentPageIndex = _pageOffsetDict.GetKey(newValue).FirstOrDefault(); Debug.WriteLine("当前页:" + currentPageIndex + " 坐标:" + newValue); var offset = 3; if (preIndex - offset > currentPageIndex && currentPageIndex > 0) { _pageVirtualizingDict[preIndex] = false; if (!_pageVirtualizingDict[preIndex]) { Debug.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@虚化 After页:" + preIndex + " 是否虚化" + _pageVirtualizingDict[preIndex]); int i = preIndex - offset; while (i <= maxPageIndex) { //if (!_pageVirtualizingDict.ContainsKey(i)) //{ // _pageVirtualizingDict[i] = false; // _pageOffsetDict[i] = oldValue += minOffset; //} if (_pageVirtualizingDict.ContainsKey(i) && !_pageVirtualizingDict[i]) { Debug.WriteLine("开始释放第:" + i + "页"); int count = _filelist.Count; Debug.WriteLine("虚化前个数:" + count); var start = (i - 1) * _filelist.perCount; var end = i * _filelist.perCount - 1; if (end < _filelist.Count) { _filelist.DisposeItemByStartAndEnd(start, end); } string writeLine = "虚化after完毕 虚化位Start:" + start + " End:" + end + " Page:" + i + " 虚化后个数:" + count; Debug.WriteLine(writeLine); _pageVirtualizingDict[i] = true; } i++; } } _pageVirtualizingDict[currentPageIndex] = false; //_pageVirtualizingDict[currentPageIndex - 1] = false; //_pageVirtualizingDict[currentPageIndex - 2] = false; //_pageVirtualizingDict[currentPageIndex - 3] = false; preIndex = currentPageIndex; } _pageVirtualizingDict[currentPageIndex] = false; } } }); if (e.NewValue == _HorizontalScrollBar.Maximum) { this.IsHitTestVisible = false; await Task.Delay(500); this.IsHitTestVisible = true; } } }
使用方式:
<toolkit:IncrementalLoadingGridView x:Name="gvMain" Visibility="{Binding IsLeafLevel, Converter={StaticResource BooleanToVisibilityConverter}}" Padding="140,40,0,0" SelectionMode="None" ItemsSource="{Binding ImageItemsSource, Mode=TwoWay}" IncrementalLoadingThreshold="0.5" DataFetchSize="0.5" IsItemClickEnabled="True" PointerMoved="gvMain_PointerMoved" ItemClick="gvMain_ItemClick" ItemTemplateSelector="{StaticResource imageDataTemplateSelector}"> <GridView.ItemsPanel> <ItemsPanelTemplate> <WrapGrid VirtualizingStackPanel.VirtualizationMode="Recycling"></WrapGrid> </ItemsPanelTemplate> </GridView.ItemsPanel> </toolkit:IncrementalLoadingGridView>
IncrementalLoadingThreshold="0.5" DataFetchSize="0.5" 这2个阀值设定的比较低 这样虚化起来效率会高一些
需要binding 的列表对象
Dispose方法 需要binding对象继承IDispose 释放本地流
_filelist = new IncrementalLoadingCollection<FileItem>((uint)_currentfiles.Count, (startIndex, Count) => { SetLoadingState(true); if (_currentfiles == null || _currentfiles.Count <= startIndex) return null; List<FileItem> list = new List<FileItem>(); foreach (var file in _currentfiles.Skip(startIndex).Take(Count)) { FileItem item = new FileItem(); item.File = file; item.Name = file.DisplayName; if (folder.Name.EndsWith("zhx")) item.Name = "zhx" + item.Name; list.Add(item); } SetLoadingState(false); return list; }, (o) => { o.Dispose(); return o; });
当增量滚动时 我会针对 触发增量加载事件同时 记录 需要虚化的offset 在回滚的同时 进行释放操作,以保证内存在较低的负载。
在超宽屏幕上的使用效果, 载入了大量本地图片