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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。