概述

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 :
                        ;

    // width should be the displayable width
    containerWidth = containerWidth - Padding.Left - Padding.Right - panelMargin;
    )
    {
        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);
     && 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 (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!!!

New UWP Community Toolkit - AdaptiveGridView的更多相关文章

  1. New UWP Community Toolkit

    概述 UWP Community Toolkit 是一个 UWP App 自定义控件.应用服务和帮助方法的集合,能够很大程度的简化和指引开发者的开发工作,相信广大 UWPer 并不陌生. 下面是截取自 ...

  2. New UWP Community Toolkit - XAML Brushes

    概述 上一篇 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾.接下来会针对每个重要更新,结合 SDK 源代码和调用代码详细讲解. 本篇我们 ...

  3. New UWP Community Toolkit - Markdown

    概述 前面 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 MarkdownTextBlock 和 MarkdownDoc ...

  4. New UWP Community Toolkit - Staggered panel

    概述 前面 New UWP Community Toolkit 文章中,我们对 2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 Staggered panel,本篇我们结合代码详细讲解  St ...

  5. New UWP Community Toolkit - Carousel

    概述 New UWP Community Toolkit  V2.2.0 的版本发布日志中提到了 Carousel 的调整,本篇我们结合代码详细讲解  Carousel 的实现. Carousel 是 ...

  6. New UWP Community Toolkit - RadialProgressBar

    概述 UWP Community Toolkit  中有一个圆形的进度条控件 - RadialProgressBar,本篇我们结合代码详细讲解  RadialProgressBar 的实现. Radi ...

  7. New UWP Community Toolkit - RadialGauge

    概述 New UWP Community Toolkit  V2.2.0 的版本发布日志中提到了 RadialGauge 的调整,本篇我们结合代码详细讲解  RadialGauge 的实现. Radi ...

  8. New UWP Community Toolkit - RangeSelector

    概述 前面 New UWP Community Toolkit 文章中,我们对 V2.2.0 版本的重要更新做了简单回顾,其中简单介绍了 RangeSelector,本篇我们结合代码详细讲解一下 Ra ...

  9. New UWP Community Toolkit - ImageEx

    概述 UWP Community Toolkit  中有一个图片的扩展控件 - ImageEx,本篇我们结合代码详细讲解  ImageEx 的实现. ImageEx 是一个图片的扩展控件,包括 Ima ...

随机推荐

  1. 版本控制-Git对象

    Git对象 版本控制在于文件的控制,git的控制方法在于为每个文件生成(key,object)的结构.git利用sha-1加密算法,对每一个文件生成一个唯一的字符序列(明文大小不超过2^64位,对于普 ...

  2. javascript 特殊的面向对象以及继承详解(入门篇)

    学习Javascript人,大多听说一句话叫js里面一切都是对象.我刚开始接触javascript面向对象编程时候,挺乱的,我当时习惯性的把PHP的面像对象思想套用在js上面,其实js的面向对象与传统 ...

  3. 接触vsto,开发word插件的利器

    研究word插件有一段时间了,现在该是总结的时候了. 首先咱们来了解下什么是vsto?所谓vsto,就是vs面向office提供的一个开发平台.一个开发平台至少包含两个要素:开发工具(sdk)和运行环 ...

  4. 通过Performance Log确定磁盘有性能问题?

    一些比较重要的performance counter: Counter Description LogicalDisk\ % Free Space 报告磁盘空间中未被分配的空间占逻辑卷中总可用空间的百 ...

  5. Online Judge(OJ)搭建——3、MVC架构

    Model Model 层主要包含数据的类,这些数据一般是现实中的实体,所以,Model 层中类的定义常常和数据库 DDL 中的 create 语句类似. 通常数据库的表和类是一对一的关系,但是有的时 ...

  6. 关于embed的一些使用兼容

    因公司需求,要做一个扫描语音播报的功能,所以用到一些音频/视频标签 考虑到   <embed>   标签对于ie的兼容性更好一些所以,我在这采用了   <embed>   标签 ...

  7. 笔记:Struts2 拦截器

    配置拦截器 Struts.xml 配置文件中,使用<interceptor-/>来定义拦截器,有属性 name 表示拦截器的名称,class 表示拦截器的具体首先类,可以使用<par ...

  8. JAVA基本数据类型和引用数据类型的区别

    [基本数据类型] 基本数据类型:声明时直接在栈内存中开辟空间,并直接在当前内存中存放数据,赋值时传递的是变量中的值,总的来说,基本数据类型是传值的. [引用数据类型] 声明引用数据类型(数组或对象), ...

  9. 避免uncaughtException错误引起node.js进程崩溃

    uncaughtException 未捕获的异常, 当node.js 遇到这个错误,整个进程直接崩溃. 或许这俩个人上辈子一定是一对冤家. 或许这俩个人经历了前世500次的回眸才换来了今生的相遇,只可 ...

  10. 19.C++-(=)赋值操作符、智能指针编写(详解)

    (=)赋值操作符 编译器为每个类默认重载了(=)赋值操作符 默认的(=)赋值操作符仅完成浅拷贝 默认的赋值操作符和默认的拷贝构造函数有相同的存在意义 (=)赋值操作符注意事项 首先要判断两个操作数是否 ...