1. 程式人生 > >New UWP Community Toolkit - Staggered panel

New UWP Community Toolkit - Staggered panel

end thumb 幫助 wpc ref type tar bubuko ocs

概述

前面 New UWP Community Toolkit 文章中,我們對 2.2.0 版本的重要更新做了簡單回顧,其中簡單介紹了 Staggered panel,本篇我們結合代碼詳細講解 Staggered panel 的實現。

Staggered panel 是一種交錯排列的面板控件,允許面板中的 item 以非整齊排列的方式排列,每個 item 會被添加到當前占用空間最小的列。這種排列方式,非常適用於圖片類,新聞資訊類的應用,官方示例展示如下圖:

技術分享圖片

Source: https://github.com/Microsoft/UWPCommunityToolkit/blob/master/Microsoft.Toolkit.Uwp.UI.Controls/StaggeredPanel/StaggeredPanel.cs

Doc: https://docs.microsoft.com/zh-cn/windows/uwpcommunitytoolkit/controls/staggeredpanel

Namespace: Microsoft.Toolkit.Uwp.UI.Controls; Nuget: Microsoft.Toolkit.Uwp.UI.Controls;

開發過程

代碼分析

StaggeredPanel 類繼承自 Panel類,我們先來看看它的構成:

  • public static 依賴屬性:PaddingProperty, DesiredColumnWidthProperty
  • public 變量:Padding, DesiredColumnWidth
  • private 變量:_columnWidth
  • public 方法:StaggeredPanel()
  • protected override 方法:MeasureOverride(availableSize), ArrangeOverride(finalSize)
  • private 方法:GetColumnIndex(columnHeights), OnHorizontalAlignmentChanged(sender, dp)
  • private static 方法:OnDesiredColumnWidthChanged(d, e), OnPaddingChanged(d, e)

技術分享圖片

我們先來看一下 StaggeredPanel 中可在調用類中獲取、設置和綁定的兩個依賴屬性:

  • DesiredColumnWidth - 獲取和設置 StaggeredPanel 內 Item 期望列寬度的屬性,默認值寬度是 250d;
  • Padding - 獲取和設置 StaggeredPanel 內 Item padding 屬性,默認值是 Thickness 的默認值 (0,0,0,0),它也是本次 V2.2.0 更新加入的內容
public static readonly DependencyProperty DesiredColumnWidthProperty = DependencyProperty.Register(
    nameof(DesiredColumnWidth),
    typeof(double),
    typeof(StaggeredPanel),
    new PropertyMetadata(250d, OnDesiredColumnWidthChanged));

public static readonly DependencyProperty PaddingProperty = DependencyProperty.Register(
    nameof(Padding),
    typeof(Thickness),
    typeof(StaggeredPanel),
    new PropertyMetadata(default(Thickness), OnPaddingChanged));

而這兩個依賴屬性註冊的 On***Changed 如下,獲取當前 StaggeredPanel 後,強制觸發一次 Measure 的重新計算:

private static void OnDesiredColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var panel = (StaggeredPanel)d;
    panel.InvalidateMeasure();
}

private static void OnPaddingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var panel = (StaggeredPanel)d;
    panel.InvalidateMeasure();
}

接下來看一下 StaggeredPanel 的類構造方法:

可以看到,構造方法中註冊了一個屬性變化後的回調事件,針對 Panel.HorizontalAlignmentProperty 的變化,註冊了 OnHorizontalAlignmentChanged 方法,這個方法的功能也很簡單,就是強制觸發一次 Measure 計算。

public StaggeredPanel()
{
    RegisterPropertyChangedCallback(Panel.HorizontalAlignmentProperty, OnHorizontalAlignmentChanged);
}
private void OnHorizontalAlignmentChanged(DependencyObject sender, DependencyProperty dp)
{
    InvalidateMeasure();
}

然後來看兩個 override 方法:MeasureOverride(availableSize) 和 ArrangeOverride(finalSize)

MeasureOverride(availableSize) :

該方法作用是傳入可用的尺寸,基於其對子元素大小的計算確定它在布局期間所需要的尺寸,我們來看一下具體實現過程:

1. 根據 availableSize,去掉 Padding 對應方向的值,獲得新的 availableSize,也就是子元素可用的尺寸;

2. 在期望列寬和可用寬度間獲得正確的列寬,根據列寬計算當前布局中可用的列數;如果當前控件的橫向對齊方式對拉伸,重新設置列寬,這時列寬實際就是期望列寬度;

3. 遍歷 panel 中的 children,根據 GetColumnIndex(columnHeights) 方法傳回指定 child 的列索引,計算原則是找到 columnHeights 數組中最小值,返回索引;根據返回的索引,把對應 child 的高度加到 columnHeights 對應索引中,更新 columnHeights 數組中每列的總高度值;

4. 在 columnHeights 數組中 ,找到最大值,返回新的尺寸:寬度為可用尺寸的寬度,高度為列數組的最大值;可以看出,這個尺寸就是根據子元素計算出的 panel 需要的空間大小;

protected override Size MeasureOverride(Size availableSize)
{
    availableSize.Width = availableSize.Width - Padding.Left - Padding.Right;
    availableSize.Height = availableSize.Height - Padding.Top - Padding.Bottom;

    _columnWidth = Math.Min(DesiredColumnWidth, availableSize.Width);
    int numColumns = (int)Math.Floor(availableSize.Width / _columnWidth);
    if (HorizontalAlignment == HorizontalAlignment.Stretch)
    {
        _columnWidth = availableSize.Width / numColumns;
    }

    var columnHeights = new double[numColumns];

    for (int i = 0; i < Children.Count; i++)
    {
        var columnIndex = GetColumnIndex(columnHeights);

        var child = Children[i];
        child.Measure(new Size(_columnWidth, availableSize.Height));
        var elementSize = child.DesiredSize;
        columnHeights[columnIndex] += elementSize.Height;
    }

    double desiredHeight = columnHeights.Max();

    return new Size(availableSize.Width, desiredHeight);
}

ArrangeOverride(finalSize):

該方法作用是根據 Measure 方法計算的最終尺寸,實際去排列 Item,排列完成後給出元素實際占用的尺寸,來看一下具體實現過程:

1. 計算列數,根據 panel 橫向對齊方式,在居中和靠右時,重新設置橫向偏移值,考慮最終寬度和實際元素寬度的偏差;

2. 遍歷 panel 的 children,在排列時對 child 寬度做矯正,如果 child 寬度大於列寬,則把寬度調整到列寬,根據寬高比調整高度;

3. 排列後,重新計算當前占用空間的 bounds,調整列數組中對應列的高度;

protected override Size ArrangeOverride(Size finalSize)
{
    double horizontalOffset = Padding.Left;
    double verticalOffset = Padding.Top;
    int numColumns = (int)Math.Floor(finalSize.Width / _columnWidth);
    if (HorizontalAlignment == HorizontalAlignment.Right)
    {
        horizontalOffset += finalSize.Width - (numColumns * _columnWidth);
    }
    else if (HorizontalAlignment == HorizontalAlignment.Center)
    {
        horizontalOffset += (finalSize.Width - (numColumns * _columnWidth)) / 2;
    }

    var columnHeights = new double[numColumns];

    for (int i = 0; i < Children.Count; i++)
    {
        var columnIndex = GetColumnIndex(columnHeights);

        var child = Children[i];
        var elementSize = child.DesiredSize;

        double elementWidth = elementSize.Width;
        double elementHeight = elementSize.Height;
        if (elementWidth > _columnWidth)
        {
            double differencePercentage = _columnWidth / elementWidth;
            elementHeight = elementHeight * differencePercentage;
            elementWidth = _columnWidth;
        }

        Rect bounds = new Rect(horizontalOffset + (_columnWidth * columnIndex), columnHeights[columnIndex] 
+ verticalOffset, elementWidth, elementHeight); child.Arrange(bounds); columnHeights[columnIndex] += elementSize.Height; } return base.ArrangeOverride(finalSize); }

最後來看一下前面 MeasureOverride 和 ArrangeOverride 方法中都用到的 GetColumnIndex(columnHeights) 方法:

這個方法的作用是根據傳入的列高度數組,計算當前高度最小的列索引;這也是 StaggeredPanel 可以實現每次添加到最小高度列的關鍵方法;

private int GetColumnIndex(double[] columnHeights)
{
    int columnIndex = 0;
    double height = columnHeights[0];
    for (int j = 1; j < columnHeights.Length; j++)
    {
        if (columnHeights[j] < height)
        {
            columnIndex = j;
            height = columnHeights[j];
        }
    }

    return columnIndex;
}

調用示例

下面示例中,我們使用了 GridView 控件,用 StaggeredPanel 作為 ItemsPanelTemplate;上面說到了兩個依賴屬性,我們分別作了設置,從下面的運行圖中也可以體現出來。大家也可以看到,StaggeredPanel 中 child 的排列規則,確實是按照每個列高度最小的列來排列;而在 panel 寬度變化時,也對應作了重新的計算和排列。

<GridView.ItemTemplate>
    <DataTemplate>
        <Grid>
            <Grid.Background>
                <SolidColorBrush Color="{Binding Color}"/>
            </Grid.Background>
            <Image Source="{Binding Thumbnail}" Stretch="Uniform"/>
            <Border Background="#44000000" VerticalAlignment="Top">
                <TextBlock Foreground="White" Margin="5,3">
                    <Run Text="{Binding Title}"/>
                </TextBlock>
            </Border>
        </Grid>
    </DataTemplate>
</GridView.ItemTemplate>
<GridView.ItemsPanel>
    <ItemsPanelTemplate>
        <controls:StaggeredPanel DesiredColumnWidth="135" Padding="25,25,25,25"
                                    HorizontalAlignment="Stretch"/>
    </ItemsPanelTemplate>
</GridView.ItemsPanel>

技術分享圖片 技術分享圖片

總結

到這裏我們就把 UWP Community Toolkit 中的 StaggeredPanel 功能的源代碼實現過程和簡單的調用示例講解完成了,希望能對大家更好的理解和使用這個控件有所幫助,也希望能啟發大家去做出更豐富排列規則的 Panel 控件。歡迎大家多多交流,謝謝!

最後,再跟大家安利一下 UWPCommunityToolkit 的官方微博:https://weibo.com/u/6506046490, 大家可以通過微博關註最新動態。

衷心感謝 UWPCommunityToolkit 的作者們傑出的工作,Thank you so much, UWPCommunityToolkit authors!!!

New UWP Community Toolkit - Staggered panel