筆記介紹怎麽抓到目前 ListView 滾動到哪個 group 分類與加大虛擬化的項目數量。
使用 ListView 時,我建議先讀下面幾篇介紹:
- List view and grid view
- 使用 ItemTemplate 調整項目的顯示方式
- ListView 和 GridView 資料虛擬化
- Scroll viewer controls
- ListView 與 GridView UI 最佳化
下面説明本篇的主要内容:
捕捉 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 | 編輯獲取或設置定義控制項佈局的面板的範本。 |
加大虛擬化的項目數量
由於 ListView 或 GridView 支援虛擬化(virtualization),減少 UI Thread 在遇到大量資料時同時繪製而效能不好,如果使用 ScrollInToView 超過目前可視與虛擬化範圍則會造成功能失效無法滾動到特定位置。
參考 ListView and GridView UI optimization 虛擬化中兩個重要元素:ItemsStackPanel 與 ItemsWrapGrid 負責虛擬化的處理,如果您使用的不是這兩個就不會有虛擬化的效果,如:虛擬化的説明。
運作方式如下圖: 從上圖 Realized Items 代表虛擬化的範圍,Visible Window 代表可視範圍,這樣做可讓 UI thread 不需要大量繪製内容(降低 CPU 與 Memory),隨著用戶滾動範圍 Realized Items 會變成 Unrealized Items 互相交換來顯示内容。更多虛擬化的説明可以參考:ListView basics and virtualization concepts。
而 ScrollInToView 的對象如果是 Unrealized item 就無法移動了,因爲它更不還沒出現在 UI 裏面。
要解決這樣的問題,可以透過 加大虛擬化的數量,在 ItemsStackPanel 與 ItemsWrapGrid 中有一個屬性: 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 非常頻繁,所以很容易忽略細節,希望這篇有幫助到大家。
操作 ListView 與 GridView 時,建議閲讀 List view and grid view 與 ListView and GridView UI optimization,有助於如何設計與調整效能。 謝謝。
References:
- ListView basics and virtualization concepts
- List view and grid view
- [UWP][C#]How to set focus on first item of a ListView?
- ListView 與 GridView UI 最佳化
- ListView 和 GridView 資料虛擬化
- 操作說明:使用觸發程序來設定 ListView 中所選項目的樣式
- WPF getting the indices of ListView displayed items
- How to get ListView visible items?
- Dramatically Increase Performance when Users Interact with Large Amounts of Data in GridView and ListView
- 最佳化您的 XAML 標記
- 最佳化您的 XAML 版面配置