UWP: ListView 中与滚动有关的两个需求的实现
在 App 的开发过程中,ListView 控件是比较常用的控件之一。掌握它的用法,能帮助我们在一定程度上提高开发效率。本文将会介绍 ListView 的一种用法——获取并设置 ListView 的滚动位置,以及获取滚动位置处的项目。这里多说一句,由于这个描述有点,所以本文的标题实在不好起。
举个例子,如果你正在开发的应用有这样一个需求,当用户从一个列表页(包括 ListView 控件)返回到前一页面时,你需要得到用户在浏览 ListView 中的内容到哪个位置以及哪一项了,以便告诉用户最近浏览项,并且可以让用户再次打开列表时,直接从上次浏览的位置处继续浏览。如下图:
本文介绍了实现上述需求的方法。具体来说,这个需求可细分为两个小需求,即:
- 获取、设置 ListView 的滚动位置;
- 获取 ListView 滚动位置处的项目。
以下我会通过上面配图中的 Demo 应用逐一说明(本文末尾有源码下载链接),这个 Demo 包括两个页面,一个主页 (MainPage),一个列表页 (ItemsPage)。主页中包括:
- 按钮:可以导航到 ItemsPage;
- 最近浏览信息区域:可以查看上次浏览的项目,并提供一个按钮可以导航到列表页中上次浏览的项目处;
而列表页,则包括一个 ListView 控件,展示若干个项目。
一、获取、设置 ListView 的滚动位置
关于获取、设置 ListView 的滚动位置,微软已经提供了相关的例子,我在这个 Demo 中是直接套用的。这个功能主要是通过 ListViewPersistenceHelper 来实现的,它提供以下两个方法:
// 获取 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 中它们的实现分别如下:
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 方法,在其中加入以下代码,来实现获取滚动位置并保存:
string position = ListViewPersistenceHelper.GetRelativeScrollPosition(this.listView, ItemToKeyHandler);
NavigationInfoHelper.SetInfo(targetItem, position);
继续为页面注册 Loaded 事件,在 Loaded 事件中加入以下代码来实现设置滚动位置:
if (navigationParameter != null)
{
if (NavigationInfoHelper.IsHasInfo)
{
await ListViewPersistenceHelper.SetRelativeScrollPositionAsync(listView, NavigationInfoHelper.LastPosition, KeyToItemHandler);
}
}
这里需要注意的是,设置滚动位置的方法是异步的,所以 Loaded 方法需要加上 async 修饰符。而上述代码中对 navigationParameter 参数的判断则是为了区别:在导航时是否定位到最近浏览的位置,具体可参考 Demo 的代码。
二、获取 ListView 滚动位置处的项目
关于第二个需求的实现,我们首先需要明白以下三点:
- ListView 的模板 (Template) 中包括 ScrollViewer,我们可以通过 VisualTreeHelper 获取到此控件;
- ListView 提供 ContainerFromItem 方法,它使们可以通过传递 Item 获取包括此 Item 的 Container,即 ListViewItem;
- UIElement 提供 TransformToVisual 方法,可以得到某控件相对指定控件的位置转换信息;
所以我们的思路就是:得到 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 包括以下两个重要的方法:
- TransformPoint, 可以将得到的转换信息计算成 Point 值,表示某控件相对于另一控件的坐标位置
- TransformBounds,可以将得到的转换信息计算成 Rect 值,表示某控件相对于另一控件的坐标位置及所占的区域。
所以,我们通过 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
UWP: ListView 中与滚动有关的两个需求的实现的更多相关文章
- Viewbox在UWP开发中的应用
Windows 8.1 与Windows Phone 8.1的UAP应用,终于在Windows 10上统一到了UWP之下.原来3个不同的project也变为一个.没有了2套xaml页面,我们需要用同一 ...
- Windows10(uwp)开发中的侧滑
还是在持续的开发一款Windows10的应用中,除了上篇博客讲讲我在Windows10(uwp)开发中遇到的一些坑,其实还有很多不完善的地方,比如(UIElement.Foreground).(Gra ...
- ListView中的setOnScrollListener
ListView是Android中最常用的控件之一,随着时代发展,RecyclerView有取代它的趋势,但是在一些老代码中,ListView依然扮演着重要的作用.项目中遇到一个需求,需要监听List ...
- 13、在 uwp应用中,给图片添加高斯模糊滤镜效果(一)
如果在应用中,如果想要给app 添加模糊滤镜,可能第一想到的是第三方类库,比如 Win2d.lumia Imaging SDK .WriteableBitmapEx,不可否认,这些类库功能强大,效果也 ...
- 最熟悉的陌生人:ListView 中的观察者模式
RecyclerView 得宠之前,ListView 可以说是我们用的最多的组件.之前一直没有好好看看它的源码,知其然不知其所以然. 今天我们来窥一窥 ListView 中的观察者模式. 不熟悉观察者 ...
- android ListView中button点击事件盖掉onItemClick解决办法
ListView 1.在android应用当中,很多时候都要用到listView,但如果ListView当中添加Button后,ListView 自己的 public void onItemClick ...
- Android中GridView滚动到底部加载数据终极版
之前在项目中有一个需求是需要GridView控件,滚动到底部自动加载.但是呢GridView控件并不提供诸如ListView监听滚动到底部的onScrollListener方法,为了实现这样一个效果, ...
- 在ListView中使用多个布局
要想在一个ListView中使用多个布局文件,比如一个信息List包含了一个信息标题和每个信息对应的时间. 关键的步骤是实现Adapter类的getItemViewType 和getViewTypeC ...
- Android ListView中 每一项都有不同的布局
实现代码 Adapter的代码 其中:ViewHolder分别是三个不同的布局,也就是ListView中每一项的布局 TYPE_1...是三种类型. 在使用不同布局的时候,getItemViewTyp ...
随机推荐
- 博客停更及OI退役公告
停更&&OI退役 公告 高中OI之路就这样结束了,曾经想过回在NOI跪,APIO跪,HNOI跪却从未想过会在NOIP跪! 没办法自己作死啊,CCF感觉还是很良心的混个省一回来了,看以后 ...
- sqlserver merge into
create table #ttt(id int,name nvarchar(10));merge into #ttt t using (select 1 as id ,'eee' as name ) ...
- c#第5章 变量的更多内容 隐式和显式转换、枚举、结构、数组、
1.目标数据 destination 英[ˌdestɪˈneɪʃn] 美[ˌdɛstəˈneʃən] n. 目的,目标; 目的地,终点; [罕用语] 预定,指定; 2.源数据 source 英[sɔ: ...
- Jmeter 新手
转载:http://www.cnblogs.com/TankXiao/p/4059378.html 什么是压力测试 顾名思义:压力测试,就是 被测试的系统,在一定的访问压力下,看程序运行是否稳定/服 ...
- Tsinsen A1333: 矩阵乘法(整体二分)
http://www.tsinsen.com/A1333 题意:-- 思路:和之前的第k小几乎一样,只不过把一维BIT换成二维BIT而已.注意二维BIT写法QAQ #include <cstdi ...
- java 之 Spring 框架(Java之负基础实战)
1.Spring是什么 相当于安卓的MVC框架,是一个开源框架.一般用于轻型或中型应用. 它的核心是控制反转(IoC)和面向切面(AOP). 主要优势是分层架构,允许选择使用哪一个组件.使用基本的Ja ...
- Viso设置背景
文件 > 形状 > 其它Visio方案 > 背景
- jQuery表单对象属性过滤选择器
jQuery表单对象属性过滤选择器 <div id="p1" attr="p1"> <input type="text" ...
- Spring Boot启动过程(一)
之前在排查一个线上问题时,不得不仔细跑了很多遍Spring Boot的代码,于是整理一下,我用的是1.4.3.RELEASE. 首先,普通的入口,这没什么好说的,我就随便贴贴代码了: SpringAp ...
- 对于python的__name__="__main__"的含义的理解
学习python的时候经常会看到python 中__name__ = \'__main__\' 这样的代码,可能很多新手一开始学习的时候都比较疑惑,python 中__name__ = '__main ...