feat: Add Gravatar control and demo.

This commit is contained in:
NaBian committed 2026-06-13 20:14:53 +08:00
1 parent ee5c47a7cd
commit 6c307a6ccc
9 files changed
+329 -17

No files matched your search

@@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:cv="clr-namespace:HandyControlDemo.Tools.Converter"
x:Class="HandyControlDemo.UserControl.GravatarDemo"
Background="{DynamicResource RegionBrush}">
<UserControl.Resources>
<cv:StringToBitmapConverter x:Key="StringToBitmap" />
</UserControl.Resources>
<TransitioningContentControl>
<ScrollViewer>
<hc:UniformSpacingPanel Margin="32" ChildWrapping="Wrap" Spacing="16" ItemHorizontalAlignment="Center">
<UniformGrid Rows="3" Columns="3" Width="240" Height="240">
<hc:Gravatar Id="User1" />
<hc:Gravatar Id="User2" />
<hc:Gravatar Theme="{StaticResource GravatarCircleImg}">
<Image Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Album/2.jpg', Converter={StaticResource StringToBitmap}}" />
</hc:Gravatar>
<hc:Gravatar Id="User4" />
<hc:Gravatar Id="User5" Theme="{StaticResource GravatarCircle}" />
<hc:Gravatar Id="User6" />
<hc:Gravatar Theme="{StaticResource GravatarCircle}"
Source="{Binding Source='avares://HandyControlDemo/Resources/Img/Album/1.jpg', Converter={StaticResource StringToBitmap}}" />
<hc:Gravatar Id="User8" />
<hc:Gravatar Id="User9" />
</UniformGrid>
<StackPanel Width="240" Height="220" VerticalAlignment="Center">
<TextBox Text="User1" Name="TextBoxName" Width="180" />
<hc:Gravatar Height="180" Width="180" Id="{Binding #TextBoxName.Text}" Margin="10" />
</StackPanel>
</hc:UniformSpacingPanel>
</ScrollViewer>
</TransitioningContentControl>
</UserControl>
@@ -0,0 +1,9 @@
namespace HandyControlDemo.UserControl;
public partial class GravatarDemo : Avalonia.Controls.UserControl
{
public GravatarDemo()
{
InitializeComponent();
}
}
@@ -23,27 +23,23 @@
<Button Command="{Binding RemoveItemCommand}" Content="{x:Static langs:Lang.RemoveItem}" />
</hc:ButtonGroup>
<Border Grid.Row="1" Padding="0" Theme="{StaticResource BorderRegion}">
<ItemsControl HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{Binding DataList}">
<ItemsControl.ItemsPanel>
<ListBox Background="Transparent"
BorderThickness="0"
Theme="{StaticResource ListBoxCustom}"
ItemsSource="{Binding DataList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<hc:HoneycombPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="data:CardModel">
<Border Width="60"
Height="60"
Margin="6"
CornerRadius="30"
ClipToBounds="True">
<Image Source="{Binding Content, Converter={StaticResource StringToBitmap}}"
Stretch="UniformToFill" />
</Border>
<hc:Gravatar Theme="{StaticResource GravatarCircle}"
Margin="10"
Source="{Binding Content, Converter={StaticResource StringToBitmap}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Grid>
</TransitioningContentControl>
@@ -0,0 +1,6 @@
namespace HandyControl.Controls;
public interface IGravatarGenerator
{
object? GetGravatar(string id);
}
@@ -0,0 +1,58 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using HandyControl.Tools;
namespace HandyControl.Controls;
public class Gravatar : ContentControl
{
public static readonly StyledProperty<IGravatarGenerator> GeneratorProperty =
AvaloniaProperty.Register<Gravatar, IGravatarGenerator>(
nameof(Generator), defaultValue: new GithubGravatarGenerator());
public IGravatarGenerator Generator
{
get => GetValue(GeneratorProperty);
set => SetValue(GeneratorProperty, value);
}
public static readonly StyledProperty<string?> IdProperty =
AvaloniaProperty.Register<Gravatar, string?>(nameof(Id));
public string? Id
{
get => GetValue(IdProperty);
set => SetValue(IdProperty, value);
}
public static readonly StyledProperty<IImage?> SourceProperty =
AvaloniaProperty.Register<Gravatar, IImage?>(nameof(Source));
public IImage? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
static Gravatar()
{
IdProperty.Changed.AddClassHandler<Gravatar>((o, e) => o.OnIdChanged(e));
SourceProperty.Changed.AddClassHandler<Gravatar>((o, e) => o.OnSourceChanged(e));
}
private void OnIdChanged(AvaloniaPropertyChangedEventArgs e)
{
if (Source != null) return;
Content = Generator.GetGravatar((string?) e.NewValue ?? string.Empty);
}
private void OnSourceChanged(AvaloniaPropertyChangedEventArgs e)
{
var v = (IImage?) e.NewValue;
Background = v is IImageBrushSource brushSource
? new ImageBrush(brushSource) { Stretch = Stretch.UniformToFill }
: ResourceHelper.GetResource<IBrush>(Data.ResourceToken.SecondaryRegionBrush);
}
}
@@ -0,0 +1,57 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
<ControlTheme x:Key="GravatarBaseStyle"
TargetType="hc:Gravatar">
<Setter Property="Focusable" Value="False" />
<Setter Property="Width" Value="72" />
<Setter Property="Height" Value="72" />
<Setter Property="Background" Value="{DynamicResource SecondaryRegionBrush}" />
<Setter Property="CornerRadius" Value="{StaticResource DefaultCornerRadius}" />
<Setter Property="Padding" Value="6" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:Gravatar">
<Border ClipToBounds="True"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:Gravatar}"
BasedOn="{StaticResource GravatarBaseStyle}"
TargetType="hc:Gravatar" />
<ControlTheme x:Key="GravatarCircle"
BasedOn="{StaticResource GravatarBaseStyle}"
TargetType="hc:Gravatar">
<Setter Property="Padding" Value="16" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:Gravatar">
<Border Theme="{StaticResource BorderCircularClip}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="GravatarCircleImg"
BasedOn="{StaticResource GravatarCircle}"
TargetType="hc:Gravatar">
<Setter Property="Padding" Value="0" />
</ControlTheme>
</ResourceDictionary>
@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls">
@@ -118,4 +118,36 @@
<Setter Property="ItemContainerTheme"
Value="{StaticResource ListBoxItemBaseStyle.Small}" />
</ControlTheme>
<ControlTheme x:Key="ListBoxItemCustom"
TargetType="ListBoxItem">
<Setter Property="Padding"
Value="0" />
<Setter Property="HorizontalContentAlignment"
Value="{Binding $parent[ItemsControl].HorizontalContentAlignment}" />
<Setter Property="VerticalContentAlignment"
Value="Top" />
<Setter Property="Background"
Value="Transparent" />
<Setter Property="BorderBrush"
Value="Transparent" />
<Setter Property="BorderThickness"
Value="0" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="ListBoxCustom"
BasedOn="{StaticResource ListBoxBaseStyle}"
TargetType="ListBox">
<Setter Property="ItemContainerTheme"
Value="{StaticResource ListBoxItemCustom}" />
</ControlTheme>
</ResourceDictionary>
@@ -60,6 +60,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Card.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ElementGroup.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ButtonGroup.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Gravatar.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>
@@ -0,0 +1,119 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Avalonia;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using HandyControl.Controls;
namespace HandyControl.Tools;
public class GithubGravatarGenerator : IGravatarGenerator
{
private const int RenderDataMaxLength = 15;
public object? GetGravatar(string id)
{
if (string.IsNullOrEmpty(id)) return null;
var hashcode = GetHashCode(id);
var renderData = GetRenderData(hashcode);
var renderBrush = GetRenderBrush(hashcode);
var geometryGroup = new GeometryGroup();
void AddRec(int i, int j, bool hidden = false)
{
var rec = new RectangleGeometry(new Rect(new Point(i, j), hidden ? new Size() : new Size(1, 1)));
geometryGroup.Children.Add(rec);
}
var index = 0;
for (var i = 0; i < 2; i++)
{
for (var j = 0; j < 5; j++, index++)
{
AddRec(i, j, renderData[index] == 0);
}
}
for (var j = 0; j < 5; j++, index++)
{
AddRec(2, j, renderData[index] == 0);
}
index -= 10;
for (var i = 3; i < 5; i++)
{
for (var j = 0; j < 5; j++, index++)
{
AddRec(i, j, renderData[index] == 0);
}
index -= 10;
}
var path = new Path
{
Data = geometryGroup,
Fill = renderBrush,
VerticalAlignment = VerticalAlignment.Top,
Stretch = Stretch.Uniform
};
return path;
}
private string GetHashCode(string id)
{
var bytes = Encoding.ASCII.GetBytes(id);
var hash = MD5.HashData(bytes);
var stringBuilder = new StringBuilder();
foreach (var item in hash)
{
stringBuilder.Append(item.ToString("X2"));
}
return stringBuilder.ToString();
}
private int[] GetRenderData(string hashcode)
{
var arr = new int[RenderDataMaxLength];
for (var i = 0; i < RenderDataMaxLength; i++)
{
var c = hashcode[i];
var v = (int) c;
arr[i] = v % 2;
}
return arr;
}
private IBrush GetRenderBrush(string hashcode)
{
var v = (double) int.Parse(hashcode.Substring(hashcode.Length - 7), NumberStyles.HexNumber);
var scale = v / 0xfffffff;
var color = Hsl2Rgb(scale);
return new SolidColorBrush(color);
}
//adapted from https://github.com/stewartlord/identicon.js/blob/4fad7cafec1b7a4d896015e084e861625ef5d64f/identicon.js#L110
private Color Hsl2Rgb(double h, double s = 0.7, double b = 0.5)
{
h *= 6;
var arr = new[]
{
b += s *= b < .5 ? b : 1 - b,
b - h % 1 * s * 2,
b -= s *= 2,
b,
b + h % 1 * s,
b + s
};
var hValue = (int) Math.Floor(h);
return Color.FromRgb((byte) (arr[hValue % 6] * 255), (byte) (arr[(hValue | 16) % 6] * 255),
(byte) (arr[(hValue | 8) % 6] * 255));
}
}