feat: Add HoneycombPanel and demo.

This commit is contained in:
NaBian committed 2026-06-13 16:16:48 +08:00
1 parent d1944b01c3
commit e7005d09b5
16 files changed
+263 -1

No files matched your search

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
@@ -19,12 +19,14 @@
<None Remove="Data\DemoInfo.json" />
<None Remove="Resources\Img\cloud.png" />
<None Remove="Resources\Img\icon.ico" />
<None Remove="Resources\Img\Avatar\*.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\DemoInfo.json" />
<AvaloniaResource Include="Resources\Img\cloud.png" />
<AvaloniaResource Include="Resources\Img\icon.ico" />
<AvaloniaResource Include="Resources\Img\Avatar\*.png" />
</ItemGroup>
<ItemGroup>
Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

@@ -0,0 +1,40 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:vm="clr-namespace:HandyControlDemo.ViewModel"
x:Class="HandyControlDemo.UserControl.HoneycombPanelDemo"
DataContext="{Binding HoneycombPanelDemo, Source={x:Static vm:ViewModelLocator.Instance}}">
<Grid Margin="32">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal"
Spacing="8"
Margin="0,0,0,16">
<Button Command="{Binding AddItemCommand}" Content="Add" />
<Button Command="{Binding RemoveItemCommand}" Content="Remove" />
</StackPanel>
<ItemsControl Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{Binding DataList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<hc:HoneycombPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="60"
Height="60"
Margin="6"
CornerRadius="30"
ClipToBounds="True">
<Image Source="{Binding}" Stretch="UniformToFill" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class HoneycombPanelDemo : Avalonia.Controls.UserControl
{
public HoneycombPanelDemo()
{
InitializeComponent();
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.ObjectModel;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace HandyControlDemo.ViewModel;
public partial class HoneycombPanelDemoViewModel : ObservableObject
{
private static readonly Random Random = new();
[ObservableProperty] private ObservableCollection<Bitmap> _dataList = [];
public HoneycombPanelDemoViewModel()
{
for (int i = 0; i < 7; i++)
{
DataList.Add(LoadRandomAvatar());
}
}
[RelayCommand]
private void AddItem() => DataList.Insert(0, LoadRandomAvatar());
[RelayCommand]
private void RemoveItem()
{
if (DataList.Count > 0)
{
DataList.RemoveAt(0);
}
}
private static Bitmap LoadRandomAvatar()
{
var uri = new Uri($"avares://HandyControlDemo/Resources/Img/Avatar/avatar{Random.Next(1, 7)}.png");
using var stream = AssetLoader.Open(uri);
return new Bitmap(stream);
}
}
@@ -20,6 +20,7 @@ public class ViewModelLocator
services.AddSingleton<DataService>();
services.AddTransient<MainViewModel>();
services.AddTransient<InputElementDemoViewModel>();
services.AddTransient<HoneycombPanelDemoViewModel>();
_serviceProvider = services.BuildServiceProvider();
}
@@ -27,4 +28,6 @@ public class ViewModelLocator
public MainViewModel Main => _serviceProvider.GetService<MainViewModel>()!;
public InputElementDemoViewModel InputElementDemo => _serviceProvider.GetService<InputElementDemoViewModel>()!;
public HoneycombPanelDemoViewModel HoneycombPanelDemo => _serviceProvider.GetService<HoneycombPanelDemoViewModel>()!;
}
@@ -0,0 +1,166 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
namespace HandyControl.Controls;
public class HoneycombPanel : Panel
{
private double _unitLength;
private HoneycombStuffer? _stuffer;
private static int GetXCount(int count)
{
if (count == 0) return 0;
count -= 1;
var index = (int) Math.Floor(Math.Pow((12.0 * count + 25) / 36, 0.5) - 5.0 / 6);
var valeIndex = 3 * index * index + 5 * index;
var centerValue = valeIndex + 2;
return count >= centerValue
? 4 * index + 6
: count > valeIndex
? 4 * index + 4
: 4 * index + 2;
}
private static int GetYCount(int count)
{
if (count == 0) return 0;
count -= 1;
var index = (int) Math.Floor(Math.Pow(count / 3.0 + 0.25, 0.5) - 0.5);
var valeIndex = 3 * index * index + 3 * index;
return count > valeIndex
? 2 * index + 2
: 2 * index;
}
/*
* layout order
*
* ● ●
* (7) (8)
*
* ● ● ●
* (6) (1) (9)
*
* ● ● ● ...
* (5) (0) (2)
*
* ● ● ...
* (4) (3)
*
*/
protected override Size MeasureOverride(Size availableSize)
{
var maxSize = new Size();
var layoutChildren = Children.Where(child => child.IsVisible).ToList();
foreach (var child in layoutChildren)
{
child.Measure(availableSize);
maxSize = new Size(
Math.Max(maxSize.Width, child.DesiredSize.Width),
Math.Max(maxSize.Height, child.DesiredSize.Height));
}
_unitLength = Math.Max(maxSize.Width, maxSize.Height) / 2;
var xCount = GetXCount(layoutChildren.Count);
var yCount = GetYCount(layoutChildren.Count);
var availableWidth = xCount * _unitLength;
var availableHeight = yCount * Math.Pow(3, 0.5) * _unitLength + _unitLength * 2;
return new Size(availableWidth, availableHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
var childLength = _unitLength * 2;
_stuffer = new HoneycombStuffer(new Rect(finalSize.Width / 2 - _unitLength,
finalSize.Height / 2 - _unitLength, childLength, childLength));
foreach (var child in Children)
{
if (!child.IsVisible) continue;
child.Arrange(_stuffer.Move());
}
return finalSize;
}
private class HoneycombStuffer
{
private int _turns;
private int _maxIndex;
private int _currentIndex = -1;
private readonly double _offsetX;
private readonly double _offsetY;
private Rect _childBounds;
private readonly double[] _offsetXArr;
private readonly double[] _offsetYArr;
public HoneycombStuffer(Rect childBounds)
{
_childBounds = childBounds;
_offsetX = childBounds.Width / 2;
_offsetY = Math.Pow(3, 0.5) * _offsetX;
_offsetXArr = new[]
{
2 * _offsetX,
_offsetX,
-_offsetX,
-2 * _offsetX,
-_offsetX,
_offsetX
};
_offsetYArr = new[]
{
0,
_offsetY,
_offsetY,
0,
-_offsetY,
-_offsetY
};
}
public Rect Move()
{
_currentIndex++;
if (_currentIndex > _maxIndex)
{
_turns++;
_maxIndex = _turns * 6 - 1;
_currentIndex = 0;
_childBounds = _childBounds.Translate(new Vector(_offsetX, -_offsetY));
return _childBounds;
}
if (_turns > 0)
{
var index = _currentIndex / _turns;
_childBounds = _childBounds.Translate(new Vector(_offsetXArr[index], _offsetYArr[index]));
}
return _childBounds;
}
}
}