欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ListView用法中与滚动相关的需求实现

程序员文章站 2023-11-26 23:22:04
在 app 的开发过程中,listview 控件是比较常用的控件之一。掌握它的用法,能帮助我们在一定程度上提高开发效率。本文将会介绍 listview 的一种用法——获取并...

在 app 的开发过程中,listview 控件是比较常用的控件之一。掌握它的用法,能帮助我们在一定程度上提高开发效率。本文将会介绍 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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。