1. 程式人生 > >UWP: ListView 中與滾動有關的兩個需求的實現

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

在 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 值並進行比較而最終實現的。如果您有更好的方法、不同的看見,請留言,共同交流。

原始碼下載

參考資料:

相關推薦

UWP: ListView 滾動有關需求實現

在 App 的開發過程中,ListView 控制元件是比較常用的控制元件之一。掌握它的用法,能幫助我們在一定程度上提高開發效率。本文將會介紹 ListView 的一種用法——獲取並設定 ListView 的滾動位置,以及獲取滾動位置處的專案。這裡多說一句,由於這個描述有點,所以本文的標題實在不好起。 舉個例

在Java,如何把String[]合併為一個 list 合併有異曲同工之妙

在Java中,如何把兩個String[]合併為一個? 看起來是一個很簡單的問題。但是如何才能把程式碼寫得高效簡潔,卻還是值得思考的。這裡介紹四種方法,請參考選用。 一、apache-commons 這是最簡單的辦法。在apache-commons中,有一個ArrayUtils.addAll

關於C#的StreamReaderFileStream這

成員函式1. Peek() Returns the next available character but does not consume it. 2. Read() Reads the next character from the input stream and advances the chara

mac安全隱私只有選項,少了一個任何來源

span 軟件 style 出現 pos 全選 nbsp font 終端 mac安裝軟件時如彈出程序已損壞,請移到廢紙簍的提示。 解決方法:在終端裏輸入: sudo spctl --master-disable 然後回車,然後輸入密碼,即可在安全選項中看到

路由器配置實踐 教你如何在Linux三臺主機網段互相通信

網絡配置 虛擬機大家好我是你們的齊天大聖又到了齊天大聖給大家講解的時間了今天我帶你們做一個 大大項目 你們信不信如果把你不小心打開這個文檔 希望你能看完 這個博文花費了我兩天的時間所以請尊重我的勞動 假裝看完好嗎 齊天大聖在此謝過各位看官首先歡迎大家觀看操作步驟 我們正式開始題目:路由配置實踐:01. 需要按

sql ,如何獲取日期之前月數、周數、天數

lvs lar sbo elif bold ss5 getdate 聲明 參數 1、獲取兩個日期之間的月數、周數、天數語法 --1、獲取兩個日期之間的月數、周數、天數 --1.1)聲明參數 declare @startDate varchar(20)

如果在一個標簽上類並用,css樣式表引用時,類必須寫在一起,不能有空格。

nbsp clas div 樣式表 ext style borde 引用 order 例: <html>   <style type="text/css">     .mui-table-view-cell.mui-collapse{      bo

Java方法重寫的面試題

col class 返回 load 重寫 this strong gpo 不同 1:方法重寫和方法重載的區別?方法重載能改變返回值類型嗎?   方法重寫:     在子類中,出現和父類中一模一樣的方法聲明的現象。(包含方法名、參數列表和返回值類型都一樣)   方法重載

phparray_walk() 和 array_map()函數區別

.html als 就是 gpo map false AR HP www. 兩個函數的共性和區別: 1.傳入這兩個函數的 $value,就是數組中的單一個元素。 2.array_walk() 僅返回true或者false,array_map() 返回處理後的數組; 3.要得

關於偽類選擇器一個冒號和冒號的區別

fault 兼容 content 中一 radi view get top AD 只要兼容現代瀏覽器如 webkit、firefox、opera等瀏覽器,對於偽元素采用雙冒號的寫法,要是非要兼容IE瀏覽器,用CSS2的單冒號寫法比較安全。 首先,閱讀 w3c 對兩者的定義:

求過圓心直線圓的交點

主要是注意所使用的資料型別。 之前用的是float,出現了一些意外,而且花費了我不少時間來反覆驗證、推導, 做了很多的無用功,而且,反覆推導得出來的計算步驟並沒有什麼不牢靠的地方。 然後計算得到的結果卻是讓人如此之不省心,梗的我悶得慌。 今天上午發來了一貼,多位朋友各抒己見, 總

Oracle使sql求日期之間的所有日期

比如,兩個日期是2018-08-27和2018-08-30,要求27,28, 29,30這四天的日期,我們可以採用下面的SQL來求得: --oracle 求兩個日期之間的所有日期 SELECT TO_CHAR((TO_DATE(KSRQ, 'yyyy-MM-dd') + (ROWNU

JavaScriptsetTimeout()和setInterval()定時器的區別和使用方法

兩者最直接的區別是: setTimeout()在載入後延遲指定時間去執行一次表示式,只是一次。 setInterval()它從載入後,每隔指定的時間就執行一次表示式 所以要讓一段程式碼, 某個函式以固定頻率重複執行應該使用setInterval()函式 如果讓一段程式碼, 某個函式在

使用Pangolon在同一副圖,畫出軌跡,比較誤差

轉載:https://www.cnblogs.com/feifanrensheng/p/8628900.html 使用 code/ground-truth.txt 和 code/estimate.txt 兩條軌跡。請你根據上面公式,實現 RMSE 的計算程式碼,給出最後的 RMSE 結果。作為驗

面試問到當物件的hashcode相同會發生什麼

當兩個物件的hashcode相同會發生什麼 【這個問題基本上就是分界點了】 一些面試者會回答因為hashcode相同,所以兩個物件是相等的,HashMap將會丟擲異常,或者不會儲存它們。 如果之前的問題回答的好,面試官的印象比較好,可能會提醒他們有equals()和hashC

java版資料結構演算法—佇列、實現一個佇列

/** * 佇列:先進先出 */ class MyQueue { int a[]; int maxSize; //大小 int front; //開頭 int rear; //結尾 int nItems; //元素個數 //初始化

實現一個類,把冒泡和插入封裝到函式去(宣告函式,一個是冒泡,一個是插入),且進行呼叫和除錯

實現一個類,把冒泡和插入封裝到兩個函式中去(宣告兩個函式,一個是冒泡,一個是插入),且進行呼叫和除錯 import java.util.Arrays; /* * 實現一個類,把冒泡和插入封裝到兩個函式中去(宣告兩個函式,一個是冒泡,一個是插入),且進行呼叫和除錯 */ public class E

一次掌握 React React Native 框架

此係列文章將整合我的 React 視訊教程與 React Native 書籍中的精華部分,給大家介紹 React 與 React Native 結合學習的方法。 1. 軟體開發語言與框架的學習本質 我們在開始系列文章的技術點內容前,花一點時間探討一下軟體開發語言以及框架的學習本質,相對於整個技術點的講解,花這

問題:在pycharm執行程式,.py檔案屬於同一目錄且都存在,在一個.py檔案無法使用from...import命令引用另一個.py檔案,即from...import...有紅色波浪線

轉自:https://blog.csdn.net/l8947943/article/details/79874180 https://blog.csdn.net/wcx1293296315/article/details/81156036 問題具體如圖: 兩個.py檔案屬於同一個檔案