概述 UWP Community Toolkit 中有一個自適應的 GridView 控制項 - AdaptiveGridView,本篇我們結合代碼詳細講解 AdaptiveGridView 的實現。 AdaptiveGridView 控制項能夠以均勻分組的方式,讓一組列填充整個顯示空間,它可以對佈局和 ...
概述
UWP Community Toolkit 中有一個自適應的 GridView 控制項 - AdaptiveGridView,本篇我們結合代碼詳細講解 AdaptiveGridView 的實現。
AdaptiveGridView 控制項能夠以均勻分組的方式,讓一組列填充整個顯示空間,它可以對佈局和內容的變化做出反應,以便自動適應不同的外觀。我們來看一下官方示例的展示:
Source: https://github.com/Microsoft/UWPCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/AdaptiveGridView
Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/adaptivegridview
Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;
開發過程
代碼分析
我們先來看看 AdaptiveGridView 控制項的類構成:
- AdaptiveGridView.Properties.cs - AdaptiveGridView 控制項的依賴屬性類;
- AdaptiveGridView.cs - AdaptiveGridView 控制項的定義和事件處理類;
- AdaptiveHeightValueConverter.cs - 自適應高度轉換器,根據傳入的 value: ItemHeight,以及 padding、margin 等參數得到自適應高度;
1. AdaptiveGridView.Properties.cs
AdaptiveGridView 控制項的依賴屬性類,包括了以下屬性:
- ItemClickCommand - 元素點擊命令
- ItemHeight - 元素高度
- ItemWidth - 元素寬度
- OneRowModeEnabled - 單行模式可用性標誌,布爾值
- DesiredWidth - 元素的期望寬度
- StretchContentForSingleRow - 內容知否已經拉伸去填充一行,布爾值
另外類中還有一個方法 CalculateColumns(containerWidth, itemWidth), 根據容器寬度和元素寬度,確定控制項應該包含幾列,向下取整,最小值為 1;
2. AdaptiveGridView.cs
AdaptiveGridView 類繼承自 GridView 類, 先來看一下類結構:
因為繼承自 GridView 類,所以 AdaptiveGridView 重載了兩個方法:
- PrepareContainerForItemOverride(d, item) - 準備特定的 element 去顯示特定的 item;當 d 為 FrameworkElement 類型時,綁定 ItemWidth 和 ItemHeight 屬性;當為 ContentControl 類型時,HorizontalContentAlignment 和 VerticalContentAlignment 設為 Stretch;
- OnApplyTemplate() - 針對單行的狀態變化,調用 DetermineOneRowMode() 方法做顯示的處理;
接下來看一下幾個重要的事件處理方法:
① RecalculateLayout(ActualWidth)
RecalculateLayout(ActualWidth) 方法會在 item 數量變化,尺寸變化,控制項尺寸變化等觸發時調用,根據 panel 的 Margin 和 AdaptiveGridView 的 Padding 來調整 containerWidth,再調用 CalculateItemWidth(containerWidth) 方法計算得到 ItemWidth。
private void RecalculateLayout(double containerWidth) { var itemsPanel = ItemsPanelRoot as Panel; var panelMargin = itemsPanel != null ? itemsPanel.Margin.Left + itemsPanel.Margin.Right : 0; // width should be the displayable width containerWidth = containerWidth - Padding.Left - Padding.Right - panelMargin; if (containerWidth > 0) { var newWidth = CalculateItemWidth(containerWidth); ItemWidth = Math.Floor(newWidth); } }
② CalculateItemWidth(containerWidth)
計算 item 的寬度;根據 containerWidth 和 item 的 DesiredWidth 計算出控制項的列數;如果需要針對單行模式調整,則調整列數為實際 item 數量;獲取 ItemMargin,當 items 或 container 為空時,設置為需要 container 的 Margin;最後根據 每一列在 container 中的寬度,減掉 itemMargin,得到 itemWidth;
protected virtual double CalculateItemWidth(double containerWidth) { if (double.IsNaN(DesiredWidth)) { return DesiredWidth; } var columns = CalculateColumns(containerWidth, DesiredWidth); // If there's less items than there's columns, reduce the column count (if requested); if (Items != null && Items.Count > 0 && Items.Count < columns && StretchContentForSingleRow) { columns = Items.Count; } // subtract the margin from the width so we place the correct width for placement var fallbackThickness = default(Thickness); var itemMargin = AdaptiveHeightValueConverter.GetItemMargin(this, fallbackThickness); if (itemMargin == fallbackThickness) { // No style explicitly defined, or no items or no container for the items // We need to get an actual margin for proper layout _needContainerMarginForLayout = true; } return (containerWidth / columns) - itemMargin.Left - itemMargin.Right; }
③ DetermineOneRowMode()
單行模式和多行模式切換時的處理;當單行時,把 MaxHeight 屬性設置為 ItemHeight,Orientation 設為縱向,滾動設置包括縱向滾動禁止,隱藏滾動條,橫向滾動可用;如果為多行模式,則根據保存的 Orientation 和 滾動條屬性恢復顯示;
private void DetermineOneRowMode() { if (_isLoaded) { var itemsWrapGridPanel = ItemsPanelRoot as ItemsWrapGrid; if (OneRowModeEnabled) { var b = new Binding() { Source = this, Path = new PropertyPath("ItemHeight"), Converter = new AdaptiveHeightValueConverter(), ConverterParameter = this }; if (itemsWrapGridPanel != null) { _savedOrientation = itemsWrapGridPanel.Orientation; itemsWrapGridPanel.Orientation = Orientation.Vertical; } SetBinding(MaxHeightProperty, b); _savedHorizontalScrollMode = ScrollViewer.GetHorizontalScrollMode(this); _savedVerticalScrollMode = ScrollViewer.GetVerticalScrollMode(this); _savedHorizontalScrollBarVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(this); _savedVerticalScrollBarVisibility = ScrollViewer.GetVerticalScrollBarVisibility(this); _needToRestoreScrollStates = true; ScrollViewer.SetVerticalScrollMode(this, ScrollMode.Disabled); ScrollViewer.SetVerticalScrollBarVisibility(this, ScrollBarVisibility.Hidden); ScrollViewer.SetHorizontalScrollBarVisibility(this, ScrollBarVisibility.Visible); ScrollViewer.SetHorizontalScrollMode(this, ScrollMode.Enabled); } else { ClearValue(MaxHeightProperty); if (!_needToRestoreScrollStates) { return; } _needToRestoreScrollStates = false; if (itemsWrapGridPanel != null) { itemsWrapGridPanel.Orientation = _savedOrientation; } ScrollViewer.SetVerticalScrollMode(this, _savedVerticalScrollMode); ScrollViewer.SetVerticalScrollBarVisibility(this, _savedVerticalScrollBarVisibility); ScrollViewer.SetHorizontalScrollBarVisibility(this, _savedHorizontalScrollBarVisibility); ScrollViewer.SetHorizontalScrollMode(this, _savedHorizontalScrollMode); } } }
④ OnSizeChanged(sender, e)
在尺寸變化時,如果橫向不是拉伸狀態,則需要計算變化前後的列數是否有變化,如果有變化則重新計算佈局;如果是拉伸狀態,則尺寸變化時直接重新計算佈局;
private void OnSizeChanged(object sender, SizeChangedEventArgs e) { // If we are in center alignment, we only care about relayout if the number of columns we can display changes // Fixes #1737 if (HorizontalAlignment != HorizontalAlignment.Stretch) { var prevColumns = CalculateColumns(e.PreviousSize.Width, DesiredWidth); var newColumns = CalculateColumns(e.NewSize.Width, DesiredWidth); // If the width of the internal list view changes, check if more or less columns needs to be rendered. if (prevColumns != newColumns) { RecalculateLayout(e.NewSize.Width); } } else if (e.PreviousSize.Width != e.NewSize.Width) { // We need to recalculate width as our size changes to adjust internal items. RecalculateLayout(e.NewSize.Width); } }
3. AdaptiveHeightValueConverter.cs
自適應高度轉換器,單向轉換,根據傳入的 value: ItemHeight,以及 padding、margin 等參數得到自適應高度;轉換隻在 OneRowMode 時使用,作用是把原高度,加上 padding 和 margin 變成新的高度,效果就是單行模式時,元素在高度上沒有空隙;設置的 Item padding 和 margin 會失效;
public object Convert(object value, Type targetType, object parameter, string language) { if (value != null) { var gridView = (GridView)parameter; if (gridView == null) { return value; } double.TryParse(value.ToString(), out double height); var padding = gridView.Padding; var margin = GetItemMargin(gridView, DefaultItemMargin); height = height + margin.Top + margin.Bottom + padding.Top + padding.Bottom; return height; } return double.NaN; }
AdaptiveHeightValueConverter 類中還有一個方法 GetItemMargin(view, fallback), 在 AdaptiveGridView 類的 CalculateItemWidth(containerWidth) 方法中使用,值設置的優先順序是:先取 GridView 對應的 Margin 屬性值,如果為空,則取 GridViewItem 的 Margin 屬性值,如果也為空,則取預設值;
internal static Thickness GetItemMargin(GridView view, Thickness fallback = default(Thickness)) { var setter = view.ItemContainerStyle?.Setters.OfType<Setter>().FirstOrDefault(s => s.Property == FrameworkElement.MarginProperty); if (setter != null) { return (Thickness)setter.Value; } else { if (view.Items.Count > 0) { var container = (GridViewItem)view.ContainerFromIndex(0); if (container != null) { return container.Margin; } } // Use the default thickness for a GridViewItem return fallback; } }
調用示例
我們簡單調用 AdaptiveGridView 控制項,設置了 DesiredWidth 和 ItemHeight,選擇模式設置為多選;可以看到在控制項尺寸變化時,列數和 Item 尺寸都發生了變化;如果不設置 ItemHeight,則每一行都會占滿寬度;第三張圖,當設置單行模式時,Item 在一行排列;
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <controls:AdaptiveGridView Name="AdaptiveGridViewControl" OneRowModeEnabled="False" ItemHeight="145" DesiredWidth="157" SelectionMode="Multiple" IsItemClickEnabled="True" ItemTemplate="{StaticResource PhotosTemplate}"/> </Grid>
總結
到這裡我們就把 UWP Community Toolkit 中的 AdaptiveGridView 控制項的源代碼實現過程和簡單的調用示例講解完成了,希望能對大家更好的理解和使用這個控制項有所幫助。歡迎大家多多交流,謝謝!
最後,再跟大家安利一下 UWPCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通過微博關註最新動態。
衷心感謝 UWPCommunityToolkit 的作者們傑出的工作,Thank you so much, UWPCommunityToolkit authors!!!