Procházet zdrojové kódy

[Grid] Add RowSpacing and ColumnSpacing (#18077)

* Add spacing properties

* Add spacing while computing final offsets

* Add spacing while calculating desired size

* Add unit tests

* fix missing spacing arrangement

* draw grid line

* Add property changed handler

* add new unit test

* Resolve star spacing

* fix AffectsMeasure

* overflow unit test

* Clean up

---------

Co-authored-by: Poker <[email protected]>
Betta_Fish před 8 měsíci
rodič
revize
7d3f490b04

+ 87 - 23
src/Avalonia.Controls/Grid.cs

@@ -25,6 +25,8 @@ namespace Avalonia.Controls
         static Grid()
         {
             ShowGridLinesProperty.Changed.AddClassHandler<Grid>(OnShowGridLinesPropertyChanged);
+            ColumnSpacingProperty.Changed.AddClassHandler<Control>(OnSpacingPropertyChanged);
+            RowSpacingProperty.Changed.AddClassHandler<Control>(OnSpacingPropertyChanged);
 
             IsSharedSizeScopeProperty.Changed.AddClassHandler<Control>(DefinitionBase.OnIsSharedSizeScopePropertyChanged);
             ColumnProperty.Changed.AddClassHandler<Control>(OnCellAttachedPropertyChanged);
@@ -32,6 +34,7 @@ namespace Avalonia.Controls
             RowProperty.Changed.AddClassHandler<Control>(OnCellAttachedPropertyChanged);
             RowSpanProperty.Changed.AddClassHandler<Control>(OnCellAttachedPropertyChanged);
 
+            AffectsMeasure<Grid>(ColumnSpacingProperty, RowSpacingProperty);
             AffectsParentMeasure<Grid>(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty);
         }
 
@@ -161,6 +164,24 @@ namespace Avalonia.Controls
             set => SetValue(ShowGridLinesProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the size of the spacing to place between grid rows.
+        /// </summary>
+        public double RowSpacing
+        {
+            get => GetValue(RowSpacingProperty);
+            set => SetValue(RowSpacingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the size of the spacing to place between grid columns.
+        /// </summary>
+        public double ColumnSpacing
+        {
+            get => GetValue(ColumnSpacingProperty);
+            set => SetValue(ColumnSpacingProperty, value);
+        }
+
         /// <summary>
         /// Returns a ColumnDefinitions of column definitions.
         /// </summary>
@@ -299,7 +320,7 @@ namespace Avalonia.Controls
                     //  the cells belonging to them.
                     //
                     //  However, there are cases when topology of a grid causes cyclical
-                    //  size dependences. For example:
+                    //  size dependencies. For example:
                     //
                     //
                     //                         column width="Auto"      column width="*"
@@ -425,17 +446,19 @@ namespace Avalonia.Controls
                     //
 
                     MeasureCellsGroup(extData.CellGroup1, constraint, false, false);
-
+                    double combinedRowSpacing = RowSpacing * (RowDefinitions.Count - 1);
+                    double combinedColumnSpacing = ColumnSpacing * (ColumnDefinitions.Count - 1);
+                    Size innerAvailableSize = new Size(constraint.Width - combinedRowSpacing, constraint.Height - combinedColumnSpacing);
                     {
                         //  after Group1 is measured,  only Group3 may have cells belonging to Auto rows.
                         bool canResolveStarsV = !HasGroup3CellsInAutoRows;
 
                         if (canResolveStarsV)
                         {
-                            if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); }
-                            MeasureCellsGroup(extData.CellGroup2, constraint, false, false);
-                            if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); }
-                            MeasureCellsGroup(extData.CellGroup3, constraint, false, false);
+                            if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); }
+                            MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, false);
+                            if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); }
+                            MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false);
                         }
                         else
                         {
@@ -444,9 +467,9 @@ namespace Avalonia.Controls
                             bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length;
                             if (canResolveStarsU)
                             {
-                                if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); }
-                                MeasureCellsGroup(extData.CellGroup3, constraint, false, false);
-                                if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); }
+                                if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); }
+                                MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false);
+                                if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); }
                             }
                             else
                             {
@@ -462,7 +485,7 @@ namespace Avalonia.Controls
                                 double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false);
                                 double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true);
 
-                                MeasureCellsGroup(extData.CellGroup2, constraint, false, true);
+                                MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, true);
 
                                 do
                                 {
@@ -472,14 +495,14 @@ namespace Avalonia.Controls
                                         ApplyCachedMinSizes(group3MinSizes, true);
                                     }
 
-                                    if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); }
-                                    MeasureCellsGroup(extData.CellGroup3, constraint, false, false);
+                                    if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); }
+                                    MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false);
 
                                     // Reset cached Group2Widths
                                     ApplyCachedMinSizes(group2MinSizes, false);
 
-                                    if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); }
-                                    MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged);
+                                    if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); }
+                                    MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged);
                                 }
                                 while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount);
                             }
@@ -489,8 +512,8 @@ namespace Avalonia.Controls
                     MeasureCellsGroup(extData.CellGroup4, constraint, false, false);
 
                     gridDesiredSize = new Size(
-                            CalculateDesiredSize(DefinitionsU),
-                            CalculateDesiredSize(DefinitionsV));
+                            CalculateDesiredSize(DefinitionsU) + ColumnSpacing * (DefinitionsU.Count - 1),
+                            CalculateDesiredSize(DefinitionsV) + RowSpacing * (DefinitionsU.Count - 1));
                 }
             }
             finally
@@ -524,9 +547,12 @@ namespace Avalonia.Controls
                 else
                 {
                     Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0);
-
-                    SetFinalSize(DefinitionsU, arrangeSize.Width, true);
-                    SetFinalSize(DefinitionsV, arrangeSize.Height, false);
+                    double columnSpacing = ColumnSpacing;
+                    double rowSpacing = RowSpacing;
+                    double combinedRowSpacing = rowSpacing * (RowDefinitions.Count - 1);
+                    double combinedColumnSpacing = columnSpacing * (ColumnDefinitions.Count - 1);
+                    SetFinalSize(DefinitionsU, arrangeSize.Width - combinedColumnSpacing, true);
+                    SetFinalSize(DefinitionsV, arrangeSize.Height - combinedRowSpacing, false);
 
                     var children = Children;
 
@@ -540,14 +566,13 @@ namespace Avalonia.Controls
                         int rowSpan = PrivateCells[currentCell].RowSpan;
 
                         Rect cellRect = new Rect(
-                            columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset,
-                            rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset,
+                            columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset + (columnSpacing * columnIndex),
+                            rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset + (rowSpacing * rowIndex),
                             GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan),
                             GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan));
 
 
                         cell.Arrange(cellRect);
-
                     }
 
                     //  update render bound on grid lines renderer visual
@@ -2088,7 +2113,7 @@ namespace Avalonia.Controls
                 // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY;
                 var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
                 double[] roundingErrors = RoundingErrors;
-                double roundedTakenSize = 0.0;
+                double roundedTakenSize = 0;
 
                 // round each of the allocated sizes, keeping track of the deltas
                 for (int i = 0; i < definitions.Count; ++i)
@@ -2363,6 +2388,17 @@ namespace Avalonia.Controls
             grid.SetFlags((bool)e.NewValue!, Flags.ShowGridLinesPropertyValue);
         }
 
+        private static void OnSpacingPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
+        {
+            Grid grid = (Grid)d;
+
+            if (grid._extData != null
+                && grid.ListenToNotifications)
+            {
+                grid.CellsStructureDirty = true;
+            }
+        }
+
         private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
         {
             if (d is Visual child)
@@ -2674,6 +2710,18 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> ShowGridLinesProperty =
             AvaloniaProperty.Register<Grid, bool>(nameof(ShowGridLines));
 
+        /// <summary>
+        /// Defines the <see cref="RowSpacing"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> RowSpacingProperty =
+            AvaloniaProperty.Register<Grid, double>(nameof(RowSpacing));
+
+        /// <summary>
+        /// Defines the <see cref="ColumnSpacing"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> ColumnSpacingProperty =
+            AvaloniaProperty.Register<Grid, double>(nameof(ColumnSpacingProperty));
+
         /// <summary>
         /// Column property. This is an attached property.
         /// Grid defines Column property, so that it can be set
@@ -3269,6 +3317,14 @@ namespace Avalonia.Controls
                         drawingContext,
                         grid.ColumnDefinitions[i].FinalOffset, 0.0,
                         grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height);
+
+                    if (grid.ColumnSpacing != 0)
+                    {
+                        DrawGridLine(
+                            drawingContext,
+                            grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, 0.0,
+                            grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, _lastArrangeSize.Height);
+                    }
                 }
 
                 for (int i = 1; i < grid.RowDefinitions.Count; ++i)
@@ -3277,6 +3333,14 @@ namespace Avalonia.Controls
                         drawingContext,
                         0.0, grid.RowDefinitions[i].FinalOffset,
                         _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset);
+
+                    if (grid.RowSpacing != 0)
+                    {
+                        DrawGridLine(
+                            drawingContext,
+                            0.0, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing,
+                            _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing);
+                    }
                 }
             }
 

+ 85 - 0
tests/Avalonia.Controls.UnitTests/GridTests.cs

@@ -1656,6 +1656,91 @@ namespace Avalonia.Controls.UnitTests
             Assert.False(grid.IsArrangeValid);
         }
 
+        [Fact]
+        public void Should_Grid_Controls_With_Spacing()
+        {
+            var target = new Grid
+            {
+                RowSpacing = 10,
+                ColumnSpacing = 10,
+                RowDefinitions = RowDefinitions.Parse("100,100"),
+                ColumnDefinitions = ColumnDefinitions.Parse("100,100"),
+                Children =
+                {
+                    new Border(),
+                    new Border { [Grid.ColumnProperty] = 1 },
+                    new Border { [Grid.RowProperty] = 1 },
+                    new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 }
+                }
+            };
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+
+            Assert.Equal(new Rect(0, 0, 210, 210), target.Bounds);
+            Assert.Equal(new Rect(0, 0, 100, 100), target.Children[0].Bounds);
+            Assert.Equal(new Rect(110, 0, 100, 100), target.Children[1].Bounds);
+            Assert.Equal(new Rect(0, 110, 100, 100), target.Children[2].Bounds);
+            Assert.Equal(new Rect(110, 110, 100, 100), target.Children[3].Bounds);
+        }
+
+        [Fact]
+        public void Should_Grid_Controls_With_Spacing_Complicated()
+        {
+            var target = new Grid
+            {
+                Width = 200,
+                Height = 200,
+                RowSpacing = 10,
+                ColumnSpacing = 10,
+                RowDefinitions = RowDefinitions.Parse("50,*,2*,Auto"),
+                ColumnDefinitions = ColumnDefinitions.Parse("50,*,2*,Auto"),
+                Children =
+                {
+                    new Border(),
+                    new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 },
+                    new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 },
+                    new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 },
+                },
+            };
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+
+            Assert.Equal(new Rect(0, 0, 200, 200), target.Bounds);
+            Assert.Equal(new Rect(0, 0, 50, 50), target.Children[0].Bounds);
+            Assert.Equal(new Rect(60, 60, 30, 30), target.Children[1].Bounds);
+            Assert.Equal(new Rect(100, 100, 60, 60), target.Children[2].Bounds);
+            Assert.Equal(new Rect(170, 170, 30, 30), target.Children[3].Bounds);
+        }
+
+        [Fact]
+        public void Should_Grid_Controls_With_Spacing_Overflow()
+        {
+            var target = new Grid
+            {
+                Width = 100,
+                Height = 100,
+                ColumnSpacing = 20,
+                RowSpacing = 20,
+                ColumnDefinitions = ColumnDefinitions.Parse("30,*,*,Auto"),
+                RowDefinitions = RowDefinitions.Parse("30,*,*,Auto"),
+                Children =
+                {
+                    new Border(),
+                    new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 },
+                    new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 },
+                    new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 },
+                },
+            };
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+
+            Assert.Equal(new Rect(0, 0, 100, 100), target.Bounds);
+            Assert.Equal(new Rect(0, 0, 30, 30), target.Children[0].Bounds);
+            Assert.Equal(new Rect(50, 50, 0, 0), target.Children[1].Bounds);
+            Assert.Equal(new Rect(70, 70, 0, 0), target.Children[2].Bounds);
+            Assert.Equal(new Rect(90, 90, 30, 30), target.Children[3].Bounds);
+        }
+
         private class TestControl : Control
         {
             public Size MeasureSize { get; set; }