mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-23 01:38:40 +08:00
Merge remote-tracking branch 'origin/feature/avatar' into develop
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<ResourceInclude Source="AdornerLayer/Themes/AdornerLayer.axaml" />
|
||||
<ResourceInclude Source="Alert/Themes/AlertTheme.axaml" />
|
||||
<ResourceInclude Source="ArrowDecoratedBox/Themes/ArrowDecoratedBoxThemes.axaml" />
|
||||
<ResourceInclude Source="Avatar/Themes/AvatarTheme.axaml" />
|
||||
<ResourceInclude Source="Badge/Themes/BadgeThemes.axaml" />
|
||||
<ResourceInclude Source="Buttons/Themes/ButtonThemes.axaml" />
|
||||
<ResourceInclude Source="ButtonSpinner/Themes/ButtonSpinnerThemes.axaml" />
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
using System.Diagnostics;
|
||||
using AtomUI.Controls.Themes;
|
||||
using AtomUI.IconPkg;
|
||||
using AtomUI.Media;
|
||||
using AtomUI.Theme;
|
||||
using AtomUI.Theme.Utils;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Transformation;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
public enum AvatarSizeType
|
||||
{
|
||||
Large,
|
||||
Middle,
|
||||
Small,
|
||||
Custom
|
||||
}
|
||||
|
||||
public enum AvatarShape
|
||||
{
|
||||
Circle,
|
||||
Square
|
||||
}
|
||||
|
||||
internal enum AvatarContentType
|
||||
{
|
||||
Icon,
|
||||
BitmapImage,
|
||||
SvgImage,
|
||||
Text
|
||||
}
|
||||
|
||||
public class Avatar : TemplatedControl, IControlSharedTokenResourcesHost, IMotionAwareControl
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
public static readonly StyledProperty<double> GapProperty =
|
||||
AvaloniaProperty.Register<Avatar, double>(nameof(Gap), defaultValue: 4.0);
|
||||
|
||||
public static readonly StyledProperty<Icon?> IconProperty =
|
||||
AvaloniaProperty.Register<Avatar, Icon?>(nameof(Icon));
|
||||
|
||||
public static readonly StyledProperty<IImage?> BitmapSrcProperty =
|
||||
AvaloniaProperty.Register<Avatar, IImage?>(nameof(BitmapSrc));
|
||||
|
||||
public static readonly StyledProperty<string?> SrcProperty =
|
||||
AvaloniaProperty.Register<Avatar, string?>(nameof(Src));
|
||||
|
||||
public static readonly StyledProperty<string?> TextProperty =
|
||||
AvaloniaProperty.Register<TemplatedControl, string?>(nameof (Text));
|
||||
|
||||
public static readonly StyledProperty<AvatarSizeType> SizeTypeProperty =
|
||||
AvaloniaProperty.Register<Avatar, AvatarSizeType>(nameof(SizeType),
|
||||
AvatarSizeType.Middle);
|
||||
|
||||
public static readonly StyledProperty<double> SizeProperty =
|
||||
AvaloniaProperty.Register<Avatar, double>(nameof(Size), Double.NaN);
|
||||
|
||||
public static readonly StyledProperty<AvatarShape> ShapeProperty =
|
||||
AvaloniaProperty.Register<Avatar, AvatarShape>(nameof(Shape), AvatarShape.Circle);
|
||||
|
||||
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
|
||||
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<Avatar>();
|
||||
|
||||
public double Gap
|
||||
{
|
||||
get => GetValue(GapProperty);
|
||||
set => SetValue(GapProperty, value);
|
||||
}
|
||||
|
||||
public Icon? Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public IImage? BitmapSrc
|
||||
{
|
||||
get => GetValue(BitmapSrcProperty);
|
||||
set => SetValue(BitmapSrcProperty, value);
|
||||
}
|
||||
|
||||
public string? Src
|
||||
{
|
||||
get => GetValue(SrcProperty);
|
||||
set => SetValue(SrcProperty, value);
|
||||
}
|
||||
|
||||
[Content]
|
||||
public string? Text
|
||||
{
|
||||
get => GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public AvatarSizeType SizeType
|
||||
{
|
||||
get => GetValue(SizeTypeProperty);
|
||||
set => SetValue(SizeTypeProperty, value);
|
||||
}
|
||||
|
||||
public double Size
|
||||
{
|
||||
get => GetValue(SizeProperty);
|
||||
set => SetValue(SizeProperty, value);
|
||||
}
|
||||
|
||||
public AvatarShape Shape
|
||||
{
|
||||
get => GetValue(ShapeProperty);
|
||||
set => SetValue(ShapeProperty, value);
|
||||
}
|
||||
|
||||
public bool IsMotionEnabled
|
||||
{
|
||||
get => GetValue(IsMotionEnabledProperty);
|
||||
set => SetValue(IsMotionEnabledProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部属性定义
|
||||
internal static readonly StyledProperty<double> EffectiveIconSizeProperty =
|
||||
AvaloniaProperty.Register<Avatar, double>(nameof(EffectiveIconSize));
|
||||
|
||||
internal static readonly StyledProperty<ITransform?> TextRenderTransformProperty =
|
||||
AvaloniaProperty.Register<Avatar, ITransform?>(nameof(TextRenderTransform));
|
||||
|
||||
internal static readonly DirectProperty<Avatar, AvatarContentType> ContentTypeProperty =
|
||||
AvaloniaProperty.RegisterDirect<Avatar, AvatarContentType>(
|
||||
nameof(ContentType),
|
||||
o => o.ContentType,
|
||||
(o, v) => o.ContentType = v);
|
||||
|
||||
internal double EffectiveIconSize
|
||||
{
|
||||
get => GetValue(EffectiveIconSizeProperty);
|
||||
set => SetValue(EffectiveIconSizeProperty, value);
|
||||
}
|
||||
|
||||
public ITransform? TextRenderTransform
|
||||
{
|
||||
get => GetValue(TextRenderTransformProperty);
|
||||
set => SetValue(TextRenderTransformProperty, value);
|
||||
}
|
||||
|
||||
private AvatarContentType _contentType = AvatarContentType.Icon;
|
||||
|
||||
internal AvatarContentType ContentType
|
||||
{
|
||||
get => _contentType;
|
||||
set => SetAndRaise(ContentTypeProperty, ref _contentType, value);
|
||||
}
|
||||
|
||||
Control IMotionAwareControl.PropertyBindTarget => this;
|
||||
Control IControlSharedTokenResourcesHost.HostControl => this;
|
||||
string IControlSharedTokenResourcesHost.TokenId => ButtonToken.ID;
|
||||
|
||||
#endregion
|
||||
|
||||
private AvatarSizeType? _originSizeType;
|
||||
private TextBlock? _textPresenter;
|
||||
|
||||
static Avatar()
|
||||
{
|
||||
AffectsMeasure<Avatar>(SizeTypeProperty, TextProperty);
|
||||
AffectsRender<Avatar>(ShapeProperty, IconProperty, SrcProperty, GapProperty);
|
||||
}
|
||||
|
||||
public Avatar()
|
||||
{
|
||||
this.RegisterResources();
|
||||
this.BindMotionProperties();
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == SizeProperty)
|
||||
{
|
||||
if (!double.IsNaN(Size))
|
||||
{
|
||||
_originSizeType = SizeType;
|
||||
SizeType = AvatarSizeType.Custom;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_originSizeType.HasValue)
|
||||
{
|
||||
SizeType = _originSizeType.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (change.Property == SizeTypeProperty)
|
||||
{
|
||||
ConfigureIconSize();
|
||||
}
|
||||
else if (change.Property == SrcProperty ||
|
||||
change.Property == IconProperty ||
|
||||
change.Property == TextProperty)
|
||||
{
|
||||
ConfigureContentType();
|
||||
}
|
||||
if (change.Property == ContentTypeProperty ||
|
||||
change.Property == GapProperty)
|
||||
{
|
||||
ConfigureTextRenderTransform();
|
||||
}
|
||||
else if (change.Property == ShapeProperty)
|
||||
{
|
||||
ConfigureShape();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_textPresenter = e.NameScope.Find<TextBlock>(AvatarThemeConstants.TextPresenterPart);
|
||||
if (_textPresenter != null)
|
||||
{
|
||||
_textPresenter.SizeChanged += (sender, args) =>
|
||||
{
|
||||
ConfigureTextRenderTransform();
|
||||
};
|
||||
}
|
||||
ConfigureShape();
|
||||
ConfigureIconSize();
|
||||
ConfigureContentType();
|
||||
}
|
||||
|
||||
private void ConfigureIconSize()
|
||||
{
|
||||
if (SizeType == AvatarSizeType.Custom)
|
||||
{
|
||||
Debug.Assert(!double.IsNaN(Size));
|
||||
// 不影响模板设置
|
||||
SetValue(WidthProperty, Size, BindingPriority.Template);
|
||||
SetValue(HeightProperty, Size, BindingPriority.Template);
|
||||
SetValue(EffectiveIconSizeProperty,
|
||||
(Icon != null || Src != null) ? Size / 2 : 18, BindingPriority.Template);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureContentType()
|
||||
{
|
||||
if (Src != null)
|
||||
{
|
||||
ContentType = AvatarContentType.SvgImage;
|
||||
}
|
||||
else if (BitmapSrc != null)
|
||||
{
|
||||
ContentType = AvatarContentType.BitmapImage;
|
||||
}
|
||||
else if (Text != null)
|
||||
{
|
||||
ContentType = AvatarContentType.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
ContentType = AvatarContentType.Icon;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureTextRenderTransform()
|
||||
{
|
||||
if (ContentType != AvatarContentType.Text)
|
||||
{
|
||||
TextRenderTransform = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_textPresenter != null && Gap * 2 < Width)
|
||||
{
|
||||
double scale = 1;
|
||||
double offsetX = 0;
|
||||
var textWidth = TextUtils.CalculateTextSize(Text ?? string.Empty, FontSize, FontFamily).Width;
|
||||
if (Gap * 2 < Width)
|
||||
{
|
||||
scale = (Width - Gap * 2) / textWidth;
|
||||
scale = Math.Min(scale, 1.0);
|
||||
offsetX = (scale * (textWidth - Width)) / 2;
|
||||
}
|
||||
var builder = new TransformOperations.Builder(2);
|
||||
builder.AppendScale(scale, scale);
|
||||
if (scale < 1.0)
|
||||
{
|
||||
builder.AppendTranslate(-offsetX, 0);
|
||||
}
|
||||
TextRenderTransform = builder.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
TextRenderTransform = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureShape()
|
||||
{
|
||||
if (Shape == AvatarShape.Circle)
|
||||
{
|
||||
SetValue(CornerRadiusProperty, new CornerRadius(Width / 2), BindingPriority.Template);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Reactive.Disposables;
|
||||
using AtomUI.Data;
|
||||
using AtomUI.Theme;
|
||||
using AtomUI.Theme.Data;
|
||||
using AtomUI.Theme.Styling;
|
||||
using AtomUI.Theme.Utils;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
using ControlList = Avalonia.Controls.Controls;
|
||||
|
||||
public class AvatarGroup : Control,
|
||||
IMotionAwareControl,
|
||||
IControlSharedTokenResourcesHost,
|
||||
IResourceBindingManager
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
public static readonly StyledProperty<IBrush?> FoldInfoAvatarBackgroundProperty =
|
||||
AvaloniaProperty.Register<AvatarGroup, IBrush?>(nameof (FoldInfoAvatarBackground));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> FoldInfoAvatarForegroundProperty =
|
||||
AvaloniaProperty.Register<AvatarGroup, IBrush?>(nameof (FoldInfoAvatarForeground));
|
||||
|
||||
public static readonly StyledProperty<FlyoutTriggerType> FoldAvatarFlyoutTriggerTypeProperty =
|
||||
AvaloniaProperty.Register<AvatarGroup, FlyoutTriggerType>(nameof(FoldAvatarFlyoutTriggerType), FlyoutTriggerType.Hover);
|
||||
|
||||
public static readonly StyledProperty<Thickness> BorderThicknessProperty =
|
||||
Border.BorderThicknessProperty.AddOwner<AvatarGroup>();
|
||||
|
||||
public static readonly StyledProperty<int?> MaxDisplayCountProperty =
|
||||
AvaloniaProperty.Register<AvatarGroup, int?>(nameof(MaxDisplayCount));
|
||||
|
||||
public static readonly StyledProperty<AvatarSizeType> SizeTypeProperty =
|
||||
Avatar.SizeTypeProperty.AddOwner<AvatarGroup>();
|
||||
|
||||
public static readonly StyledProperty<double> SizeProperty =
|
||||
Avatar.SizeProperty.AddOwner<AvatarGroup>();
|
||||
|
||||
public static readonly StyledProperty<AvatarShape> ShapeProperty =
|
||||
Avatar.ShapeProperty.AddOwner<AvatarGroup>();
|
||||
|
||||
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
|
||||
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<AvatarGroup>();
|
||||
|
||||
public IBrush? FoldInfoAvatarBackground
|
||||
{
|
||||
get => GetValue(FoldInfoAvatarBackgroundProperty);
|
||||
set => SetValue(FoldInfoAvatarBackgroundProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? FoldInfoAvatarForeground
|
||||
{
|
||||
get => GetValue(FoldInfoAvatarForegroundProperty);
|
||||
set => SetValue(FoldInfoAvatarForegroundProperty, value);
|
||||
}
|
||||
|
||||
public FlyoutTriggerType FoldAvatarFlyoutTriggerType
|
||||
{
|
||||
get => GetValue(FoldAvatarFlyoutTriggerTypeProperty);
|
||||
set => SetValue(FoldAvatarFlyoutTriggerTypeProperty, value);
|
||||
}
|
||||
|
||||
public Thickness BorderThickness
|
||||
{
|
||||
get => GetValue(BorderThicknessProperty);
|
||||
set => SetValue(BorderThicknessProperty, value);
|
||||
}
|
||||
|
||||
public int? MaxDisplayCount
|
||||
{
|
||||
get => GetValue(MaxDisplayCountProperty);
|
||||
set => SetValue(MaxDisplayCountProperty, value);
|
||||
}
|
||||
|
||||
public AvatarSizeType SizeType
|
||||
{
|
||||
get => GetValue(SizeTypeProperty);
|
||||
set => SetValue(SizeTypeProperty, value);
|
||||
}
|
||||
|
||||
public double Size
|
||||
{
|
||||
get => GetValue(SizeProperty);
|
||||
set => SetValue(SizeProperty, value);
|
||||
}
|
||||
|
||||
public AvatarShape Shape
|
||||
{
|
||||
get => GetValue(ShapeProperty);
|
||||
set => SetValue(ShapeProperty, value);
|
||||
}
|
||||
|
||||
public bool IsMotionEnabled
|
||||
{
|
||||
get => GetValue(IsMotionEnabledProperty);
|
||||
set => SetValue(IsMotionEnabledProperty, value);
|
||||
}
|
||||
|
||||
[Content] public ControlList Children { get; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部属性定义
|
||||
|
||||
internal static readonly DirectProperty<AvatarGroup, double> GroupSpaceProperty =
|
||||
AvaloniaProperty.RegisterDirect<AvatarGroup, double>(
|
||||
nameof(GroupSpace),
|
||||
o => o.GroupSpace,
|
||||
(o, v) => o.GroupSpace = v);
|
||||
|
||||
internal static readonly DirectProperty<AvatarGroup, double> GroupOverlappingProperty =
|
||||
AvaloniaProperty.RegisterDirect<AvatarGroup, double>(
|
||||
nameof(GroupOverlapping),
|
||||
o => o.GroupOverlapping,
|
||||
(o, v) => o.GroupOverlapping = v);
|
||||
|
||||
private double _groupSpace;
|
||||
|
||||
internal double GroupSpace
|
||||
{
|
||||
get => _groupSpace;
|
||||
set => SetAndRaise(GroupSpaceProperty, ref _groupSpace, value);
|
||||
}
|
||||
|
||||
private double _groupOverlapping;
|
||||
|
||||
internal double GroupOverlapping
|
||||
{
|
||||
get => _groupOverlapping;
|
||||
set => SetAndRaise(GroupOverlappingProperty, ref _groupOverlapping, value);
|
||||
}
|
||||
|
||||
Control IMotionAwareControl.PropertyBindTarget => this;
|
||||
Control IControlSharedTokenResourcesHost.HostControl => this;
|
||||
string IControlSharedTokenResourcesHost.TokenId => ButtonToken.ID;
|
||||
|
||||
CompositeDisposable? IResourceBindingManager.ResourceBindingsDisposable
|
||||
{
|
||||
get => _resourceBindingsDisposable;
|
||||
set => _resourceBindingsDisposable = value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private CompositeDisposable? _resourceBindingsDisposable;
|
||||
private Avatar? _foldCountAvatar;
|
||||
private FlyoutHost? _foldCountFlyout;
|
||||
private StackPanel? _foldCountStackPanel;
|
||||
private Dictionary<Control, CompositeDisposable> _disposables = new();
|
||||
|
||||
static AvatarGroup()
|
||||
{
|
||||
AffectsMeasure<AvatarGroup>(GroupOverlappingProperty);
|
||||
}
|
||||
|
||||
public AvatarGroup()
|
||||
{
|
||||
Children.CollectionChanged += ChildrenChanged;
|
||||
this.RegisterResources();
|
||||
this.BindMotionProperties();
|
||||
}
|
||||
|
||||
protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
var tobeAdded = e.NewItems!.OfType<Control>().ToList();
|
||||
foreach (var item in tobeAdded)
|
||||
{
|
||||
if (item is Avatar avatar)
|
||||
{
|
||||
ConfigureAvatar(avatar);
|
||||
}
|
||||
}
|
||||
if (!MaxDisplayCount.HasValue || LogicalChildren.Count < MaxDisplayCount.Value)
|
||||
{
|
||||
var startingIndex = e.NewStartingIndex;
|
||||
foreach (var item in tobeAdded)
|
||||
{
|
||||
LogicalChildren.Insert(startingIndex, item);
|
||||
VisualChildren.Insert(startingIndex, item);
|
||||
startingIndex++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
if (!MaxDisplayCount.HasValue)
|
||||
{
|
||||
LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
|
||||
VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (e.OldStartingIndex < MaxDisplayCount.Value &&
|
||||
e.OldStartingIndex + e.OldItems!.Count <= MaxDisplayCount.Value &&
|
||||
e.NewStartingIndex <= MaxDisplayCount.Value)
|
||||
{
|
||||
LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
|
||||
VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
var items = e.OldItems!.OfType<Control>().ToList();
|
||||
LogicalChildren.RemoveAll(items);
|
||||
VisualChildren.RemoveAll(items);
|
||||
foreach (var child in items)
|
||||
{
|
||||
if (_disposables.TryGetValue(child, out var disposable))
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
_disposables.Remove(child);
|
||||
}
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
if (!MaxDisplayCount.HasValue)
|
||||
{
|
||||
for (var i = 0; i < e.OldItems!.Count; ++i)
|
||||
{
|
||||
var index = i + e.OldStartingIndex;
|
||||
var child = (Control)e.NewItems![i]!;
|
||||
LogicalChildren[index] = child;
|
||||
VisualChildren[index] = child;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < e.OldItems!.Count; ++i)
|
||||
{
|
||||
var index = i + e.OldStartingIndex;
|
||||
if (index < MaxDisplayCount.Value)
|
||||
{
|
||||
var child = (Control)e.NewItems![i]!;
|
||||
LogicalChildren[index] = child;
|
||||
VisualChildren[index] = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var zindex = 1;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.ZIndex = zindex++;
|
||||
}
|
||||
|
||||
ConfigureFoldInfo();
|
||||
InvalidateMeasureOnChildrenChanged();
|
||||
}
|
||||
|
||||
private void ConfigureAvatar(Avatar avatar)
|
||||
{
|
||||
var disposable = new CompositeDisposable();
|
||||
disposable.Add(BindUtils.RelayBind(this, BorderThicknessProperty, avatar, BorderThicknessProperty));
|
||||
disposable.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, avatar, IsMotionEnabledProperty));
|
||||
disposable.Add(BindUtils.RelayBind(this, ShapeProperty, avatar, ShapeProperty));
|
||||
disposable.Add(BindUtils.RelayBind(this, SizeProperty, avatar, SizeProperty));
|
||||
disposable.Add(BindUtils.RelayBind(this, SizeTypeProperty, avatar, SizeTypeProperty));
|
||||
_disposables.Add(avatar, disposable);
|
||||
}
|
||||
|
||||
private Avatar GetFoldCountAvatar()
|
||||
{
|
||||
_foldCountAvatar ??= new Avatar();
|
||||
BindUtils.RelayBind(this, BorderThicknessProperty, _foldCountAvatar, BorderThicknessProperty);
|
||||
BindUtils.RelayBind(this, ShapeProperty, _foldCountAvatar, ShapeProperty);
|
||||
BindUtils.RelayBind(this, SizeProperty, _foldCountAvatar, SizeProperty);
|
||||
BindUtils.RelayBind(this, SizeTypeProperty, _foldCountAvatar, SizeTypeProperty);
|
||||
BindUtils.RelayBind(this, FoldInfoAvatarForegroundProperty, _foldCountAvatar, Avatar.ForegroundProperty);
|
||||
BindUtils.RelayBind(this, FoldInfoAvatarBackgroundProperty, _foldCountAvatar, Avatar.BackgroundProperty);
|
||||
return _foldCountAvatar;
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_resourceBindingsDisposable = new CompositeDisposable();
|
||||
this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, BorderThicknessProperty,
|
||||
SharedTokenKey.BorderThickness,
|
||||
BindingPriority.Template,
|
||||
new RenderScaleAwareThicknessConfigure(this)));
|
||||
this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, GroupSpaceProperty,
|
||||
AvatarTokenKey.GroupSpace));
|
||||
this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, GroupOverlappingProperty,
|
||||
AvatarTokenKey.GroupOverlapping));
|
||||
this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, FoldInfoAvatarForegroundProperty,
|
||||
AvatarTokenKey.AvatarColor));
|
||||
this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, FoldInfoAvatarBackgroundProperty,
|
||||
AvatarTokenKey.AvatarBg));
|
||||
var foldCountAvatar = GetFoldCountAvatar();
|
||||
if (_foldCountFlyout == null)
|
||||
{
|
||||
_foldCountFlyout = new FlyoutHost();
|
||||
_foldCountFlyout.ZIndex = Int32.MaxValue;
|
||||
_foldCountFlyout.AnchorTarget = foldCountAvatar;
|
||||
_foldCountStackPanel = new StackPanel();
|
||||
_foldCountStackPanel.Orientation = Orientation.Horizontal;
|
||||
BindUtils.RelayBind(this, GroupSpaceProperty, _foldCountStackPanel, StackPanel.SpacingProperty);
|
||||
BindUtils.RelayBind(this, FoldAvatarFlyoutTriggerTypeProperty, _foldCountFlyout, FlyoutHost.TriggerProperty);
|
||||
_foldCountFlyout.Flyout = new Flyout()
|
||||
{
|
||||
Content = _foldCountStackPanel
|
||||
};
|
||||
}
|
||||
|
||||
ConfigureFoldInfo();
|
||||
}
|
||||
|
||||
private void ConfigureFoldInfo()
|
||||
{
|
||||
var foldCountAvatar = GetFoldCountAvatar();
|
||||
if (_foldCountFlyout != null)
|
||||
{
|
||||
if (MaxDisplayCount.HasValue && Children.Count > MaxDisplayCount.Value)
|
||||
{
|
||||
foldCountAvatar.Text = $"+{Children.Count - MaxDisplayCount.Value}";
|
||||
LogicalChildren.Add(_foldCountFlyout);
|
||||
VisualChildren.Add(_foldCountFlyout);
|
||||
for (var i = MaxDisplayCount.Value; i < Children.Count; ++i)
|
||||
{
|
||||
_foldCountStackPanel?.Children.Add(Children[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogicalChildren.Remove(_foldCountFlyout);
|
||||
VisualChildren.Remove(_foldCountFlyout);
|
||||
_foldCountStackPanel?.Children.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
this.DisposeTokenBindings();
|
||||
}
|
||||
|
||||
private protected virtual void InvalidateMeasureOnChildrenChanged()
|
||||
{
|
||||
InvalidateMeasure();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var size = base.MeasureOverride(availableSize);
|
||||
// 理论上是统一的,我们的孩子都一样大
|
||||
var count = LogicalChildren.Count;
|
||||
var totalWidth = count * size.Width - (count - 1) * GroupOverlapping;
|
||||
return size.WithWidth(totalWidth);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var offetX = 0.0;
|
||||
for (var i = 0; i < LogicalChildren.Count; i++)
|
||||
{
|
||||
var child = LogicalChildren[i];
|
||||
if (child is Control avatar)
|
||||
{
|
||||
var childSize = avatar.DesiredSize;
|
||||
avatar.Arrange(new Rect(offetX, 0, childSize.Width, childSize.Height));
|
||||
offetX += childSize.Width - GroupOverlapping;
|
||||
}
|
||||
}
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == FoldAvatarFlyoutTriggerTypeProperty)
|
||||
{
|
||||
ConfigureFoldAvatarCursor();
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureFoldAvatarCursor()
|
||||
{
|
||||
var foldCountAvatar = GetFoldCountAvatar();
|
||||
if (FoldAvatarFlyoutTriggerType == FlyoutTriggerType.Click)
|
||||
{
|
||||
|
||||
foldCountAvatar.Cursor = new Cursor(StandardCursorType.Hand);
|
||||
}
|
||||
else
|
||||
{
|
||||
foldCountAvatar.Cursor = new Cursor(StandardCursorType.Arrow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
public static class AvatarPseudoClass
|
||||
{
|
||||
public const string Large = ":large";
|
||||
public const string Middle = ":middle";
|
||||
public const string Small = ":small";
|
||||
public const string Custom = ":custom-size";
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using AtomUI.Theme.TokenSystem;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace AtomUI.Controls;
|
||||
|
||||
[ControlDesignToken]
|
||||
internal class AvatarToken : AbstractControlDesignToken
|
||||
{
|
||||
public const string ID = "Avatar";
|
||||
|
||||
/// <summary>
|
||||
/// 头像尺寸
|
||||
/// </summary>
|
||||
public double ContainerSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 大号头像尺寸
|
||||
/// </summary>
|
||||
public double ContainerSizeLG { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小号头像尺寸
|
||||
/// </summary>
|
||||
public double ContainerSizeSM { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像文字大小
|
||||
/// </summary>
|
||||
public double TextFontSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 大号头像文字大小
|
||||
/// </summary>
|
||||
public double TextFontSizeLG { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小号头像文字大小
|
||||
/// </summary>
|
||||
public double TextFontSizeSM { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像组间距
|
||||
/// </summary>
|
||||
public double GroupSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像组重叠宽度
|
||||
/// </summary>
|
||||
public double GroupOverlapping { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像组边框颜色
|
||||
/// </summary>
|
||||
public Color GroupBorderColor { get; set; }
|
||||
|
||||
// 内部使用别名
|
||||
public Color AvatarBg { get; set; }
|
||||
public Color AvatarColor { get; set; }
|
||||
|
||||
public AvatarToken()
|
||||
: base(ID)
|
||||
{
|
||||
}
|
||||
|
||||
protected internal override void CalculateFromAlias()
|
||||
{
|
||||
base.CalculateFromAlias();
|
||||
ContainerSize = SharedToken.ControlHeight;
|
||||
ContainerSizeLG = SharedToken.ControlHeightLG;
|
||||
ContainerSizeSM = SharedToken.ControlHeightSM;
|
||||
TextFontSize = Math.Round((SharedToken.FontSizeLG + SharedToken.FontSizeXL) / 2);
|
||||
TextFontSizeLG = SharedToken.FontSizeHeading3;
|
||||
TextFontSizeSM = SharedToken.FontSize;
|
||||
GroupSpace = SharedToken.UniformlyMarginXXS;
|
||||
GroupOverlapping = SharedToken.UniformlyMarginXS;
|
||||
GroupBorderColor = SharedToken.ColorBorderBg;
|
||||
|
||||
AvatarBg = SharedToken.ColorTextPlaceholder;
|
||||
AvatarColor = SharedToken.ColorTextLightSolid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net">
|
||||
<ControlTheme x:Key="{x:Type atom:Avatar}" TargetType="atom:Avatar">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Panel Name="RootLayout">
|
||||
<Border Name="Frame"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"/>
|
||||
<atom:IconPresenter Name="IconPresenter"
|
||||
IconWidth="{TemplateBinding EffectiveIconSize}"
|
||||
IconHeight="{TemplateBinding EffectiveIconSize}"
|
||||
Icon="{TemplateBinding Icon}"
|
||||
NormalFilledBrush="{TemplateBinding Foreground}"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
<Image Name="ImagePresenter"
|
||||
Width="{TemplateBinding Width}"
|
||||
Height="{TemplateBinding Height}"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Source="{TemplateBinding BitmapSrc}"/>
|
||||
<Svg Name="SvgPresenter"
|
||||
Width="{TemplateBinding Width}"
|
||||
Height="{TemplateBinding Height}"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Path="{TemplateBinding Src}" />
|
||||
<atom:TextBlock Name="{x:Static atom:AvatarThemeConstants.TextPresenterPart}"
|
||||
Text="{TemplateBinding Text}"
|
||||
RenderTransform="{TemplateBinding TextRenderTransform}"
|
||||
IsVisible="False"
|
||||
ClipToBounds="False"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Panel>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static atom:AvatarTokenKey.AvatarColor}}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static atom:AvatarTokenKey.GroupBorderColor}}" />
|
||||
<Style Selector="^[ContentType=Icon]">
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static atom:AvatarTokenKey.AvatarBg}}" />
|
||||
<Style Selector="^ /template/ atom|IconPresenter#IconPresenter">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[ContentType=Text]">
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static atom:AvatarTokenKey.AvatarBg}}" />
|
||||
<Style Selector="^ /template/ atom|TextBlock#PART_TextPresenter">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[ContentType=BitmapImage]">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style Selector="^ /template/ Image#ImagePresenter">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[ContentType=SvgImage]">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style Selector="^ /template/ Svg#SvgPresenter">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SizeType=Large]">
|
||||
<Setter Property="Width" Value="{DynamicResource {x:Static atom:AvatarTokenKey.ContainerSizeLG}}" />
|
||||
<Setter Property="Height" Value="{DynamicResource {x:Static atom:AvatarTokenKey.ContainerSizeLG}}" />
|
||||
<Setter Property="EffectiveIconSize"
|
||||
Value="{DynamicResource {x:Static atom:AvatarTokenKey.TextFontSizeLG}}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SizeType=Middle]">
|
||||
<Setter Property="Width" Value="{DynamicResource {x:Static atom:AvatarTokenKey.ContainerSize}}" />
|
||||
<Setter Property="Height" Value="{DynamicResource {x:Static atom:AvatarTokenKey.ContainerSize}}" />
|
||||
<Setter Property="EffectiveIconSize" Value="{DynamicResource {x:Static atom:AvatarTokenKey.TextFontSize}}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SizeType=Small]">
|
||||
<Setter Property="Width" Value="{DynamicResource {x:Static atom:AvatarTokenKey.ContainerSizeSM}}" />
|
||||
<Setter Property="Height" Value="{DynamicResource {x:Static atom:AvatarTokenKey.ContainerSizeSM}}" />
|
||||
<Setter Property="EffectiveIconSize"
|
||||
Value="{DynamicResource {x:Static atom:AvatarTokenKey.TextFontSizeSM}}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[Shape=Square]">
|
||||
<Style Selector="^[SizeType=Large]">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadiusLG}}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SizeType=Middle]">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadius}}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SizeType=Small]">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadiusSM}}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[SizeType=Custom]">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource {x:Static atom:SharedTokenKey.BorderRadius}}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AtomUI.Controls.Themes;
|
||||
|
||||
internal static class AvatarThemeConstants
|
||||
{
|
||||
public const string TextPresenterPart = "PART_TextPresenter";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="AvatarTheme.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
+1
@@ -12,6 +12,7 @@ namespace AtomUI.Theme
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.AdornerLayerToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.AlertToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.ArrowDecoratedBoxToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.AvatarToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.BadgeToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.ButtonSpinnerToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Controls.ButtonToken));
|
||||
|
||||
+15
@@ -47,6 +47,21 @@ namespace AtomUI.Theme.Styling
|
||||
public static readonly TokenResourceKey Padding = new TokenResourceKey("ArrowDecoratedBox.Padding");
|
||||
}
|
||||
|
||||
public static class AvatarTokenKey
|
||||
{
|
||||
public static readonly TokenResourceKey AvatarBg = new TokenResourceKey("Avatar.AvatarBg");
|
||||
public static readonly TokenResourceKey AvatarColor = new TokenResourceKey("Avatar.AvatarColor");
|
||||
public static readonly TokenResourceKey ContainerSize = new TokenResourceKey("Avatar.ContainerSize");
|
||||
public static readonly TokenResourceKey ContainerSizeLG = new TokenResourceKey("Avatar.ContainerSizeLG");
|
||||
public static readonly TokenResourceKey ContainerSizeSM = new TokenResourceKey("Avatar.ContainerSizeSM");
|
||||
public static readonly TokenResourceKey GroupBorderColor = new TokenResourceKey("Avatar.GroupBorderColor");
|
||||
public static readonly TokenResourceKey GroupOverlapping = new TokenResourceKey("Avatar.GroupOverlapping");
|
||||
public static readonly TokenResourceKey GroupSpace = new TokenResourceKey("Avatar.GroupSpace");
|
||||
public static readonly TokenResourceKey TextFontSize = new TokenResourceKey("Avatar.TextFontSize");
|
||||
public static readonly TokenResourceKey TextFontSizeLG = new TokenResourceKey("Avatar.TextFontSizeLG");
|
||||
public static readonly TokenResourceKey TextFontSizeSM = new TokenResourceKey("Avatar.TextFontSizeSM");
|
||||
}
|
||||
|
||||
public static class BadgeTokenKey
|
||||
{
|
||||
public static readonly TokenResourceKey BadgeColor = new TokenResourceKey("Badge.BadgeColor");
|
||||
|
||||
Reference in New Issue
Block a user