feat: Add responsive grid Row/Col controls and demo.

This commit is contained in:
NaBian committed 2026-06-13 18:12:59 +08:00
1 parent 3aea65a6b9
commit 700abba273
7 files changed
+589

No files matched your search

@@ -0,0 +1,156 @@
<UserControl x:Class="HandyControlDemo.UserControl.GridDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:langs="clr-namespace:HandyControlDemo.Properties.Langs"
Background="{DynamicResource RegionBrush}">
<ScrollViewer>
<StackPanel Margin="32">
<TextBlock Text="{x:Static langs:Lang.BasicLayout}" Theme="{StaticResource TextBlockLargeBold}" HorizontalAlignment="Left" />
<hc:Row Margin="0,20,0,0">
<hc:Col Span="24">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0">
<hc:Col Span="12">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="12">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0">
<hc:Col Span="8">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="8">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="8">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0">
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0">
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<TextBlock Text="{x:Static langs:Lang.ColumnSpacing}" Margin="0,32,0,0" Theme="{StaticResource TextBlockLargeBold}" HorizontalAlignment="Left" />
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<TextBlock Text="{x:Static langs:Lang.HybridLayout}" Margin="0,32,0,0" Theme="{StaticResource TextBlockLargeBold}" HorizontalAlignment="Left" />
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="16">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="8">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="8">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="8">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="16">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="4">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<TextBlock Text="{x:Static langs:Lang.ColumnOffset}" Margin="0,32,0,0" Theme="{StaticResource TextBlockLargeBold}" HorizontalAlignment="Left" />
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6" Offset="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="6" Offset="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Span="6" Offset="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<hc:Row Margin="0,20,0,0" Gutter="20">
<hc:Col Span="12" Offset="6">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
<TextBlock Text="{x:Static langs:Lang.ResponsiveLayout}" Margin="0,32,0,0" Theme="{StaticResource TextBlockLargeBold}" HorizontalAlignment="Left" />
<hc:Row Margin="0,20,0,0" Gutter="10">
<hc:Col Layout="8,6,4,3,1">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Layout="4 6 8 9 11">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Layout="4 6 8 9 11">
<Border Background="{DynamicResource BorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
<hc:Col Layout="8 6 4 3 1">
<Border Background="{DynamicResource SecondaryBorderBrush}" Height="36" CornerRadius="4" />
</hc:Col>
</hc:Row>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class GridDemo : Avalonia.Controls.UserControl, IFull
{
public GridDemo()
{
InitializeComponent();
}
}
@@ -0,0 +1,89 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.VisualTree;
using HandyControl.Data;
using HandyControl.Tools.Extension;
namespace HandyControl.Controls;
public class Col : ContentControl
{
public static readonly StyledProperty<ColLayout?> LayoutProperty =
AvaloniaProperty.Register<Col, ColLayout?>(nameof(Layout));
public ColLayout? Layout
{
get => GetValue(LayoutProperty);
set => SetValue(LayoutProperty, value);
}
public static readonly StyledProperty<int> OffsetProperty =
AvaloniaProperty.Register<Col, int>(nameof(Offset));
public int Offset
{
get => GetValue(OffsetProperty);
set => SetValue(OffsetProperty, value);
}
public static readonly StyledProperty<int> SpanProperty =
AvaloniaProperty.Register<Col, int>(nameof(Span), defaultValue: ColLayout.ColMaxCellCount,
validate: v => v is >= 1 and <= ColLayout.ColMaxCellCount);
public int Span
{
get => GetValue(SpanProperty);
set => SetValue(SpanProperty, value);
}
public static readonly StyledProperty<bool> IsFixedProperty =
AvaloniaProperty.Register<Col, bool>(nameof(IsFixed));
public bool IsFixed
{
get => GetValue(IsFixedProperty);
set => SetValue(IsFixedProperty, value);
}
static Col()
{
LayoutProperty.Changed.AddClassHandler<Col>(InvalidateParentMeasure);
OffsetProperty.Changed.AddClassHandler<Col>(InvalidateParentMeasure);
SpanProperty.Changed.AddClassHandler<Col>(InvalidateParentMeasure);
IsFixedProperty.Changed.AddClassHandler<Col>(InvalidateParentMeasure);
}
private static void InvalidateParentMeasure(Col col, AvaloniaPropertyChangedEventArgs e)
{
if (col.GetVisualParent() is Control parent)
{
parent.InvalidateMeasure();
}
}
internal int GetLayoutCellCount(ColLayoutStatus status)
{
if (Layout is not null)
{
return status switch
{
ColLayoutStatus.Xs => Layout.Xs,
ColLayoutStatus.Sm => Layout.Sm,
ColLayoutStatus.Md => Layout.Md,
ColLayoutStatus.Lg => Layout.Lg,
ColLayoutStatus.Xl => Layout.Xl,
ColLayoutStatus.Xxl => Layout.Xxl,
ColLayoutStatus.Auto => 0,
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
};
}
if (IsFixed)
{
return 0;
}
return Span;
}
}
@@ -0,0 +1,118 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using HandyControl.Data;
using HandyControl.Tools.Extension;
namespace HandyControl.Controls;
public class Row : Panel
{
private ColLayoutStatus _layoutStatus;
private double _maxChildDesiredHeight;
private double _fixedWidth;
public static readonly StyledProperty<double> GutterProperty =
AvaloniaProperty.Register<Row, double>(nameof(Gutter), coerce: CoerceGutter);
private static double CoerceGutter(AvaloniaObject d, double value)
=> double.IsNaN(value) || value < 0 ? 0 : value;
public double Gutter
{
get => GetValue(GutterProperty);
set => SetValue(GutterProperty, value);
}
static Row()
{
AffectsMeasure<Row>(GutterProperty);
}
protected override Size MeasureOverride(Size constraint)
{
var gutter = Gutter;
var totalCellCount = 0;
var totalRowCount = 1;
_fixedWidth = 0;
_maxChildDesiredHeight = 0;
var cols = Children.OfType<Col>().ToList();
foreach (var child in cols)
{
var cellCount = child.GetLayoutCellCount(_layoutStatus);
if (cellCount == 0 || child.IsFixed)
{
child.Measure(constraint);
_maxChildDesiredHeight = Math.Max(_maxChildDesiredHeight, child.DesiredSize.Height);
_fixedWidth += child.DesiredSize.Width + gutter;
}
}
var itemWidth = (constraint.Width - _fixedWidth + gutter) / ColLayout.ColMaxCellCount;
itemWidth = Math.Max(0, itemWidth);
foreach (var child in cols)
{
var cellCount = child.GetLayoutCellCount(_layoutStatus);
if (cellCount > 0 && !child.IsFixed)
{
totalCellCount += cellCount;
var availableWidth = Math.Max(0, cellCount * itemWidth - gutter);
child.Measure(new Size(availableWidth, constraint.Height));
_maxChildDesiredHeight = Math.Max(_maxChildDesiredHeight, child.DesiredSize.Height);
if (totalCellCount > ColLayout.ColMaxCellCount)
{
totalCellCount = cellCount;
totalRowCount++;
}
}
}
return new Size(0, _maxChildDesiredHeight * totalRowCount);
}
protected override Size ArrangeOverride(Size finalSize)
{
var gutter = Gutter;
var totalCellCount = 0;
var cols = Children.OfType<Col>().ToList();
var itemWidth = (finalSize.Width - _fixedWidth + gutter) / ColLayout.ColMaxCellCount;
itemWidth = Math.Max(0, itemWidth);
var childBounds = new Rect(0, 0, 0, _maxChildDesiredHeight);
_layoutStatus = ColLayout.GetLayoutStatus(finalSize.Width);
foreach (var child in cols)
{
if (!child.IsVisible)
{
continue;
}
var cellCount = child.GetLayoutCellCount(_layoutStatus);
totalCellCount += cellCount;
var childWidth = (cellCount > 0 && !child.IsFixed) ? Math.Max(0, cellCount * itemWidth - gutter) : child.DesiredSize.Width;
childBounds = childBounds.WithWidth(childWidth);
childBounds = childBounds.WithX(childBounds.X + child.Offset * itemWidth);
if (totalCellCount > ColLayout.ColMaxCellCount)
{
childBounds = childBounds.WithX(0);
childBounds = childBounds.WithY(childBounds.Y + _maxChildDesiredHeight);
totalCellCount = cellCount;
}
child.Arrange(childBounds);
childBounds = childBounds.WithX(childBounds.X + childWidth + gutter);
}
return finalSize;
}
}
@@ -0,0 +1,12 @@
namespace HandyControl.Data;
public enum ColLayoutStatus
{
Xs,
Sm,
Md,
Lg,
Xl,
Xxl,
Auto
}
@@ -0,0 +1,46 @@
using System;
using System.ComponentModel;
using System.Globalization;
using HandyControl.Tools.Extension;
namespace HandyControl.Tools.Converter;
public class ColLayoutConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return Type.GetTypeCode(sourceType) switch
{
TypeCode.Int16 or TypeCode.UInt16
or TypeCode.Int32 or TypeCode.UInt32
or TypeCode.Int64 or TypeCode.UInt64
or TypeCode.Single or TypeCode.Double or TypeCode.Decimal
or TypeCode.String => true,
_ => false
};
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
=> destinationType == typeof(string);
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value == null) throw GetConvertFromException(null!);
return value switch
{
string s => ColLayout.Parse(s),
double d => new ColLayout((int) d),
_ => new ColLayout(System.Convert.ToInt32(value, culture))
};
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is ColLayout layout && destinationType == typeof(string))
{
return layout.ToString();
}
throw new ArgumentException("CannotConvertType");
}
}
@@ -0,0 +1,159 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using HandyControl.Data;
using HandyControl.Tools.Converter;
namespace HandyControl.Tools.Extension;
[TypeConverter(typeof(ColLayoutConverter))]
public class ColLayout
{
public const int ColMaxCellCount = 24;
public const int HalfColMaxCellCount = 12;
public const int XsMaxWidth = 768;
public const int SmMaxWidth = 992;
public const int MdMaxWidth = 1200;
public const int LgMaxWidth = 1920;
public const int XlMaxWidth = 2560;
public int Xs { get; set; } = 24;
public int Sm { get; set; } = 12;
public int Md { get; set; } = 8;
public int Lg { get; set; } = 6;
public int Xl { get; set; } = 4;
public int Xxl { get; set; } = 2;
public ColLayout()
{
}
public ColLayout(int uniformWidth)
{
Xs = uniformWidth;
Sm = uniformWidth;
Md = uniformWidth;
Lg = uniformWidth;
Xl = uniformWidth;
Xxl = uniformWidth;
}
public ColLayout(int xs, int sm, int md, int lg, int xl, int xxl)
{
Xs = xs;
Sm = sm;
Md = md;
Lg = lg;
Xl = xl;
Xxl = xxl;
}
public object ProvideValue(IServiceProvider serviceProvider)
{
return new ColLayout
{
Xs = Xs,
Sm = Sm,
Md = Md,
Lg = Lg,
Xl = Xl,
Xxl = Xxl
};
}
public static ColLayoutStatus GetLayoutStatus(double width)
{
if (width < MdMaxWidth)
{
if (width < SmMaxWidth)
{
if (width < XsMaxWidth)
{
return ColLayoutStatus.Xs;
}
return ColLayoutStatus.Sm;
}
return ColLayoutStatus.Md;
}
if (width < XlMaxWidth)
{
if (width < LgMaxWidth)
{
return ColLayoutStatus.Lg;
}
return ColLayoutStatus.Xl;
}
return ColLayoutStatus.Xxl;
}
public static ColLayout Parse(string s)
{
if (string.IsNullOrWhiteSpace(s)) throw new FormatException("InvalidStringColLayout");
var separators = new[] { ',', ' ' };
var tokens = s.Split(separators, StringSplitOptions.RemoveEmptyEntries);
var lengths = new int[6];
var i = 0;
foreach (var token in tokens)
{
if (i >= 6) throw new FormatException("InvalidStringColLayout");
lengths[i++] = int.Parse(token, CultureInfo.InvariantCulture);
}
return i switch
{
1 => new ColLayout(lengths[0]),
2 => new ColLayout { Xs = lengths[0], Sm = lengths[1] },
3 => new ColLayout { Xs = lengths[0], Sm = lengths[1], Md = lengths[2] },
4 => new ColLayout { Xs = lengths[0], Sm = lengths[1], Md = lengths[2], Lg = lengths[3] },
5 => new ColLayout
{
Xs = lengths[0],
Sm = lengths[1],
Md = lengths[2],
Lg = lengths[3],
Xl = lengths[4]
},
6 => new ColLayout
{
Xs = lengths[0],
Sm = lengths[1],
Md = lengths[2],
Lg = lengths[3],
Xl = lengths[4],
Xxl = lengths[5]
},
_ => throw new FormatException("InvalidStringColLayout")
};
}
public override string ToString()
{
var sb = new StringBuilder(128);
sb.Append(Xs.ToString(CultureInfo.CurrentCulture));
sb.Append(',');
sb.Append(Sm.ToString(CultureInfo.CurrentCulture));
sb.Append(',');
sb.Append(Md.ToString(CultureInfo.CurrentCulture));
sb.Append(',');
sb.Append(Lg.ToString(CultureInfo.CurrentCulture));
sb.Append(',');
sb.Append(Xl.ToString(CultureInfo.CurrentCulture));
sb.Append(',');
sb.Append(Xxl.ToString(CultureInfo.CurrentCulture));
return sb.ToString();
}
}