在 App 的开发过程当中,ListView 控件是比较经常使用的控件之一。掌握它的用法,能帮助咱们在必定程度上提升开发效率。本文将会介绍 ListView 的一种用法——获取并设置 ListView 的滚动位置,以及获取滚动位置处的项目。这里多说一句,因为这个描述有点,因此本文的标题实在很差起。git
举个例子,若是你正在开发的应用有这样一个需求,当用户从一个列表页(包括 ListView 控件)返回到前一页面时,你须要获得用户在浏览 ListView 中的内容到哪一个位置以及哪一项了,以便告诉用户最近浏览项,而且可让用户再次打开列表时,直接从上次浏览的位置处继续浏览。以下图:github
本文介绍了实现上述需求的方法。具体来讲,这个需求可细分为两个小需求,即:算法
如下我会经过上面配图中的 Demo 应用逐一说明(本文末尾有源码下载连接),这个 Demo 包括两个页面,一个主页 (MainPage),一个列表页 (ItemsPage)。主页中包括:windows
而列表页,则包括一个 ListView 控件,展现若干个项目。api
1、获取、设置 ListView 的滚动位置异步
关于获取、设置 ListView 的滚动位置,微软已经提供了相关的例子,我在这个 Demo 中是直接套用的。这个功能主要是经过 ListViewPersistenceHelper 来实现的,它提供如下两个方法:async
// 获取 ListView 的滚动位置 public static string GetRelativeScrollPosition(ListViewBase listViewBase, ListViewItemToKeyHandler itemToKeyHandler)
// 设置 ListView 的滚动位置 public static IAsyncAction SetRelativeScrollPositionAsync(ListViewBase listViewBase, String relativeScrollPosition, ListViewKeyToItemHandler keyToItemHandler)
这两个方法中各有一个参考是委托类型,分别是 ListViewItemToKeyHandler 和 ListViewKeyToItemHandler,它们的做用是告诉这个类如何处理列表项与 Key 的对应关系,好使得该类能够正确地获取或设置滚动位置。这里的 Key 是 ListViewItem 所表明的项目的一个属性(好比 Demo 中 Item 类的 Id 属性),这个属性的值在整个列表中是惟一的;而 Item 是在 Item 对象自己。在 Demo 中它们的实现分别以下:ide
private string ItemToKeyHandler(object item) { Item dataItem = item as Item; if (dataItem == null) return null; return dataItem.Id.ToString(); } private IAsyncOperation<object> KeyToItemHandler(string key) { Func<System.Threading.CancellationToken, Task<object>> taskProvider = token => { var items = listView.ItemsSource as List<Item>; if (items != null) { var targetItem = items.FirstOrDefault(m => m.Id == int.Parse(key)); return Task.FromResult((object)targetItem); } else { return Task.FromResult((object)null); } }; return AsyncInfo.Run(taskProvider); }
实现这两个方法后,重载列表页的 OnNavigatingFrom 方法,在其中加入如下代码,来实现获取滚动位置并保存:ui
string position = ListViewPersistenceHelper.GetRelativeScrollPosition(this.listView, ItemToKeyHandler); NavigationInfoHelper.SetInfo(targetItem, position);
继续为页面注册 Loaded 事件,在 Loaded 事件中加入如下代码来实现设置滚动位置:this
if (navigationParameter != null) { if (NavigationInfoHelper.IsHasInfo) { await ListViewPersistenceHelper.SetRelativeScrollPositionAsync(listView, NavigationInfoHelper.LastPosition, KeyToItemHandler); } }
这里须要注意的是,设置滚动位置的方法是异步的,因此 Loaded 方法须要加上 async 修饰符。而上述代码中对 navigationParameter 参数的判断则是为了区别:在导航时是否认位到最近浏览的位置,具体可参考 Demo 的代码。
2、获取 ListView 滚动位置处的项目
关于第二个需求的实现,咱们首先须要明白如下三点:
因此咱们的思路就是:获得 ListView 控件中的 ScrollViewer,并遍历 ListView 中全部的 Item,在遍历过程当中,获得每一项目的 ListViewItem,并判断它的位置是否位于 ScrollViewer 的位置中。如下是获取 ListView 中当前全部可见项的代码:
public static List<T> GetAllVisibleItems<T>(this ListViewBase listView) { var scrollViewer = listView.GetScrollViewer(); if (scrollViewer == null) { return null; } List<T> targetItems = new List<T>(); foreach (T item in listView.Items) { var itemContainer = listView.ContainerFromItem(item) as FrameworkElement; bool isVisible = IsVisibileToUser(itemContainer, scrollViewer, true); if (isVisible) { targetItems.Add(item); } } return targetItems; }
在上述代码的 foreach 循环中的部分,正是咱们前述思路的体现。而其中所调用的 IsVisibleToUser 方法,则是如何判断某一 ListViewItem 是否在 ScrollViewer 中为当前可见。其代码以下:
/// <summary> /// Code from here: /// https://social.msdn.microsoft.com/Forums/en-US/86ccf7a1-5481-4a59-9db2-34ebc760058a/uwphow-to-get-the-first-visible-group-key-in-the-grouped-listview?forum=wpdevelop /// </summary> /// <param name="element">ListViewItem or element in ListViewItem</param> /// <param name="container">ScrollViewer</param> /// <param name="isTotallyVisible">If the element is partially visible, then include it. The default value is false</param> /// <returns>Get the visibility of the target element</returns> private static bool IsVisibileToUser(FrameworkElement element, FrameworkElement container, bool isTotallyVisible = false) { if (element == null || container == null) return false; if (element.Visibility != Visibility.Visible) return false; Rect elementBounds = element.TransformToVisual(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight)); Rect containerBounds = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight); if (!isTotallyVisible) { return (elementBounds.Top < containerBounds.Bottom && elementBounds.Bottom > containerBounds.Top); } else { return (elementBounds.Bottom < containerBounds.Bottom && elementBounds.Top > containerBounds.Top); } }
能够看出,咱们是能过获得两个 Rect 值。Rect 类型的值表明一个矩形区域的位置和大小,咱们对这两个值进行比较后,返回最终的结果。
获取 ListViewItem 的 Rect 值: element.TransformToVisual(container) 返回的结果是 GeneralTransform 类型,这个值代表了 ListViewItem 相对于 Container(即 ScrollViewer)的位置转换信息。GeneralTransform 类型可能咱们并不太熟悉,不过,从它派生出来的这些类: ScaleTransform、TranslateTransform ,咱们就熟悉了,GeneralTransform 正是它们的基类。GeneralTransform 包括如下两个重要的方法:
因此,咱们经过 TransformBounds 方法就获得了 ListViewItem 相对于 ScrollViewer 的位置和所占区域的信息。
获取 ScrollViewer 的 Rect 值: 直接实例化一个 Rect,以 0,0 做为你左上角的坐标位置点, ScrollViewer 的 ActualWidth 和 ActualHeight 做为其大小。
接下来,就是比较的过程:这里,咱们作了一个判断,判断是否要求元素 (ListViewItem) 彻底在 ScrollViewer 中(而非仅部分在其中)。若是要求部分显示便可,则只要元素的 Top 小于 Container 的 Bottom 值,而且元素的 Bottom 大于 Container 的 Top;若是要求所有显示,那么算法是:元素的 Top 大于 Container 的 Top 而且元素的 Bottom 小于 Container 的 Bottom。若是您对语言描述或者代码都还不明白,也能够在纸上画一下进行比较。
接下来,咱们照着 GetAllVisbleItems 方法的思路能够实现 GetFirstVisibleItem 方法,即获取列表中第一个可见项,代码可参考 Demo 的源码,在此再也不赘述。
咱们在以前重载的方法 OnNavigatingFrom 中加上这句代码,便可以获取到用户浏览位置处的那一项。
var targetItem = this.listView.GetFirstVisibleItem<Item>();
至此,全部主要功能已经基本完成。
结语
本文介绍了如何获取和设置 ListView 的滚动位置,以及获取滚动位置处的那一项,前者主要是借助于 ListViewPersistenceHelper 来实现,后者则是经过获取 ListViewItem 和 ScrollViewer 的 Rect 值并进行比较而最终实现的。若是您有更好的方法、不一样的看见,请留言,共同交流。
参考资料:
ListView Sample
How to get the first visible group key in the grouped listview