在 App 的开发过程中,ListView 控件是比较常用的控件之一。掌握它的用法,能帮助我们在一定程度上提高开发效率。本文将会介绍 ListView 的一种用法——获取并设置 ListView 的滚动位置,以及获取滚动位置处的项目。这里多说一句,由于这个描述有点,所以本文的标题实在不好起。

举个例子,如果你正在开发的应用有这样一个需求,当用户从一个列表页(包括 ListView 控件)返回到前一页面时,你需要得到用户在浏览 ListView 中的内容到哪个位置以及哪一项了,以便告诉用户最近浏览项,并且可以让用户再次打开列表时,直接从上次浏览的位置处继续浏览。如下图:

本文介绍了实现上述需求的方法。具体来说,这个需求可细分为两个小需求,即:

  1. 获取、设置 ListView 的滚动位置;
  2. 获取 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)

这两个方法中各有一个参考是委托类型,分别是 ListViewItemToKeyHandlerListViewKeyToItemHandler,它们的作用是告诉这个类如何处理列表项与 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 滚动位置处的项目

关于第二个需求的实现,我们首先需要明白以下三点:

  1. ListView 的模板 (Template) 中包括 ScrollViewer,我们可以通过 VisualTreeHelper 获取到此控件;
  2. ListView 提供 ContainerFromItem 方法,它使们可以通过传递 Item 获取包括此 Item 的 Container,即 ListViewItem;
  3. 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 包括以下两个重要的方法:

  1. TransformPoint, 可以将得到的转换信息计算成 Point 值,表示某控件相对于另一控件的坐标位置
  2. 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 中与滚动有关的两个需求的实现的更多相关文章

  1. Viewbox在UWP开发中的应用

    Windows 8.1 与Windows Phone 8.1的UAP应用,终于在Windows 10上统一到了UWP之下.原来3个不同的project也变为一个.没有了2套xaml页面,我们需要用同一 ...

  2. Windows10(uwp)开发中的侧滑

    还是在持续的开发一款Windows10的应用中,除了上篇博客讲讲我在Windows10(uwp)开发中遇到的一些坑,其实还有很多不完善的地方,比如(UIElement.Foreground).(Gra ...

  3. ListView中的setOnScrollListener

    ListView是Android中最常用的控件之一,随着时代发展,RecyclerView有取代它的趋势,但是在一些老代码中,ListView依然扮演着重要的作用.项目中遇到一个需求,需要监听List ...

  4. 13、在 uwp应用中,给图片添加高斯模糊滤镜效果(一)

    如果在应用中,如果想要给app 添加模糊滤镜,可能第一想到的是第三方类库,比如 Win2d.lumia Imaging SDK .WriteableBitmapEx,不可否认,这些类库功能强大,效果也 ...

  5. 最熟悉的陌生人:ListView 中的观察者模式

    RecyclerView 得宠之前,ListView 可以说是我们用的最多的组件.之前一直没有好好看看它的源码,知其然不知其所以然. 今天我们来窥一窥 ListView 中的观察者模式. 不熟悉观察者 ...

  6. android ListView中button点击事件盖掉onItemClick解决办法

    ListView 1.在android应用当中,很多时候都要用到listView,但如果ListView当中添加Button后,ListView 自己的 public void onItemClick ...

  7. Android中GridView滚动到底部加载数据终极版

    之前在项目中有一个需求是需要GridView控件,滚动到底部自动加载.但是呢GridView控件并不提供诸如ListView监听滚动到底部的onScrollListener方法,为了实现这样一个效果, ...

  8. 在ListView中使用多个布局

    要想在一个ListView中使用多个布局文件,比如一个信息List包含了一个信息标题和每个信息对应的时间. 关键的步骤是实现Adapter类的getItemViewType 和getViewTypeC ...

  9. Android ListView中 每一项都有不同的布局

    实现代码 Adapter的代码 其中:ViewHolder分别是三个不同的布局,也就是ListView中每一项的布局 TYPE_1...是三种类型. 在使用不同布局的时候,getItemViewTyp ...

随机推荐

  1. kmp算法理解与记录

    字符串匹配的暴力解法 给定字符串s和p,寻找字符串p在字符串s中出现的位置,暴力解法如下所示: 如果当前字符匹配成功,++i;++j,继续匹配下一字符. 如果s[i]与s[j]匹配失败,令i-=(j- ...

  2. spring.net的基本搭建

    这几天在学C#,感觉还是需要一个控制反转的框架,正好Spirng也有.net版的,看着API搭建一个 大致目录是这样的,我们在APP.CONFIG里面配好xml文件的地址,这个APP.CONFIG就相 ...

  3. VPS 上ubuntu 里搭建VPN服务器

    根据https://my.oschina.net/isnail/blog/363151里逐步完成,自己本机WIN10连接不成功,VM里面用WIN7连接也不行,找别人试连却成功了,然后自己用手机4G网络 ...

  4. VIM 第二天--快捷键

    好吧,今天是重新学习vim的第二天,不得不说,收货丰厚. 先来勉励一下自己: 存活(目前状态) 感觉很好 感觉更好,更强,更快 吃用vim超能力 vimtutor练到lesson 7了,略微总结一下, ...

  5. 【翻译】使用Visual Studio创建Asp.Net Core MVC (一)

    This tutorial will teach you the basics of building an ASP.NET Core MVC web app using Visual Studio ...

  6. Handler消息传递机制——Handler类简洁

    Handler类的主要作用有两个: 在新启动的线程中发送消息. 在主线程中获取.处理消息. 上面的说法很简单,只要分成两步即可:在新启动的线程中发送消息:然后在主线程上获取.并处理消息.但这个过程涉及 ...

  7. Raphael的transform用法

    Raphael的transform用法 <%@ page language="java" contentType="text/html; charset=UTF-8 ...

  8. Firebug及YSlow简介与使用图文详解

    Firebug本是Firefox浏览器下一个出色的网页设计插件,随着浏览器的发展,Firebug也推出了支持IE.Opera.Chrome的Firebug Lite.凭借Firebug的出色代码调试功 ...

  9. spring mvc 必须传某个参数的写法

    在controller中写 @RequestMapping(中的params="json") @RequestMapping(value = "/{username}&q ...

  10. linux下的Source命令的基本功能

    source命令用法:source FileName作用:在当前bash环境下读取并执行FileName中的命令.注:该命令通常用命令“.”来替代.如:source .bash_rc 与 . .bas ...