From 22cbc471568c96e18d38b0985b0412571ecb1dce Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Sat, 13 Jun 2026 15:12:10 +0800 Subject: [PATCH] feat: Add WaterfallPanel control and demo. Introduce a new WaterfallPanel control for Avalonia and a demo UserControl showcasing its usage. The panel supports Groups, AutoGroup, DesiredLength (coerced to >= 0) and Orientation; AutoGroup computes columns/rows based on DesiredLength. Measure/Arrange logic distributes children into the shortest group (waterfall layout) and sizes items by dividing the primary axis by group count. Also add WaterfallPanelDemo.axaml and code-behind demonstrating horizontal/vertical examples with a NumericUpDown to adjust groups. --- .../Styles/WaterfallPanelDemo.axaml | 132 +++++++++++++++ .../Styles/WaterfallPanelDemo.axaml.cs | 9 + .../Controls/Panel/WaterfallPanel.cs | 158 ++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml new file mode 100644 index 00000000..b1a5fe62 --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs new file mode 100644 index 00000000..f4f7cbba --- /dev/null +++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Styles/WaterfallPanelDemo.axaml.cs @@ -0,0 +1,9 @@ +namespace HandyControlDemo.UserControl; + +public partial class WaterfallPanelDemo : Avalonia.Controls.UserControl +{ + public WaterfallPanelDemo() + { + InitializeComponent(); + } +} diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs new file mode 100644 index 00000000..c689a9cd --- /dev/null +++ b/src/Avalonia/HandyControl_Avalonia/Controls/Panel/WaterfallPanel.cs @@ -0,0 +1,158 @@ +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using HandyControl.Data; +using HandyControl.Tools; + +namespace HandyControl.Controls; + +public class WaterfallPanel : Panel +{ + public static readonly StyledProperty GroupsProperty = + AvaloniaProperty.Register(nameof(Groups), defaultValue: 2, + validate: IsGroupsValid); + + public int Groups + { + get => GetValue(GroupsProperty); + set => SetValue(GroupsProperty, value); + } + + private static bool IsGroupsValid(int value) => value >= 1; + + public static readonly StyledProperty AutoGroupProperty = + AvaloniaProperty.Register(nameof(AutoGroup)); + + public bool AutoGroup + { + get => GetValue(AutoGroupProperty); + set => SetValue(AutoGroupProperty, value); + } + + public static readonly StyledProperty DesiredLengthProperty = + AvaloniaProperty.Register(nameof(DesiredLength), defaultValue: 0d, + coerce: CoerceLength); + + public double DesiredLength + { + get => GetValue(DesiredLengthProperty); + set => SetValue(DesiredLengthProperty, value); + } + + public static readonly StyledProperty OrientationProperty = + StackPanel.OrientationProperty.AddOwner(new StyledPropertyMetadata( + defaultValue: Orientation.Horizontal)); + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + static WaterfallPanel() + { + AffectsMeasure( + GroupsProperty, + AutoGroupProperty, + DesiredLengthProperty, + OrientationProperty + ); + } + + private static double CoerceLength(AvaloniaObject _, double length) => length < 0 ? 0 : length; + + private int CalculateGroupCount(PanelUvSize size) + { + if (!AutoGroup) + { + return Groups; + } + + var itemLength = DesiredLength; + + if (MathHelper.IsVerySmall(itemLength)) + { + return Groups; + } + + return (int) (size.U / itemLength); + } + + protected override Size MeasureOverride(Size constraint) + { + var orientation = Orientation; + var uvConstraint = new PanelUvSize(orientation, constraint); + + var groups = CalculateGroupCount(uvConstraint); + if (groups < 1) + { + return constraint; + } + + var vArr = new double[groups].ToList(); + var itemU = uvConstraint.U / groups; + if (double.IsNaN(itemU) || double.IsInfinity(itemU)) + { + return constraint; + } + + var children = Children; + for (int i = 0, count = children.Count; i < count; i++) + { + var child = children[i]; + if (child == null) + { + continue; + } + + child.Measure(constraint); + + var sz = new PanelUvSize(orientation, child.DesiredSize); + var minIndex = vArr.IndexOf(vArr.Min()); + var minV = vArr[minIndex]; + + vArr[minIndex] = minV + sz.V; + } + + uvConstraint = new PanelUvSize(orientation, new Size(uvConstraint.ScreenSize.Width, vArr.Max())); + + return uvConstraint.ScreenSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + var orientation = Orientation; + var uvConstraint = new PanelUvSize(orientation, finalSize); + + var groups = CalculateGroupCount(uvConstraint); + if (groups < 1) + { + return finalSize; + } + + var vArr = new double[groups].ToList(); + var itemU = uvConstraint.U / groups; + + var children = Children; + for (int i = 0, count = children.Count; i < count; i++) + { + var child = children[i]; + if (child == null) + { + continue; + } + + var minIndex = vArr.IndexOf(vArr.Min()); + var minV = vArr[minIndex]; + var childUvSize = new PanelUvSize(orientation, child.DesiredSize); + var childSize = new PanelUvSize(orientation, itemU, childUvSize.V); + var childRectSize = new PanelUvSize(orientation, minIndex * itemU, minV); + + child.Arrange(new Rect(new Point(childRectSize.U, childRectSize.V), childSize.ScreenSize)); + vArr[minIndex] = minV + childUvSize.V; + } + + return finalSize; + } +}