UWP: ListView 中與滾動有關的兩個需求的實現

在 App 的開發過程當中,ListView 控件是比較經常使用的控件之一。掌握它的用法,能幫助咱們在必定程度上提升開發效率。本文將會介紹 ListView 的一種用法——獲取並設置 ListView 的滾動位置,以及獲取滾動位置處的項目。這裏多說一句,因爲這個描述有點,因此本文的標題實在很差起。git

舉個例子,若是你正在開發的應用有這樣一個需求,當用戶從一個列表頁(包括 ListView 控件)返回到前一頁面時,你須要獲得用戶在瀏覽 ListView 中的內容到哪一個位置以及哪一項了,以便告訴用戶最近瀏覽項,而且可讓用戶再次打開列表時,直接從上次瀏覽的位置處繼續瀏覽。以下圖:github

本文介紹了實現上述需求的方法。具體來講,這個需求可細分爲兩個小需求,即:算法

  1. 獲取、設置 ListView 的滾動位置;
  2. 獲取 ListView 滾動位置處的項目。

如下我會經過上面配圖中的 Demo 應用逐一說明(本文末尾有源碼下載連接),這個 Demo 包括兩個頁面,一個主頁 (MainPage),一個列表頁 (ItemsPage)。主頁中包括:windows

  • 按鈕:能夠導航到 ItemsPage;
  • 最近瀏覽信息區域:能夠查看上次瀏覽的項目,並提供一個按鈕能夠導航到列表頁中上次瀏覽的項目處;

而列表頁,則包括一個 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)

這兩個方法中各有一個參考是委託類型,分別是 ListViewItemToKeyHandlerListViewKeyToItemHandler,它們的做用是告訴這個類如何處理列表項與 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 滾動位置處的項目

關於第二個需求的實現,咱們首先須要明白如下三點:

  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

相關文章
相關標籤/搜索