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.
This commit is contained in:
NaBian committed 2026-06-13 15:12:10 +08:00
1 parent 3f384cb0c2
commit 22cbc47156
3 files changed
+299

No files matched your search

@@ -0,0 +1,132 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
x:Class="HandyControlDemo.UserControl.WaterfallPanelDemo">
<Grid Margin="32">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<NumericUpDown x:Name="UpDownGroups"
HorizontalAlignment="Center"
Value="2"
Minimum="1"
Maximum="3"
Width="200"
Theme="{StaticResource NumericUpDownExtend}"
hc:TitleElement.Title="Groups"
hc:TitleElement.TitleWidth="50"
hc:TitleElement.TitlePlacement="Left"/>
<hc:UniformSpacingPanel Grid.Row="1"
Spacing="16"
ChildWrapping="Wrap"
Margin="0,16,0,0">
<ScrollViewer MaxHeight="300">
<hc:WaterfallPanel VerticalAlignment="Center"
Width="300"
Groups="{Binding #UpDownGroups.Value}">
<Border Height="100"
Background="{DynamicResource PrimaryBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Height="140"
Background="{DynamicResource DangerBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Height="100"
Background="{DynamicResource SuccessBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Height="170"
Background="{DynamicResource InfoBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Height="100"
Background="{DynamicResource WarningBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
</hc:WaterfallPanel>
</ScrollViewer>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Hidden"
hc:ScrollViewerAttach.Orientation="Horizontal"
MaxWidth="300">
<hc:WaterfallPanel VerticalAlignment="Center"
Height="300"
Orientation="Vertical"
Groups="{Binding #UpDownGroups.Value}">
<Border Width="100"
Background="{DynamicResource PrimaryBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Width="140"
Background="{DynamicResource DangerBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Width="100"
Background="{DynamicResource SuccessBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Width="170"
Background="{DynamicResource InfoBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
<Border Width="100"
Background="{DynamicResource WarningBrush}"
Effect="{StaticResource EffectShadow1}"
Margin="5">
<TextBlock Text="Content"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Foreground="White"/>
</Border>
</hc:WaterfallPanel>
</ScrollViewer>
</hc:UniformSpacingPanel>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class WaterfallPanelDemo : Avalonia.Controls.UserControl
{
public WaterfallPanelDemo()
{
InitializeComponent();
}
}
@@ -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<int> GroupsProperty =
AvaloniaProperty.Register<WaterfallPanel, int>(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<bool> AutoGroupProperty =
AvaloniaProperty.Register<WaterfallPanel, bool>(nameof(AutoGroup));
public bool AutoGroup
{
get => GetValue(AutoGroupProperty);
set => SetValue(AutoGroupProperty, value);
}
public static readonly StyledProperty<double> DesiredLengthProperty =
AvaloniaProperty.Register<WaterfallPanel, double>(nameof(DesiredLength), defaultValue: 0d,
coerce: CoerceLength);
public double DesiredLength
{
get => GetValue(DesiredLengthProperty);
set => SetValue(DesiredLengthProperty, value);
}
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<WaterfallPanel>(new StyledPropertyMetadata<Orientation>(
defaultValue: Orientation.Horizontal));
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
static WaterfallPanel()
{
AffectsMeasure<WaterfallPanel>(
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;
}
}