概述
UWP Community Toolkit 中有一个自适应的 GridView 控件 - AdaptiveGridView,本篇我们结合代码详细讲解 AdaptiveGridView 的实现。
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 控件的类构成:
1. AdaptiveGridView.Properties.cs
AdaptiveGridView 控件的依赖属性类,包括了以下属性:
另外类中还有一个方法 CalculateColumns(containerWidth, itemWidth), 根据容器宽度和元素宽度,确定控件应该包含几列,向下取整,最小值为 1;
2. AdaptiveGridView.cs
AdaptiveGridView 类继承自 GridView 类, 先来看一下类结构:
因为继承自 GridView 类,所以 AdaptiveGridView 重载了两个方法:
接下来看一下几个重要的事件处理方法:
① 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!!!