Browse Source

[WrapPanel] Add Spacing Properties (#18079)

* add Spacing properties for WrapPanel

* add Unit Tests

* remove spacing for items having IsVisble=false

* refactor

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
Co-authored-by: Betta_Fish <zxbmmmmmmmmm@qq.com>
Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/18562/head
Poker 2 years ago
committed by GitHub
parent
commit
14c9b27ef9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 159
      src/Avalonia.Controls/WrapPanel.cs
  2. 145
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

159
src/Avalonia.Controls/WrapPanel.cs

@ -38,6 +38,18 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class WrapPanel : Panel, INavigableContainer public class WrapPanel : Panel, INavigableContainer
{ {
/// <summary>
/// Defines the <see cref="ItemSpacing"/> dependency property.
/// </summary>
public static readonly StyledProperty<double> ItemSpacingProperty =
AvaloniaProperty.Register<WrapPanel, double>(nameof(ItemSpacing));
/// <summary>
/// Defines the <see cref="LineSpacing"/> dependency property.
/// </summary>
public static readonly StyledProperty<double> LineSpacingProperty =
AvaloniaProperty.Register<WrapPanel, double>(nameof(LineSpacing));
/// <summary> /// <summary>
/// Defines the <see cref="Orientation"/> property. /// Defines the <see cref="Orientation"/> property.
/// </summary> /// </summary>
@ -67,12 +79,30 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static WrapPanel() static WrapPanel()
{ {
AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty); AffectsMeasure<WrapPanel>(ItemSpacingProperty, LineSpacingProperty, OrientationProperty, ItemWidthProperty, ItemHeightProperty);
AffectsArrange<WrapPanel>(ItemsAlignmentProperty); AffectsArrange<WrapPanel>(ItemsAlignmentProperty);
} }
/// <summary> /// <summary>
/// Gets or sets the orientation in which child controls will be layed out. /// Gets or sets the spacing between lines.
/// </summary>
public double ItemSpacing
{
get => GetValue(ItemSpacingProperty);
set => SetValue(ItemSpacingProperty, value);
}
/// <summary>
/// Gets or sets the spacing between items.
/// </summary>
public double LineSpacing
{
get => GetValue(LineSpacingProperty);
set => SetValue(LineSpacingProperty, value);
}
/// <summary>
/// Gets or sets the orientation in which child controls will be laid out.
/// </summary> /// </summary>
public Orientation Orientation public Orientation Orientation
{ {
@ -164,6 +194,8 @@ namespace Avalonia.Controls
{ {
double itemWidth = ItemWidth; double itemWidth = ItemWidth;
double itemHeight = ItemHeight; double itemHeight = ItemHeight;
double itemSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
var orientation = Orientation; var orientation = Orientation;
var children = Children; var children = Children;
var curLineSize = new UVSize(orientation); var curLineSize = new UVSize(orientation);
@ -171,45 +203,46 @@ namespace Avalonia.Controls
var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height); var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
bool itemWidthSet = !double.IsNaN(itemWidth); bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight); bool itemHeightSet = !double.IsNaN(itemHeight);
bool itemExists = false;
bool lineExists = false;
var childConstraint = new Size( var childConstraint = new Size(
itemWidthSet ? itemWidth : constraint.Width, itemWidthSet ? itemWidth : constraint.Width,
itemHeightSet ? itemHeight : constraint.Height); itemHeightSet ? itemHeight : constraint.Height);
for (int i = 0, count = children.Count; i < count; i++) for (int i = 0, count = children.Count; i < count; ++i)
{ {
var child = children[i]; var child = children[i];
// Flow passes its own constraint to children // Flow passes its own constraint to children
child.Measure(childConstraint); child.Measure(childConstraint);
// This is the size of the child in UV space // This is the size of the child in UV space
var sz = new UVSize(orientation, UVSize childSize = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width, itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height); itemHeightSet ? itemHeight : child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) // Need to switch to another line var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line
{ {
panelSize.U = Max(curLineSize.U, panelSize.U); panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V; panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
curLineSize = sz; curLineSize = childSize;
if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) // The element is wider then the constraint - give it a separate line itemExists = child.IsVisible;
{ lineExists = true;
panelSize.U = Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(orientation);
}
} }
else // Continue to accumulate a line else // Continue to accumulate a line
{ {
curLineSize.U += sz.U; curLineSize.U += childSize.U + nextSpacing;
curLineSize.V = Max(sz.V, curLineSize.V); curLineSize.V = Max(childSize.V, curLineSize.V);
itemExists |= child.IsVisible; // keep true
} }
} }
// The last line size, if any should be added // The last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U); panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V; panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
// Go from UV space to W/H space // Go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height); return new Size(panelSize.Width, panelSize.Height);
@ -220,89 +253,91 @@ namespace Avalonia.Controls
{ {
double itemWidth = ItemWidth; double itemWidth = ItemWidth;
double itemHeight = ItemHeight; double itemHeight = ItemHeight;
double itemSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
var orientation = Orientation; var orientation = Orientation;
bool isHorizontal = orientation == Orientation.Horizontal;
var children = Children; var children = Children;
int firstInLine = 0; int firstInLine = 0;
double accumulatedV = 0; double accumulatedV = 0;
double itemU = orientation == Orientation.Horizontal ? itemWidth : itemHeight; double itemU = isHorizontal ? itemWidth : itemHeight;
var curLineSize = new UVSize(orientation); var curLineSize = new UVSize(orientation);
var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height); var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height);
bool itemWidthSet = !double.IsNaN(itemWidth); bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight); bool itemHeightSet = !double.IsNaN(itemHeight);
bool useItemU = orientation == Orientation.Horizontal ? itemWidthSet : itemHeightSet; bool itemExists = false;
bool lineExists = false;
for (int i = 0; i < children.Count; i++) for (int i = 0; i < children.Count; ++i)
{ {
var child = children[i]; var child = children[i];
var sz = new UVSize(orientation, var childSize = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width, itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height); itemHeightSet ? itemHeight : child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) // Need to switch to another line var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line
{ {
ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, uvFinalSize.U); accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
ArrangeLine(curLineSize.V, firstInLine, i);
accumulatedV += curLineSize.V; accumulatedV += curLineSize.V; // add the height of the line just arranged
curLineSize = sz; curLineSize = childSize;
if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) // The element is wider then the constraint - give it a separate line
{
// Switch to next line which only contain one element
ArrangeLine(accumulatedV, sz.V, i, ++i, useItemU, itemU, uvFinalSize.U);
accumulatedV += sz.V;
curLineSize = new UVSize(orientation);
}
firstInLine = i; firstInLine = i;
itemExists = child.IsVisible;
lineExists = true;
} }
else // Continue to accumulate a line else // Continue to accumulate a line
{ {
curLineSize.U += sz.U; curLineSize.U += childSize.U + nextSpacing;
curLineSize.V = Max(sz.V, curLineSize.V); curLineSize.V = Max(childSize.V, curLineSize.V);
itemExists |= child.IsVisible; // keep true
} }
} }
// Arrange the last line, if any // Arrange the last line, if any
if (firstInLine < children.Count) if (firstInLine < children.Count)
{ {
ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, uvFinalSize.U); accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
ArrangeLine(curLineSize.V, firstInLine, children.Count);
} }
return finalSize; return finalSize;
}
private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU, double panelU) void ArrangeLine(double lineV, int start, int end)
{
var orientation = Orientation;
var children = Children;
double u = 0;
bool isHorizontal = orientation == Orientation.Horizontal;
if (ItemsAlignment != WrapPanelItemsAlignment.Start)
{ {
double totalU = 0; bool useItemU = isHorizontal ? itemWidthSet : itemHeightSet;
for (int i = start; i < end; i++) double u = 0;
if (ItemsAlignment != WrapPanelItemsAlignment.Start)
{ {
totalU += GetChildU(i); double totalU = -itemSpacing;
for (int i = start; i < end; ++i)
{
totalU += GetChildU(i) + (!children[i].IsVisible ? 0 : itemSpacing);
}
u = ItemsAlignment switch
{
WrapPanelItemsAlignment.Center => (uvFinalSize.U - totalU) / 2,
WrapPanelItemsAlignment.End => uvFinalSize.U - totalU,
WrapPanelItemsAlignment.Start => 0,
_ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null),
};
} }
u = ItemsAlignment switch for (int i = start; i < end; ++i)
{ {
WrapPanelItemsAlignment.Center => (panelU - totalU) / 2, double layoutSlotU = GetChildU(i);
WrapPanelItemsAlignment.End => panelU - totalU, children[i].Arrange(isHorizontal ? new(u, accumulatedV, layoutSlotU, lineV) : new(accumulatedV, u, lineV, layoutSlotU));
WrapPanelItemsAlignment.Start => 0, u += layoutSlotU + (!children[i].IsVisible ? 0 : itemSpacing);
_ => throw new NotImplementedException(), }
};
}
for (int i = start; i < end; i++) return;
{ double GetChildU(int i) => useItemU ? itemU :
double layoutSlotU = GetChildU(i); isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
children[i].Arrange(isHorizontal ? new(u, v, layoutSlotU, lineV) : new(v, u, lineV, layoutSlotU));
u += layoutSlotU;
} }
double GetChildU(int i) => useItemU ? itemU : isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
} }
private struct UVSize private struct UVSize

145
tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

@ -13,10 +13,10 @@ namespace Avalonia.Controls.UnitTests
{ {
Width = 100, Width = 100,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
@ -34,10 +34,10 @@ namespace Avalonia.Controls.UnitTests
{ {
Width = 200, Width = 200,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
@ -110,10 +110,10 @@ namespace Avalonia.Controls.UnitTests
Orientation = Orientation.Vertical, Orientation = Orientation.Vertical,
Height = 120, Height = 120,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
@ -132,10 +132,10 @@ namespace Avalonia.Controls.UnitTests
Orientation = Orientation.Vertical, Orientation = Orientation.Vertical,
Height = 60, Height = 60,
Children = Children =
{ {
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
new Border { Height = 50, Width = 100 }, new Border { Height = 50, Width = 100 },
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
@ -146,6 +146,83 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds); Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
} }
[Fact]
public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing()
{
var target = new WrapPanel
{
Width = 100,
ItemSpacing = 10,
LineSpacing = 20,
Children =
{
new Border { Height = 50, Width = 60 }, // line 0
new Border { Height = 50, Width = 30 }, // line 0
new Border { Height = 50, Width = 70 }, // line 1
new Border { Height = 50, Width = 30 }, // line 2
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(100, 190), target.Bounds.Size);
Assert.Equal(new Rect(0, 0, 60, 50), target.Children[0].Bounds);
Assert.Equal(new Rect(70, 0, 30, 50), target.Children[1].Bounds);
Assert.Equal(new Rect(0, 70, 70, 50), target.Children[2].Bounds);
Assert.Equal(new Rect(0, 140, 30, 50), target.Children[3].Bounds);
}
[Fact]
public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing_Invisible()
{
var target = new WrapPanel
{
ItemSpacing = 10,
Children =
{
new Border { Height = 50, Width = 60 }, // line 0
new Border { Height = 50, Width = 30 , IsVisible = false }, // line 0
new Border { Height = 50, Width = 50 }, // line 0
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(120, 50), target.Bounds.Size);
Assert.Equal(new Rect(0, 0, 60, 50), target.Children[0].Bounds);
Assert.Equal(new Rect(70, 0, 50, 50), target.Children[2].Bounds);
}
[Fact]
public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing_Vertical()
{
var target = new WrapPanel
{
Height = 100,
Orientation = Orientation.Vertical,
ItemSpacing = 10,
LineSpacing = 20,
Children =
{
new Border { Width = 50, Height = 60 }, // line 0
new Border { Width = 50, Height = 30 }, // line 0
new Border { Width = 50, Height = 70 }, // line 1
new Border { Width = 50, Height = 30 }, // line 2
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(190, 100), target.Bounds.Size);
Assert.Equal(new Rect(0, 0, 50, 60), target.Children[0].Bounds);
Assert.Equal(new Rect(0, 70, 50, 30), target.Children[1].Bounds);
Assert.Equal(new Rect(70, 0, 50, 70), target.Children[2].Bounds);
Assert.Equal(new Rect(140, 0, 50, 30), target.Children[3].Bounds);
}
[Fact] [Fact]
public void Applies_ItemWidth_And_ItemHeight_Properties() public void Applies_ItemWidth_And_ItemHeight_Properties()
{ {
@ -156,10 +233,10 @@ namespace Avalonia.Controls.UnitTests
ItemWidth = 20, ItemWidth = 20,
ItemHeight = 15, ItemHeight = 15,
Children = Children =
{ {
new Border(), new Border(),
new Border(), new Border(),
} }
}; };
target.Measure(Size.Infinity); target.Measure(Size.Infinity);
@ -170,6 +247,34 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds); Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds);
} }
[Fact]
public void Zero_Size_Visible_Child()
{
var target = new WrapPanel()
{
Orientation = Orientation.Horizontal,
Width = 50,
ItemSpacing = 10,
LineSpacing = 10,
Children =
{
new Border(), // line 0
new Border // line 1
{
Width = 50,
Height = 50
},
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Size(50, 60), target.Bounds.Size);
Assert.Equal(new Rect(0, 0, 0, 0), target.Children[0].Bounds);
Assert.Equal(new Rect(0, 10, 50, 50), target.Children[1].Bounds);
}
[Fact] [Fact]
void ItemWidth_Trigger_InvalidateMeasure() void ItemWidth_Trigger_InvalidateMeasure()
{ {

Loading…
Cancel
Save