UWP - 抓取 ListView 滾動到哪一個 group 分類與加大虛擬化的項目數量

筆記介紹怎麽抓到目前 ListView 滾動到哪個 group 分類與加大虛擬化的項目數量。

使用 ListView 時,我建議先讀下面幾篇介紹:

下面説明本篇的主要内容:

捕捉 ListView 滾動到哪一個 group 分類

什麽例子會需要做這樣的處理,例如:集合中的群組顯示,行事曆日期的顯示...等,用來表示目前瀏覽的項目到哪一個群組了。

要如何做到呢?給與每一個 item 有一個 group name,配合 scroll 時抓到的每一個 item 來判斷到了哪一個 group

看到這裏是否覺得跟 ListView 做 group 的機制一樣呢?關於 Group 的做法可以參考 CollectionViewSource

舉例利用日期與節目名稱當資料來源,用戶滾動 scroll bar 時,通過 ListView 頂端的 item 來影響目前閲讀的日期:

1. 準備 UI XAML 擺放日期與節目清單:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <ListView Grid.Row="0" ItemsSource="{x:Bind ViewModel.GroupDateTime}" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollMode="Enabled" Margin="10,0" SelectedIndex="{x:Bind ViewModel.DateSelectedIndex, Mode=TwoWay}">
    <ListView.ItemsPanel>
      <ItemsPanelTemplate>
        <ItemsStackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ListView.ItemsPanel>
    <ListView.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding}" FontSize="18" Margin="5,10" />
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
  <ListView Grid.Row="1" x:Name="PrgramsListView" ItemsSource="{x:Bind ViewModel.ProgramCollection}" Loaded="ListView_Loaded">
    <ListView.ItemTemplate>
      <DataTemplate x:DataType="local:ProgramData">
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="{x:Bind StartAt}" FontSize="14" Margin="0,0,10,0" /> <TextBlock Text="{x:Bind Title}" FontSize="16" />
        </StackPanel>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</Grid>

2. 從放置節目資料的 ListView 中抓出 ScrollViewer 並注冊 ViewChanged 事件處理 scroll 的距離,並抓取 ListView 中哪一個 Item 已經走到了 ScrollViewer 的 top position(0,0),並對應回去 Calendar 的項目:

private void ListView_Loaded(object sender, RoutedEventArgs e)
{
    // 抓出 ListView 中的 ScrollViewer, 並注冊處理 ViewChanged 事件
    scrollViewer = GetScrollViewer(PrgramsListView);
    scrollViewer.ViewChanged += ScrollViewer_ViewChanged;
}

private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    // 檢查哪一個 item 已經滾動到 scroll viewer 的 top (positon(0,0)) 的位置
    for (int i = 0; i < PrgramsListView.Items.Count; i++)
    {
        var item = PrgramsListView.ContainerFromIndex(i) as ListViewItem;
        if (item == null)
        {
            continue;
        }

        // 先用 TransformToVisual 抓出 item 在 ListView 的位移資訊,
        // 再用 TransformPoint 根據位移資訊轉成座標
        var positionToTop = item.TransformToVisual(PrgramsListView).TransformPoint(new Point(0, 0));

        // 如果剛好到了 0 點,通知 ViewModel 執行 SelectedIndex
        if (positionToTop.Y >= 0)
        {
            ViewModel.OnScrollTo(i);
            break;
        }
    }
}

private ScrollViewer GetScrollViewer(DependencyObject depObj)
{
    if (depObj is ScrollViewer)
    {
        return depObj as ScrollViewer;
    }
    
    // 利用 VisualTreeHelper 抓出需要的 UIElement
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
    {
 var child = VisualTreeHelper.GetChild(depObj, i);
 var result = GetScrollViewer(child);

 if (result != null)
 {
     return result;
 }
    }

    return null;
}

效果如下圖:

範例幫助找到每個分類的頭,最後一個項目可能會找不到,因爲數量過少可能造成 top position 的檢查無法跑到而造成不會顯示到某個分類。

關鍵技術

Class Property/Method Description
UIElement TransformToVisual(UIElement visual) 回傳可用於將座標從 UIElement 轉換為指定物件的轉換物件。 例如,抓取 StackPanel 中 TextBlock 的座標轉換物件。
GeneralTransform TransformPoint 使用此轉換物件的邏輯轉換指定的點, 並返回結果。邏輯説明:Remarks
ItemsControl ContainerFromIndex ItemCollection 清單中抓出特定 index 的 item (也是容器)。 由於 ListView 繼承了 ItemControl,所以可以從 Container 中找到 Item 來比對, 進一步影響我們要調整的畫面。
  ItemsPanel 編輯獲取或設置定義控制項佈局的面板的範本。

 

加大虛擬化的項目數量

由於 ListViewGridView 支援虛擬化(virtualization),減少 UI Thread 在遇到大量資料時同時繪製而效能不好,如果使用 ScrollInToView 超過目前可視與虛擬化範圍則會造成功能失效無法滾動到特定位置。

參考 ListView and GridView UI optimization 虛擬化中兩個重要元素:ItemsStackPanelItemsWrapGrid 負責虛擬化的處理,如果您使用的不是這兩個就不會有虛擬化的效果,如:虛擬化的説明。

運作方式如下圖: 從上圖 Realized Items 代表虛擬化的範圍,Visible Window 代表可視範圍,這樣做可讓 UI thread 不需要大量繪製内容(降低 CPU 與 Memory),隨著用戶滾動範圍 Realized Items 會變成 Unrealized Items 互相交換來顯示内容。更多虛擬化的説明可以參考:ListView basics and virtualization concepts

ScrollInToView 的對象如果是 Unrealized item 就無法移動了,因爲它更不還沒出現在 UI 裏面。

要解決這樣的問題,可以透過 加大虛擬化的數量,在 ItemsStackPanelItemsWrapGrid 中有一個屬性: CacheLength,可以藉由調整它來加到 realized item。

Name Description
CacheLength 獲取或設置 viewport 外的緩衝區大小, 以 viewport size 的倍數為值。(預設 4.0) 為改善 scrolling performance,ItemsWrapGrid 建立並緩存項目來支援屏幕内外的顯示。 CacheLength 設定熒幕外的緩存大小,讓可視範圍的内容倍速增加。 注意 設置較小的緩存加快啓動時間,也可以設定較大緩存優化滾動性能, 但加大緩存也代表記憶體用量會增加,建議只用在必要的地方,避免效能不好。
FirstCacheIndex 取得目前緩存中的第一個項目在資料集合的 index。
FirstVisibleIndex 獲取螢幕上第一項的資料集合中的索引。
LastCacheIndex 獲取緩存中最後一項的資料集合中的索引。
LastVisibleIndex 獲取螢幕上最後一項的資料集合中的索引。

例如:CacheLength = 4 ,4 的倍數,資料集合有 30 個項目,可視範圍有 10 個,那就是 30*10*4 = 1200,該 ListView 就需要預留這麽大的範圍。

建議可以減少 UI 元素本身的設計或是把資料量減少藉由 Load more 的方式降低載入的時間與滾動的效能。

細節可以參考 Uwp ListView Tips And Common Mistakes

======

開發 UWP 使用 ListView 非常頻繁,所以很容易忽略細節,希望這篇有幫助到大家。

操作 ListViewGridView 時,建議閲讀 List view and grid viewListView and GridView UI optimization,有助於如何設計與調整效能。 謝謝。

References