feat: Add CirclePanel control and demo.

Introduce a new CirclePanel control that arranges visible children evenly around a circle. The control exposes Diameter, KeepVertical and OffsetAngle styled properties and implements MeasureOverride/ArrangeOverride to position and optionally rotate items on a circular path. Also add a demo UserControl (CirclePanelDemo.axaml and .axaml.cs) with a themed button template to showcase the panel.
This commit is contained in:
NaBian committed 2026-06-13 15:37:34 +08:00
1 parent 22cbc47156
commit d1944b01c3
3 files changed
+158

No files matched your search

@@ -0,0 +1,49 @@
<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.CirclePanelDemo">
<UserControl.Resources>
<ControlTheme x:Key="CirclePanelButton" TargetType="Button">
<Setter Property="UseLayoutRounding" Value="False" />
<Setter Property="Height" Value="77" />
<Setter Property="Width" Value="36.06" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<ControlTemplate TargetType="Button">
<Panel Width="36.06" Height="77">
<Path Data="{StaticResource CirclePanelDemoGeometry}"
Fill="{DynamicResource PrimaryBrush}"
Stretch="Fill" />
<Path Data="{StaticResource CirclePanelRightGeometry}"
Fill="White"
Stretch="Uniform"
Height="21"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Setter Property="Opacity" Value=".9" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Opacity" Value=".6" />
</Style>
<Style Selector="^:disabled">
<Setter Property="Opacity" Value="0.4" />
</Style>
</ControlTheme>
</UserControl.Resources>
<hc:CirclePanel Margin="64" Diameter="170">
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
<Button Theme="{StaticResource CirclePanelButton}" />
</hc:CirclePanel>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class CirclePanelDemo : Avalonia.Controls.UserControl
{
public CirclePanelDemo()
{
InitializeComponent();
}
}
@@ -0,0 +1,100 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace HandyControl.Controls;
public class CirclePanel : Panel
{
public static readonly StyledProperty<double> DiameterProperty =
AvaloniaProperty.Register<CirclePanel, double>(nameof(Diameter), defaultValue: 170.0);
public double Diameter
{
get => GetValue(DiameterProperty);
set => SetValue(DiameterProperty, value);
}
public static readonly StyledProperty<bool> KeepVerticalProperty =
AvaloniaProperty.Register<CirclePanel, bool>(nameof(KeepVertical));
public bool KeepVertical
{
get => GetValue(KeepVerticalProperty);
set => SetValue(KeepVerticalProperty, value);
}
public static readonly StyledProperty<double> OffsetAngleProperty =
AvaloniaProperty.Register<CirclePanel, double>(nameof(OffsetAngle), defaultValue: 0d);
public double OffsetAngle
{
get => GetValue(OffsetAngleProperty);
set => SetValue(OffsetAngleProperty, value);
}
static CirclePanel()
{
AffectsMeasure<CirclePanel>(DiameterProperty, KeepVerticalProperty, OffsetAngleProperty);
}
protected override Size MeasureOverride(Size availableSize)
{
var diameter = Diameter;
var layoutChildren = Children.Where(element => element.IsVisible).ToList();
if (layoutChildren.Count == 0) return new Size(diameter, diameter);
var newSize = new Size(diameter, diameter);
foreach (var element in layoutChildren)
{
element.Measure(newSize);
}
return newSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var keepVertical = KeepVertical;
var offsetAngle = OffsetAngle;
var layoutChildren = Children.Where(element => element.IsVisible).ToList();
if (layoutChildren.Count == 0)
{
return finalSize;
}
var i = 0;
var perDeg = 360.0 / layoutChildren.Count;
var radius = Diameter / 2;
foreach (var element in layoutChildren)
{
var centerX = element.DesiredSize.Width / 2.0;
var centerY = element.DesiredSize.Height / 2.0;
var angle = perDeg * i++ + offsetAngle;
element.RenderTransformOrigin = new RelativePoint(centerX, centerY, RelativeUnit.Absolute);
element.RenderTransform = new RotateTransform
{
Angle = keepVertical ? 0 : angle
};
var r = Math.PI * angle / 180.0;
var x = radius * Math.Cos(r);
var y = radius * Math.Sin(r);
var rectX = x + finalSize.Width / 2 - centerX;
var rectY = y + finalSize.Height / 2 - centerY;
element.Arrange(new Rect(rectX, rectY, element.DesiredSize.Width, element.DesiredSize.Height));
}
return finalSize;
}
}