Refactor badge Motion

This commit is contained in:
polarboy 2024-10-01 17:59:58 +08:00
parent d604d2aec3
commit 2ef0690a96
9 changed files with 347 additions and 611 deletions

View File

@ -63,6 +63,9 @@ internal class BadgeToken : AbstractControlDesignToken
public int BadgeRibbonCornerDarkenAmount { get; set; } public int BadgeRibbonCornerDarkenAmount { get; set; }
public Thickness BadgeRibbonTextPadding { get; set; } public Thickness BadgeRibbonTextPadding { get; set; }
public Thickness DotBadgeLabelMargin { get; set; } public Thickness DotBadgeLabelMargin { get; set; }
public Thickness CountBadgeTextPadding { get; set; }
public CornerRadius CountBadgeCornerRadius { get; set; }
public CornerRadius CountBadgeCornerRadiusSM { get; set; }
#endregion #endregion
@ -92,5 +95,8 @@ internal class BadgeToken : AbstractControlDesignToken
BadgeRibbonCornerDarkenAmount = 15; BadgeRibbonCornerDarkenAmount = 15;
BadgeRibbonTextPadding = new Thickness(_globalToken.PaddingXS, 0); BadgeRibbonTextPadding = new Thickness(_globalToken.PaddingXS, 0);
DotBadgeLabelMargin = new Thickness(_globalToken.MarginXS, 0, 0, 0); DotBadgeLabelMargin = new Thickness(_globalToken.MarginXS, 0, 0, 0);
CountBadgeTextPadding = new Thickness(_globalToken.PaddingXXS, 0);
CountBadgeCornerRadius = new CornerRadius(IndicatorHeight);
CountBadgeCornerRadiusSM = new CornerRadius(IndicatorHeightSM);
} }
} }

View File

@ -1,7 +1,4 @@
using AtomUI.Controls.Badge; using AtomUI.Data;
using AtomUI.Controls.MotionScene;
using AtomUI.Data;
using AtomUI.MotionScene;
using AtomUI.Theme.Palette; using AtomUI.Theme.Palette;
using AtomUI.Theme.Styling; using AtomUI.Theme.Styling;
using AtomUI.Utils; using AtomUI.Utils;
@ -48,7 +45,7 @@ public class CountBadge : Control
AvaloniaProperty.Register<CountBadge, CountBadgeSize>(nameof(Size)); AvaloniaProperty.Register<CountBadge, CountBadgeSize>(nameof(Size));
public static readonly StyledProperty<bool> BadgeIsVisibleProperty = public static readonly StyledProperty<bool> BadgeIsVisibleProperty =
AvaloniaProperty.Register<CountBadge, bool>(nameof(BadgeIsVisible)); AvaloniaProperty.Register<CountBadge, bool>(nameof(BadgeIsVisible), true);
public string? BadgeColor public string? BadgeColor
{ {
@ -117,7 +114,7 @@ public class CountBadge : Control
private CountBadgeAdorner? _badgeAdorner; private CountBadgeAdorner? _badgeAdorner;
private AdornerLayer? _adornerLayer; private AdornerLayer? _adornerLayer;
private bool _animating; private bool _isInitialized;
static CountBadge() static CountBadge()
{ {
@ -131,10 +128,17 @@ public class CountBadge : Control
public sealed override void ApplyTemplate() public sealed override void ApplyTemplate()
{ {
base.ApplyTemplate(); base.ApplyTemplate();
if (!_isInitialized)
{
if (DecoratedTarget is null) if (DecoratedTarget is null)
{ {
CreateBadgeAdorner(); CreateBadgeAdorner();
} }
SetupShowZero();
_isInitialized = true;
}
} }
private CountBadgeAdorner CreateBadgeAdorner() private CountBadgeAdorner CreateBadgeAdorner()
@ -154,123 +158,47 @@ public class CountBadge : Control
} }
private void PrepareAdorner() private void PrepareAdorner()
{
if (_adornerLayer is null && DecoratedTarget is not null)
{ {
var badgeAdorner = CreateBadgeAdorner(); var badgeAdorner = CreateBadgeAdorner();
if (DecoratedTarget is not null)
{
_adornerLayer = AdornerLayer.GetAdornerLayer(this); _adornerLayer = AdornerLayer.GetAdornerLayer(this);
// 这里需要抛出异常吗? // 这里需要抛出异常吗?
if (_adornerLayer == null) if (_adornerLayer == null)
{ {
return; return;
} }
badgeAdorner.ApplyToTarget(_adornerLayer, this);
AdornerLayer.SetAdornedElement(badgeAdorner, this);
AdornerLayer.SetIsClipEnabled(badgeAdorner, false);
_adornerLayer.Children.Add(badgeAdorner);
}
}
private void PrepareAdornerWithMotion()
{
PrepareAdorner();
if (VisualRoot is null || _animating)
{
return;
}
_animating = true;
var director = Director.Instance;
AbstractMotion motion;
var adorner = _badgeAdorner!;
if (DecoratedTarget is not null)
{
var countBadgeZoomBadgeIn = new CountBadgeZoomBadgeIn();
countBadgeZoomBadgeIn.ConfigureOpacity(MotionDuration);
countBadgeZoomBadgeIn.ConfigureRenderTransform(MotionDuration);
motion = countBadgeZoomBadgeIn;
adorner.AnimationRenderTransformOrigin = motion.MotionRenderTransformOrigin;
} }
else else
{ {
var countBadgeNoWrapperZoomBadgeIn = new CountBadgeNoWrapperZoomBadgeIn(); badgeAdorner.ApplyToTarget(null, this);
countBadgeNoWrapperZoomBadgeIn.ConfigureOpacity(MotionDuration); }
countBadgeNoWrapperZoomBadgeIn.ConfigureRenderTransform(MotionDuration);
motion = countBadgeNoWrapperZoomBadgeIn;
} }
var motionActor = new MotionActor(adorner, motion); private void HideAdorner(bool enableMotion)
motionActor.DispatchInSceneLayer = false;
motionActor.Completed += (sender, args) =>
{
adorner.AnimationRenderTransformOrigin = null;
_animating = false;
};
director?.Schedule(motionActor);
}
private void HideAdorner()
{ {
// 这里需要抛出异常吗? // 这里需要抛出异常吗?
if (_adornerLayer is null || _badgeAdorner is null) if (_badgeAdorner is null)
{ {
return; return;
} }
_badgeAdorner.DetachFromTarget(_adornerLayer, enableMotion);
_adornerLayer.Children.Remove(_badgeAdorner);
_adornerLayer = null;
}
private void HideAdornerWithMotion()
{
if (VisualRoot is null || _animating)
{
return;
}
_animating = true;
var director = Director.Instance;
AbstractMotion motion;
var adorner = _badgeAdorner!;
if (DecoratedTarget is not null)
{
var countBadgeZoomBadgeOut = new CountBadgeZoomBadgeOut();
countBadgeZoomBadgeOut.ConfigureOpacity(MotionDuration);
countBadgeZoomBadgeOut.ConfigureRenderTransform(MotionDuration);
motion = countBadgeZoomBadgeOut;
adorner.AnimationRenderTransformOrigin = motion.MotionRenderTransformOrigin;
}
else
{
var countBadgeNoWrapperZoomBadgeOut = new CountBadgeNoWrapperZoomBadgeOut();
countBadgeNoWrapperZoomBadgeOut.ConfigureOpacity(MotionDuration);
countBadgeNoWrapperZoomBadgeOut.ConfigureRenderTransform(MotionDuration);
motion = countBadgeNoWrapperZoomBadgeOut;
}
var motionActor = new MotionActor(adorner, motion);
motionActor.DispatchInSceneLayer = false;
motionActor.Completed += (sender, args) =>
{
HideAdorner();
adorner.AnimationRenderTransformOrigin = null;
_animating = false;
};
director?.Schedule(motionActor);
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
if (BadgeIsVisible)
{
PrepareAdorner(); PrepareAdorner();
} }
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTree(e); base.OnDetachedFromVisualTree(e);
HideAdorner(); HideAdorner(false);
} }
private void SetupTokenBindings() private void SetupTokenBindings()
@ -308,38 +236,16 @@ public class CountBadge : Control
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{ {
base.OnPropertyChanged(e); base.OnPropertyChanged(e);
if (e.Property == IsVisibleProperty) if (e.Property == BadgeIsVisibleProperty)
{ {
var badgeIsVisible = e.GetNewValue<bool>(); if (BadgeIsVisible)
if (badgeIsVisible)
{ {
if (_adornerLayer is not null) SetupShowZero();
{
return;
}
PrepareAdorner(); PrepareAdorner();
} }
else else
{ {
HideAdorner(); HideAdorner(true);
}
}
else if (e.Property == BadgeIsVisibleProperty)
{
var badgeIsVisible = e.GetNewValue<bool>();
if (badgeIsVisible)
{
if (_adornerLayer is not null)
{
return;
}
PrepareAdornerWithMotion();
}
else
{
HideAdornerWithMotion();
} }
} }
@ -356,19 +262,24 @@ public class CountBadge : Control
} }
} }
if (e.Property == CountProperty) if (e.Property == CountProperty ||
e.Property == ShowZeroProperty)
{ {
var newCount = e.GetNewValue<int>(); SetupShowZero();
if (newCount == 0 && !ShowZero) }
}
private void SetupShowZero()
{
if (Count == 0 && !ShowZero)
{ {
BadgeIsVisible = false; BadgeIsVisible = false;
} }
else if (newCount > 0) else if (Count > 0)
{ {
BadgeIsVisible = true; BadgeIsVisible = true;
} }
} }
}
private void SetupBadgeColor(string colorStr) private void SetupBadgeColor(string colorStr)
{ {

View File

@ -1,18 +1,16 @@
using System.Globalization; using AtomUI.Controls.Badge;
using AtomUI.Media; using AtomUI.MotionScene;
using AtomUI.Theme.Styling; using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia; using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Styling;
namespace AtomUI.Controls; namespace AtomUI.Controls;
internal class CountBadgeAdorner : Control internal class CountBadgeAdorner : TemplatedControl
{ {
#region #region
@ -20,17 +18,28 @@ internal class CountBadgeAdorner : Control
AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>( AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>(
nameof(BadgeColor)); nameof(BadgeColor));
public static readonly DirectProperty<CountBadgeAdorner, int> OverflowCountProperty = internal static readonly StyledProperty<Point> OffsetProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, int>( AvaloniaProperty.Register<CountBadgeAdorner, Point>(
nameof(OverflowCount), nameof(Offset));
o => o.OverflowCount,
(o, v) => o.OverflowCount = v);
public static readonly DirectProperty<CountBadgeAdorner, CountBadgeSize> SizeProperty = public static readonly StyledProperty<int> OverflowCountProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, CountBadgeSize>( AvaloniaProperty.Register<CountBadgeAdorner, int>(nameof(OverflowCount));
nameof(Size),
o => o.Size, public static readonly StyledProperty<CountBadgeSize> SizeProperty =
(o, v) => o.Size = v); AvaloniaProperty.Register<CountBadgeAdorner, CountBadgeSize>(
nameof(Size));
internal static readonly StyledProperty<int> CountProperty =
AvaloniaProperty.Register<CountBadgeAdorner, int>(
nameof(Count));
internal static readonly StyledProperty<IBrush?> BadgeShadowColorProperty =
AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>(
nameof(BadgeShadowColor));
internal static readonly StyledProperty<double> BadgeShadowSizeProperty =
AvaloniaProperty.Register<CountBadgeAdorner, double>(
nameof(BadgeShadowSize));
public IBrush? BadgeColor public IBrush? BadgeColor
{ {
@ -44,115 +53,63 @@ internal class CountBadgeAdorner : Control
set => SetValue(OffsetProperty, value); set => SetValue(OffsetProperty, value);
} }
private int _overflowCount;
public int OverflowCount public int OverflowCount
{ {
get => _overflowCount; get => GetValue(OverflowCountProperty);
set => SetAndRaise(OverflowCountProperty, ref _overflowCount, value); set => SetValue(OverflowCountProperty, value);
}
public CountBadgeSize Size
{
get => GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
public int Count
{
get => GetValue(CountProperty);
set => SetValue(CountProperty, value);
}
public IBrush? BadgeShadowColor
{
get => GetValue(BadgeShadowColorProperty);
set => SetValue(BadgeShadowColorProperty, value);
}
public double BadgeShadowSize
{
get => GetValue(BadgeShadowSizeProperty);
set => SetValue(BadgeShadowSizeProperty, value);
} }
#endregion #endregion
#region #region
internal static readonly StyledProperty<IBrush?> BadgeTextColorProperty = internal static readonly DirectProperty<CountBadgeAdorner, bool> IsAdornerModeProperty =
AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>( AvaloniaProperty.RegisterDirect<CountBadgeAdorner, bool>(
nameof(BadgeTextColor));
internal static readonly StyledProperty<double> TextFontSizeProperty =
AvaloniaProperty.Register<CountBadgeAdorner, double>(
nameof(TextFontSize));
internal static readonly StyledProperty<double> IndicatorHeightProperty =
AvaloniaProperty.Register<CountBadgeAdorner, double>(
nameof(IndicatorHeight));
internal static readonly StyledProperty<int> CountProperty =
AvaloniaProperty.Register<CountBadgeAdorner, int>(
nameof(Count));
internal static readonly StyledProperty<FontFamily> FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner<CountBadgeAdorner>();
internal static readonly StyledProperty<FontWeight> TextFontWeightProperty =
TextElement.FontWeightProperty.AddOwner<CountBadgeAdorner>();
internal static readonly StyledProperty<IBrush?> BadgeShadowColorProperty =
AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>(
nameof(BadgeShadowColor));
internal static readonly StyledProperty<double> BadgeShadowSizeProperty =
AvaloniaProperty.Register<CountBadgeAdorner, double>(
nameof(BadgeShadowSize));
internal static readonly StyledProperty<double> PaddingInlineProperty =
AvaloniaProperty.Register<CountBadgeAdorner, double>(
nameof(PaddingInline));
internal static readonly DirectProperty<DotBadgeAdorner, bool> IsAdornerModeProperty =
AvaloniaProperty.RegisterDirect<DotBadgeAdorner, bool>(
nameof(IsAdornerMode), nameof(IsAdornerMode),
o => o.IsAdornerMode, o => o.IsAdornerMode,
(o, v) => o.IsAdornerMode = v); (o, v) => o.IsAdornerMode = v);
internal static readonly StyledProperty<Point> OffsetProperty = internal static readonly DirectProperty<CountBadgeAdorner, BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<CountBadgeAdorner, Point>( AvaloniaProperty.RegisterDirect<CountBadgeAdorner, BoxShadows>(
nameof(Offset)); nameof(BoxShadow),
o => o.BoxShadow,
(o, v) => o.BoxShadow = v);
internal IBrush? BadgeTextColor internal static readonly DirectProperty<CountBadgeAdorner, string?> CountTextProperty =
{ AvaloniaProperty.RegisterDirect<CountBadgeAdorner, string?>(
get => GetValue(BadgeTextColorProperty); nameof(CountText),
set => SetValue(BadgeTextColorProperty, value); o => o.CountText,
} (o, v) => o.CountText = v);
internal double TextFontSize internal static readonly DirectProperty<CountBadgeAdorner, TimeSpan> MotionDurationProperty =
{ AvaloniaProperty.RegisterDirect<CountBadgeAdorner, TimeSpan>(
get => GetValue(TextFontSizeProperty); nameof(MotionDuration),
set => SetValue(TextFontSizeProperty, value); o => o.MotionDuration,
} (o, v) => o.MotionDuration = v);
internal double IndicatorHeight
{
get => GetValue(IndicatorHeightProperty);
set => SetValue(IndicatorHeightProperty, value);
}
internal int Count
{
get => GetValue(CountProperty);
set => SetValue(CountProperty, value);
}
internal FontFamily FontFamily
{
get => GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
internal FontWeight TextFontWeight
{
get => GetValue(TextFontWeightProperty);
set => SetValue(TextFontWeightProperty, value);
}
internal IBrush? BadgeShadowColor
{
get => GetValue(BadgeShadowColorProperty);
set => SetValue(BadgeShadowColorProperty, value);
}
internal double BadgeShadowSize
{
get => GetValue(BadgeShadowSizeProperty);
set => SetValue(BadgeShadowSizeProperty, value);
}
internal double PaddingInline
{
get => GetValue(PaddingInlineProperty);
set => SetValue(PaddingInlineProperty, value);
}
private bool _isAdornerMode; private bool _isAdornerMode;
@ -162,19 +119,33 @@ internal class CountBadgeAdorner : Control
set => SetAndRaise(IsAdornerModeProperty, ref _isAdornerMode, value); set => SetAndRaise(IsAdornerModeProperty, ref _isAdornerMode, value);
} }
private CountBadgeSize _size; private BoxShadows _boxShadow;
public BoxShadows BoxShadow
public CountBadgeSize Size
{ {
get => _size; get => _boxShadow;
set => SetAndRaise(SizeProperty, ref _size, value); set => SetAndRaise(BoxShadowProperty, ref _boxShadow, value);
}
private string? _countText;
public string? CountText
{
get => _countText;
set => SetAndRaise(CountTextProperty, ref _countText, value);
}
private TimeSpan _motionDuration;
public TimeSpan MotionDuration
{
get => _motionDuration;
set => SetAndRaise(MotionDurationProperty, ref _motionDuration, value);
} }
#endregion #endregion
// 不知道为什么这个值会被 AdornerLayer 重写 private Panel? _rootLayout;
// 非常不优美,但是能工作 private MotionActorControl? _indicatorMotionActor;
internal RelativePoint? AnimationRenderTransformOrigin; private CancellationTokenSource? _motionCancellationTokenSource;
private bool _needInitialHide;
static CountBadgeAdorner() static CountBadgeAdorner()
{ {
@ -185,66 +156,28 @@ internal class CountBadgeAdorner : Control
AffectsRender<CountBadgeAdorner>(BadgeColorProperty, OffsetProperty); AffectsRender<CountBadgeAdorner>(BadgeColorProperty, OffsetProperty);
} }
private bool _initialized; protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
private BoxShadows _boxShadows;
private Size _countTextSize;
private string? _countText;
private readonly List<FormattedText> _formattedTexts;
public CountBadgeAdorner()
{ {
_formattedTexts = new List<FormattedText>(); base.OnApplyTemplate(e);
TokenResourceBinder.CreateTokenBinding(this, BadgeShadowSizeProperty, BadgeTokenResourceKey.BadgeShadowSize);
TokenResourceBinder.CreateTokenBinding(this, BadgeShadowColorProperty, BadgeTokenResourceKey.BadgeShadowColor);
TokenResourceBinder.CreateTokenBinding(this, MotionDurationProperty, GlobalTokenResourceKey.MotionDurationMid);
TokenResourceBinder.CreateTokenBinding(this, BadgeColorProperty, BadgeTokenResourceKey.BadgeColor);
_indicatorMotionActor = e.NameScope.Get<MotionActorControl>(CountBadgeAdornerTheme.IndicatorMotionActorPart);
if (_needInitialHide)
{
_indicatorMotionActor.IsVisible = false;
_needInitialHide = false;
} }
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (Styles.Count == 0)
{
BuildStyles();
}
}
private void BuildStyles()
{
var commonStyle = new Style();
commonStyle.Add(TextFontWeightProperty, BadgeTokenResourceKey.TextFontWeight);
commonStyle.Add(BadgeColorProperty, BadgeTokenResourceKey.BadgeColor);
commonStyle.Add(BadgeShadowSizeProperty, BadgeTokenResourceKey.BadgeShadowSize);
commonStyle.Add(BadgeShadowColorProperty, BadgeTokenResourceKey.BadgeShadowColor);
commonStyle.Add(BadgeTextColorProperty, BadgeTokenResourceKey.BadgeTextColor);
commonStyle.Add(PaddingInlineProperty, GlobalTokenResourceKey.PaddingXS);
Styles.Add(commonStyle);
var defaultSizeStyle =
new Style(selector => selector.PropertyEquals(SizeProperty, CountBadgeSize.Default));
defaultSizeStyle.Add(TextFontSizeProperty, BadgeTokenResourceKey.TextFontSize);
defaultSizeStyle.Add(IndicatorHeightProperty, BadgeTokenResourceKey.IndicatorHeight);
Styles.Add(defaultSizeStyle);
var smallSizeStyle = new Style(selector => selector.PropertyEquals(SizeProperty, CountBadgeSize.Small));
smallSizeStyle.Add(TextFontSizeProperty, BadgeTokenResourceKey.TextFontSizeSM);
smallSizeStyle.Add(IndicatorHeightProperty, BadgeTokenResourceKey.IndicatorHeightSM);
Styles.Add(smallSizeStyle);
}
public override void ApplyTemplate()
{
if (!_initialized)
{
BuildBoxShadow(); BuildBoxShadow();
BuildCountText(); BuildCountText();
CalculateCountTextSize();
BuildFormattedTexts();
_initialized = true;
}
} }
private void BuildBoxShadow() private void BuildBoxShadow()
{ {
if (BadgeShadowColor is not null) if (BadgeShadowColor is not null)
{ {
_boxShadows = new BoxShadows(new BoxShadow BoxShadow = new BoxShadows(new BoxShadow
{ {
OffsetX = 0, OffsetX = 0,
OffsetY = 0, OffsetY = 0,
@ -269,143 +202,100 @@ internal class CountBadgeAdorner : Control
if (e.Property == CountProperty || e.Property == OverflowCountProperty) if (e.Property == CountProperty || e.Property == OverflowCountProperty)
{ {
BuildCountText(); BuildCountText();
CalculateCountTextSize(true);
BuildFormattedTexts(true);
} }
} }
} }
protected override Size MeasureOverride(Size availableSize)
{
if (IsAdornerMode)
{
return availableSize;
}
return GetBadgePillSize();
}
protected Size GetBadgePillSize()
{
var targetWidth = IndicatorHeight;
var targetHeight = IndicatorHeight;
if (_countText?.Length > 1)
{
targetWidth += PaddingInline;
if (Count > _overflowCount)
{
targetWidth += PaddingInline;
}
}
targetWidth = Math.Max(targetWidth, _countTextSize.Width);
targetHeight = Math.Max(targetHeight, _countTextSize.Height);
return new Size(targetWidth, targetHeight);
}
private void CalculateCountTextSize(bool force = false)
{
if (force || _countTextSize == default)
{
var fontSize = TextFontSize;
var typeface = new Typeface(FontFamily, FontStyle.Normal, TextFontWeight);
var textLayout = new TextLayout(_countText,
typeface,
null,
fontSize,
null,
lineHeight: IndicatorHeight);
_countTextSize = new Size(Math.Round(textLayout.Width), Math.Round(textLayout.Height));
}
}
private void BuildCountText() private void BuildCountText()
{ {
if (Count > _overflowCount) CountText = Count > OverflowCount ? $"{OverflowCount}+" : $"{Count}";
}
protected override Size ArrangeOverride(Size finalSize)
{ {
_countText = $"{_overflowCount}+"; var size = base.ArrangeOverride(finalSize);
if (IsAdornerMode && _indicatorMotionActor is not null)
{
var offsetX = Offset.X;
var offsetY = Offset.Y;
var indicatorSize = _indicatorMotionActor.DesiredSize;
offsetX += finalSize.Width - indicatorSize.Width / 2;
offsetY -= indicatorSize.Height / 2;
_indicatorMotionActor.Arrange(new Rect(new Point(offsetX, offsetY), indicatorSize));
}
return size;
}
private void ApplyShowMotion()
{
if (_indicatorMotionActor is not null)
{
_indicatorMotionActor.IsVisible = false;
var zoomBadgeInMotionConfig = BadgeMotionFactory.BuildBadgeZoomBadgeInMotion(MotionDuration, null,
FillMode.Forward);
MotionInvoker.Invoke(_indicatorMotionActor, zoomBadgeInMotionConfig, () =>
{
_indicatorMotionActor.IsVisible = true;
}, null, _motionCancellationTokenSource!.Token);
}
}
private void ApplyHideMotion(Action completedAction)
{
if (_indicatorMotionActor is not null)
{
var zoomBadgeOutMotionConfig = BadgeMotionFactory.BuildBadgeZoomBadgeOutMotion(MotionDuration, null,
FillMode.Forward);
_motionCancellationTokenSource?.Cancel();
_motionCancellationTokenSource = new CancellationTokenSource();
MotionInvoker.Invoke(_indicatorMotionActor, zoomBadgeOutMotionConfig, null, () =>
{
completedAction();
}, _motionCancellationTokenSource.Token);
} }
else else
{ {
_countText = $"{Count}"; _needInitialHide = true;
} }
} }
public override void Render(DrawingContext context) internal void ApplyToTarget(AdornerLayer? adornerLayer, Control adorned)
{ {
var offsetX = 0d; if (adornerLayer is not null)
var offsetY = 0d;
var badgeSize = GetBadgePillSize();
if (IsAdornerMode)
{ {
offsetX = DesiredSize.Width - badgeSize.Width / 2; adornerLayer.Children.Remove(this);
offsetY = -badgeSize.Height / 2;
offsetX += Offset.X; AdornerLayer.SetAdornedElement(this, adorned);
offsetY += Offset.Y; AdornerLayer.SetIsClipEnabled(this, false);
adornerLayer.Children.Add(this);
} }
var badgeRect = new Rect(new Point(offsetX, offsetY), badgeSize); _motionCancellationTokenSource?.Cancel();
_motionCancellationTokenSource = new CancellationTokenSource();
if (RenderTransform is not null) ApplyShowMotion();
}
internal void DetachFromTarget(AdornerLayer? adornerLayer, bool enableMotion = true)
{ {
Point origin;
if (AnimationRenderTransformOrigin.HasValue) if (enableMotion)
{ {
origin = AnimationRenderTransformOrigin.Value.ToPixels(badgeRect.Size); ApplyHideMotion(() =>
{
if (adornerLayer is not null)
{
adornerLayer.Children.Remove(this);
}
});
} }
else else
{ {
origin = RenderTransformOrigin.ToPixels(badgeRect.Size); if (adornerLayer is not null)
}
var offset = Matrix.CreateTranslation(new Point(origin.X + offsetX, origin.Y + offsetY));
var renderTransform = -offset * RenderTransform.Value * offset;
context.PushTransform(renderTransform);
}
context.DrawPilledRect(BadgeColor, null, badgeRect, Orientation.Horizontal, _boxShadows);
// 计算合适的文字 x 坐标
var textOffsetX = offsetX + (badgeSize.Width - _countTextSize.Width) / 2;
var textOffsetY = offsetY + (badgeSize.Height - _countTextSize.Height) / 2;
foreach (var formattedText in _formattedTexts)
{ {
context.DrawText(formattedText, new Point(textOffsetX, textOffsetY)); adornerLayer.Children.Remove(this);
textOffsetX += formattedText.Width;
} }
} }
private void BuildFormattedTexts(bool force = false)
{
if (_formattedTexts.Count == 0 || force)
{
_formattedTexts.Clear();
if (_countText is not null)
{
if (Count > _overflowCount)
{
// 生成一个即可
_formattedTexts.Add(BuildFormattedText(_countText));
}
else
{
// 没有数字都生成一个
foreach (var c in _countText)
{
_formattedTexts.Add(BuildFormattedText(c.ToString()));
}
}
}
}
}
private FormattedText BuildFormattedText(string text)
{
var typeface = new Typeface(FontFamily, FontStyle.Normal, TextFontWeight);
var formattedText = new FormattedText(text, CultureInfo.CurrentUICulture, GetFlowDirection(this),
typeface, 1, BadgeTextColor);
formattedText.SetFontSize(TextFontSize);
formattedText.TextAlignment = TextAlignment.Left;
formattedText.LineHeight = IndicatorHeight;
return formattedText;
} }
} }

View File

@ -1,13 +1,114 @@
using AtomUI.Theme; using AtomUI.MotionScene;
using AtomUI.Theme;
using AtomUI.Theme.Styling; using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls.Badge; namespace AtomUI.Controls.Badge;
[ControlThemeProvider] [ControlThemeProvider]
internal class CountBadgeAdornerTheme : BaseControlTheme internal class CountBadgeAdornerTheme : BaseControlTheme
{ {
internal const string IndicatorMotionActorPart = "PART_IndicatorMotionActor";
internal const string RootLayoutPart = "PART_RootLayout";
internal const string BadgeIndicatorPart = "PART_BadgeIndicator";
internal const string BadgeTextPart = "PART_BadgeText";
public CountBadgeAdornerTheme() public CountBadgeAdornerTheme()
: base(typeof(CountBadgeAdorner)) : base(typeof(CountBadgeAdorner))
{ {
} }
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<CountBadgeAdorner>((adorner, scope) =>
{
var indicatorMotionActor = new MotionActorControl()
{
Name = IndicatorMotionActorPart,
ClipToBounds = false,
UseRenderTransform = true
};
var layout = new Panel()
{
Name = RootLayoutPart,
};
indicatorMotionActor.Child = layout;
indicatorMotionActor.RegisterInNameScope(scope);
layout.RegisterInNameScope(scope);
BuildBadgeIndicator(adorner, layout, scope);
BuildBadgeText(layout, scope);
return indicatorMotionActor;
});
}
private void BuildBadgeIndicator(CountBadgeAdorner adorner, Panel layout, INameScope scope)
{
var indicator = new Border()
{
Name = BadgeIndicatorPart
};
CreateTemplateParentBinding(indicator, Border.BoxShadowProperty, CountBadgeAdorner.BoxShadowProperty);
CreateTemplateParentBinding(indicator, Border.BackgroundProperty, CountBadgeAdorner.BadgeColorProperty);
layout.Children.Add(indicator);
indicator.RegisterInNameScope(scope);
}
private void BuildBadgeText(Panel layout, INameScope scope)
{
var badgeText = new TextBlock()
{
Name = BadgeTextPart,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
CreateTemplateParentBinding(badgeText, TextBlock.TextProperty, CountBadgeAdorner.CountTextProperty);
layout.Children.Add(badgeText);
badgeText.RegisterInNameScope(scope);
}
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(CountBadgeAdorner.ClipToBoundsProperty, false);
var inAdornerStyle = new Style(selector => selector.Nesting().PropertyEquals(CountBadgeAdorner.IsAdornerModeProperty, true));
var layoutStyle = new Style(selector => selector.Nesting().Template().Name(RootLayoutPart));
layoutStyle.Add(Panel.HorizontalAlignmentProperty, HorizontalAlignment.Right);
layoutStyle.Add(Panel.VerticalAlignmentProperty, VerticalAlignment.Top);
inAdornerStyle.Add(layoutStyle);
commonStyle.Add(inAdornerStyle);
{
var indicatorStyle = new Style(selector => selector.Nesting().Template().Name(BadgeIndicatorPart));
indicatorStyle.Add(Border.HeightProperty, BadgeTokenResourceKey.IndicatorHeight);
indicatorStyle.Add(Border.MinWidthProperty, BadgeTokenResourceKey.IndicatorHeight);
indicatorStyle.Add(Border.CornerRadiusProperty, BadgeTokenResourceKey.CountBadgeCornerRadius);
commonStyle.Add(indicatorStyle);
var badgeTextStyle = new Style(selector => selector.Nesting().Template().Name(BadgeTextPart));
badgeTextStyle.Add(TextBlock.ForegroundProperty, BadgeTokenResourceKey.BadgeTextColor);
badgeTextStyle.Add(TextBlock.FontSizeProperty, BadgeTokenResourceKey.TextFontSize);
badgeTextStyle.Add(TextBlock.PaddingProperty, BadgeTokenResourceKey.CountBadgeTextPadding);
commonStyle.Add(badgeTextStyle);
}
var smallSizeStyle = new Style(selector => selector.Nesting().PropertyEquals(CountBadgeAdorner.SizeProperty, CountBadgeSize.Small));
{
var indicatorStyle = new Style(selector => selector.Nesting().Template().Name(BadgeIndicatorPart));
indicatorStyle.Add(Border.HeightProperty, BadgeTokenResourceKey.IndicatorHeightSM);
indicatorStyle.Add(Border.MinWidthProperty, BadgeTokenResourceKey.IndicatorHeightSM);
indicatorStyle.Add(Border.CornerRadiusProperty, BadgeTokenResourceKey.CountBadgeCornerRadiusSM);
smallSizeStyle.Add(indicatorStyle);
var badgeTextStyle = new Style(selector => selector.Nesting().Template().Name(BadgeTextPart));
badgeTextStyle.Add(TextBlock.FontSizeProperty, BadgeTokenResourceKey.TextFontSizeSM);
smallSizeStyle.Add(badgeTextStyle);
}
commonStyle.Add(smallSizeStyle);
Add(commonStyle);
}
} }

View File

@ -1,168 +0,0 @@
using AtomUI.MotionScene;
using Avalonia;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
namespace AtomUI.Controls.Badge;
internal class CountBadgeZoomBadgeIn : AbstractMotion
{
public MotionConfig? OpacityConfig => GetMotionConfig(MotionOpacityProperty);
public MotionConfig? RenderTransformConfig => GetMotionConfig(MotionRenderTransformProperty);
public CountBadgeZoomBadgeIn()
{
MotionRenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
}
public void ConfigureOpacity(TimeSpan duration, Easing? easing = null)
{
easing ??= new ExponentialEaseOut();
var config = new MotionConfig(MotionOpacityProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 0d,
EndValue = 1d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
public void ConfigureRenderTransform(TimeSpan duration, Easing? easing = null)
{
easing ??= new BackEaseOut();
var config = new MotionConfig(MotionRenderTransformProperty)
{
TransitionKind = TransitionKind.TransformOperations,
StartValue = BuildScaleTransform(0),
EndValue = BuildScaleTransform(1),
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
internal override void NotifyConfigMotionTarget(Control motionTarget)
{
base.NotifyConfigMotionTarget(motionTarget);
motionTarget.RenderTransformOrigin = MotionRenderTransformOrigin;
}
}
internal class CountBadgeZoomBadgeOut : AbstractMotion
{
public MotionConfig? OpacityConfig => GetMotionConfig(MotionOpacityProperty);
public MotionConfig? RenderTransformConfig => GetMotionConfig(MotionRenderTransformProperty);
public CountBadgeZoomBadgeOut()
{
MotionRenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
}
public void ConfigureOpacity(TimeSpan duration, Easing? easing = null)
{
easing ??= new ExponentialEaseIn();
var config = new MotionConfig(MotionOpacityProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 1d,
EndValue = 0d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
public void ConfigureRenderTransform(TimeSpan duration, Easing? easing = null)
{
easing ??= new ExponentialEaseIn();
var config = new MotionConfig(MotionRenderTransformProperty)
{
TransitionKind = TransitionKind.TransformOperations,
StartValue = BuildScaleTransform(1),
EndValue = BuildScaleTransform(0),
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
internal override void NotifyConfigMotionTarget(Control motionTarget)
{
base.NotifyConfigMotionTarget(motionTarget);
motionTarget.RenderTransformOrigin = MotionRenderTransformOrigin;
}
}
internal class CountBadgeNoWrapperZoomBadgeIn : AbstractMotion
{
public MotionConfig? OpacityConfig => GetMotionConfig(MotionOpacityProperty);
public MotionConfig? RenderTransformConfig => GetMotionConfig(MotionRenderTransformProperty);
public void ConfigureOpacity(TimeSpan duration, Easing? easing = null)
{
easing ??= new QuarticEaseOut();
var config = new MotionConfig(MotionOpacityProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 0d,
EndValue = 1d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
public void ConfigureRenderTransform(TimeSpan duration, Easing? easing = null)
{
easing ??= new QuarticEaseOut();
var config = new MotionConfig(MotionRenderTransformProperty)
{
TransitionKind = TransitionKind.TransformOperations,
StartValue = BuildScaleTransform(0),
EndValue = BuildScaleTransform(1),
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
}
internal class CountBadgeNoWrapperZoomBadgeOut : AbstractMotion
{
public MotionConfig? OpacityConfig => GetMotionConfig(MotionOpacityProperty);
public MotionConfig? RenderTransformConfig => GetMotionConfig(MotionRenderTransformProperty);
public void ConfigureOpacity(TimeSpan duration, Easing? easing = null)
{
easing ??= new CircularEaseIn();
var config = new MotionConfig(MotionOpacityProperty)
{
TransitionKind = TransitionKind.Double,
StartValue = 1d,
EndValue = 0d,
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
public void ConfigureRenderTransform(TimeSpan duration, Easing? easing = null)
{
easing ??= new CircularEaseIn();
var config = new MotionConfig(MotionRenderTransformProperty)
{
TransitionKind = TransitionKind.TransformOperations,
StartValue = BuildScaleTransform(1),
EndValue = BuildScaleTransform(0),
MotionDuration = duration,
MotionEasing = easing
};
AddMotionConfig(config);
}
}

View File

@ -125,20 +125,20 @@ public class DotBadge : Control
} }
} }
private void HideAdorner() private void HideAdorner(bool enableMotion)
{ {
// 这里需要抛出异常吗? // 这里需要抛出异常吗?
if ( _dotBadgeAdorner is null) if ( _dotBadgeAdorner is null)
{ {
return; return;
} }
_dotBadgeAdorner.DetachFromTarget(_adornerLayer); _dotBadgeAdorner.DetachFromTarget(_adornerLayer, enableMotion);
} }
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTree(e); base.OnDetachedFromVisualTree(e);
HideAdorner(); HideAdorner(false);
} }
private void SetupTokenBindings() private void SetupTokenBindings()
@ -186,32 +186,17 @@ public class DotBadge : Control
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{ {
base.OnPropertyChanged(e); base.OnPropertyChanged(e);
if (e.Property == IsVisibleProperty) if (e.Property == IsVisibleProperty ||
e.Property == BadgeIsVisibleProperty)
{ {
var badgeIsVisible = e.GetNewValue<bool>(); var badgeIsVisible = e.GetNewValue<bool>();
if (badgeIsVisible) if (badgeIsVisible)
{ {
if (_adornerLayer is not null)
{
return;
}
PrepareAdorner(); PrepareAdorner();
} }
else else
{ {
HideAdorner(); HideAdorner(true);
}
}
else if (e.Property == BadgeIsVisibleProperty)
{
if (BadgeIsVisible)
{
PrepareAdorner();
}
else
{
HideAdorner();
} }
} }

View File

@ -187,8 +187,8 @@ internal class DotBadgeAdorner : TemplatedControl
var offsetY = Offset.Y; var offsetY = Offset.Y;
var dotSize = _indicatorMotionActor.Bounds.Size; var dotSize = _indicatorMotionActor.Bounds.Size;
offsetX += dotSize.Width / 3; offsetX += dotSize.Width / 3;
offsetY += dotSize.Height / 3; offsetY -= dotSize.Height / 3;
_indicatorMotionActor.Arrange(new Rect(new Point(offsetX, -offsetY), dotSize)); _indicatorMotionActor.Arrange(new Rect(new Point(offsetX, offsetY), dotSize));
} }
return size; return size;
} }
@ -212,12 +212,20 @@ internal class DotBadgeAdorner : TemplatedControl
ApplyShowMotion(); ApplyShowMotion();
} }
internal void DetachFromTarget(AdornerLayer? adornerLayer) internal void DetachFromTarget(AdornerLayer? adornerLayer, bool enableMotion = true)
{ {
if (adornerLayer is null) if (adornerLayer is null)
{ {
return; return;
} }
if (enableMotion)
{
ApplyHideMotion(() => adornerLayer.Children.Remove(this)); ApplyHideMotion(() => adornerLayer.Children.Remove(this));
} }
else
{
adornerLayer.Children.Remove(this);
}
}
} }

View File

@ -23,7 +23,7 @@ internal class DotBadgeAdornerTheme : BaseControlTheme
{ {
} }
protected override IControlTemplate? BuildControlTemplate() protected override IControlTemplate BuildControlTemplate()
{ {
return new FuncControlTemplate<DotBadgeAdorner>((adorner, scope) => return new FuncControlTemplate<DotBadgeAdorner>((adorner, scope) =>
{ {

View File

@ -65,6 +65,9 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey BadgeRibbonCornerDarkenAmount = new TokenResourceKey("Badge.BadgeRibbonCornerDarkenAmount", "AtomUI.Token"); public static readonly TokenResourceKey BadgeRibbonCornerDarkenAmount = new TokenResourceKey("Badge.BadgeRibbonCornerDarkenAmount", "AtomUI.Token");
public static readonly TokenResourceKey BadgeRibbonTextPadding = new TokenResourceKey("Badge.BadgeRibbonTextPadding", "AtomUI.Token"); public static readonly TokenResourceKey BadgeRibbonTextPadding = new TokenResourceKey("Badge.BadgeRibbonTextPadding", "AtomUI.Token");
public static readonly TokenResourceKey DotBadgeLabelMargin = new TokenResourceKey("Badge.DotBadgeLabelMargin", "AtomUI.Token"); public static readonly TokenResourceKey DotBadgeLabelMargin = new TokenResourceKey("Badge.DotBadgeLabelMargin", "AtomUI.Token");
public static readonly TokenResourceKey CountBadgeTextPadding = new TokenResourceKey("Badge.CountBadgeTextPadding", "AtomUI.Token");
public static readonly TokenResourceKey CountBadgeCornerRadius = new TokenResourceKey("Badge.CountBadgeCornerRadius", "AtomUI.Token");
public static readonly TokenResourceKey CountBadgeCornerRadiusSM = new TokenResourceKey("Badge.CountBadgeCornerRadiusSM", "AtomUI.Token");
} }
public static class ButtonSpinnerTokenResourceKey public static class ButtonSpinnerTokenResourceKey