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 Thickness BadgeRibbonTextPadding { get; set; }
public Thickness DotBadgeLabelMargin { get; set; }
public Thickness CountBadgeTextPadding { get; set; }
public CornerRadius CountBadgeCornerRadius { get; set; }
public CornerRadius CountBadgeCornerRadiusSM { get; set; }
#endregion
@ -92,5 +95,8 @@ internal class BadgeToken : AbstractControlDesignToken
BadgeRibbonCornerDarkenAmount = 15;
BadgeRibbonTextPadding = new Thickness(_globalToken.PaddingXS, 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.Controls.MotionScene;
using AtomUI.Data;
using AtomUI.MotionScene;
using AtomUI.Data;
using AtomUI.Theme.Palette;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
@ -48,7 +45,7 @@ public class CountBadge : Control
AvaloniaProperty.Register<CountBadge, CountBadgeSize>(nameof(Size));
public static readonly StyledProperty<bool> BadgeIsVisibleProperty =
AvaloniaProperty.Register<CountBadge, bool>(nameof(BadgeIsVisible));
AvaloniaProperty.Register<CountBadge, bool>(nameof(BadgeIsVisible), true);
public string? BadgeColor
{
@ -117,7 +114,7 @@ public class CountBadge : Control
private CountBadgeAdorner? _badgeAdorner;
private AdornerLayer? _adornerLayer;
private bool _animating;
private bool _isInitialized;
static CountBadge()
{
@ -131,9 +128,16 @@ public class CountBadge : Control
public sealed override void ApplyTemplate()
{
base.ApplyTemplate();
if (DecoratedTarget is null)
if (!_isInitialized)
{
CreateBadgeAdorner();
if (DecoratedTarget is null)
{
CreateBadgeAdorner();
}
SetupShowZero();
_isInitialized = true;
}
}
@ -155,122 +159,46 @@ public class CountBadge : Control
private void PrepareAdorner()
{
if (_adornerLayer is null && DecoratedTarget is not null)
var badgeAdorner = CreateBadgeAdorner();
if (DecoratedTarget is not null)
{
var badgeAdorner = CreateBadgeAdorner();
_adornerLayer = AdornerLayer.GetAdornerLayer(this);
// 这里需要抛出异常吗?
if (_adornerLayer == null)
{
return;
}
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;
badgeAdorner.ApplyToTarget(_adornerLayer, this);
}
else
{
var countBadgeNoWrapperZoomBadgeIn = new CountBadgeNoWrapperZoomBadgeIn();
countBadgeNoWrapperZoomBadgeIn.ConfigureOpacity(MotionDuration);
countBadgeNoWrapperZoomBadgeIn.ConfigureRenderTransform(MotionDuration);
motion = countBadgeNoWrapperZoomBadgeIn;
badgeAdorner.ApplyToTarget(null, this);
}
var motionActor = new MotionActor(adorner, motion);
motionActor.DispatchInSceneLayer = false;
motionActor.Completed += (sender, args) =>
{
adorner.AnimationRenderTransformOrigin = null;
_animating = false;
};
director?.Schedule(motionActor);
}
private void HideAdorner()
private void HideAdorner(bool enableMotion)
{
// 这里需要抛出异常吗?
if (_adornerLayer is null || _badgeAdorner is null)
if (_badgeAdorner is null)
{
return;
}
_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);
_badgeAdorner.DetachFromTarget(_adornerLayer, enableMotion);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
PrepareAdorner();
if (BadgeIsVisible)
{
PrepareAdorner();
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
HideAdorner();
HideAdorner(false);
}
private void SetupTokenBindings()
@ -308,38 +236,16 @@ public class CountBadge : Control
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs 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)
{
return;
}
SetupShowZero();
PrepareAdorner();
}
else
{
HideAdorner();
}
}
else if (e.Property == BadgeIsVisibleProperty)
{
var badgeIsVisible = e.GetNewValue<bool>();
if (badgeIsVisible)
{
if (_adornerLayer is not null)
{
return;
}
PrepareAdornerWithMotion();
}
else
{
HideAdornerWithMotion();
HideAdorner(true);
}
}
@ -356,17 +262,22 @@ public class CountBadge : Control
}
}
if (e.Property == CountProperty)
if (e.Property == CountProperty ||
e.Property == ShowZeroProperty)
{
var newCount = e.GetNewValue<int>();
if (newCount == 0 && !ShowZero)
{
BadgeIsVisible = false;
}
else if (newCount > 0)
{
BadgeIsVisible = true;
}
SetupShowZero();
}
}
private void SetupShowZero()
{
if (Count == 0 && !ShowZero)
{
BadgeIsVisible = false;
}
else if (Count > 0)
{
BadgeIsVisible = true;
}
}

View File

@ -1,18 +1,16 @@
using System.Globalization;
using AtomUI.Media;
using AtomUI.Controls.Badge;
using AtomUI.MotionScene;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Styling;
namespace AtomUI.Controls;
internal class CountBadgeAdorner : Control
internal class CountBadgeAdorner : TemplatedControl
{
#region
@ -20,17 +18,28 @@ internal class CountBadgeAdorner : Control
AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>(
nameof(BadgeColor));
public static readonly DirectProperty<CountBadgeAdorner, int> OverflowCountProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, int>(
nameof(OverflowCount),
o => o.OverflowCount,
(o, v) => o.OverflowCount = v);
internal static readonly StyledProperty<Point> OffsetProperty =
AvaloniaProperty.Register<CountBadgeAdorner, Point>(
nameof(Offset));
public static readonly DirectProperty<CountBadgeAdorner, CountBadgeSize> SizeProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, CountBadgeSize>(
nameof(Size),
o => o.Size,
(o, v) => o.Size = v);
public static readonly StyledProperty<int> OverflowCountProperty =
AvaloniaProperty.Register<CountBadgeAdorner, int>(nameof(OverflowCount));
public static readonly StyledProperty<CountBadgeSize> SizeProperty =
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
{
@ -44,115 +53,63 @@ internal class CountBadgeAdorner : Control
set => SetValue(OffsetProperty, value);
}
private int _overflowCount;
public int OverflowCount
{
get => _overflowCount;
set => SetAndRaise(OverflowCountProperty, ref _overflowCount, value);
get => GetValue(OverflowCountProperty);
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
#region
internal static readonly StyledProperty<IBrush?> BadgeTextColorProperty =
AvaloniaProperty.Register<CountBadgeAdorner, IBrush?>(
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>(
internal static readonly DirectProperty<CountBadgeAdorner, bool> IsAdornerModeProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, bool>(
nameof(IsAdornerMode),
o => o.IsAdornerMode,
(o, v) => o.IsAdornerMode = v);
internal static readonly StyledProperty<Point> OffsetProperty =
AvaloniaProperty.Register<CountBadgeAdorner, Point>(
nameof(Offset));
internal static readonly DirectProperty<CountBadgeAdorner, BoxShadows> BoxShadowProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, BoxShadows>(
nameof(BoxShadow),
o => o.BoxShadow,
(o, v) => o.BoxShadow = v);
internal IBrush? BadgeTextColor
{
get => GetValue(BadgeTextColorProperty);
set => SetValue(BadgeTextColorProperty, value);
}
internal static readonly DirectProperty<CountBadgeAdorner, string?> CountTextProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, string?>(
nameof(CountText),
o => o.CountText,
(o, v) => o.CountText = v);
internal double TextFontSize
{
get => GetValue(TextFontSizeProperty);
set => SetValue(TextFontSizeProperty, value);
}
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);
}
internal static readonly DirectProperty<CountBadgeAdorner, TimeSpan> MotionDurationProperty =
AvaloniaProperty.RegisterDirect<CountBadgeAdorner, TimeSpan>(
nameof(MotionDuration),
o => o.MotionDuration,
(o, v) => o.MotionDuration = v);
private bool _isAdornerMode;
@ -162,19 +119,33 @@ internal class CountBadgeAdorner : Control
set => SetAndRaise(IsAdornerModeProperty, ref _isAdornerMode, value);
}
private CountBadgeSize _size;
public CountBadgeSize Size
private BoxShadows _boxShadow;
public BoxShadows BoxShadow
{
get => _size;
set => SetAndRaise(SizeProperty, ref _size, value);
get => _boxShadow;
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
// 不知道为什么这个值会被 AdornerLayer 重写
// 非常不优美,但是能工作
internal RelativePoint? AnimationRenderTransformOrigin;
private Panel? _rootLayout;
private MotionActorControl? _indicatorMotionActor;
private CancellationTokenSource? _motionCancellationTokenSource;
private bool _needInitialHide;
static CountBadgeAdorner()
{
@ -185,66 +156,28 @@ internal class CountBadgeAdorner : Control
AffectsRender<CountBadgeAdorner>(BadgeColorProperty, OffsetProperty);
}
private bool _initialized;
private BoxShadows _boxShadows;
private Size _countTextSize;
private string? _countText;
private readonly List<FormattedText> _formattedTexts;
public CountBadgeAdorner()
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_formattedTexts = new List<FormattedText>();
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (Styles.Count == 0)
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)
{
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();
BuildCountText();
CalculateCountTextSize();
BuildFormattedTexts();
_initialized = true;
_indicatorMotionActor.IsVisible = false;
_needInitialHide = false;
}
BuildBoxShadow();
BuildCountText();
}
private void BuildBoxShadow()
{
if (BadgeShadowColor is not null)
{
_boxShadows = new BoxShadows(new BoxShadow
BoxShadow = new BoxShadows(new BoxShadow
{
OffsetX = 0,
OffsetY = 0,
@ -269,143 +202,100 @@ internal class CountBadgeAdorner : Control
if (e.Property == CountProperty || e.Property == OverflowCountProperty)
{
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()
{
if (Count > _overflowCount)
CountText = Count > OverflowCount ? $"{OverflowCount}+" : $"{Count}";
}
protected override Size ArrangeOverride(Size finalSize)
{
var size = base.ArrangeOverride(finalSize);
if (IsAdornerMode && _indicatorMotionActor is not null)
{
_countText = $"{_overflowCount}+";
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
{
_countText = $"{Count}";
_needInitialHide = true;
}
}
public override void Render(DrawingContext context)
internal void ApplyToTarget(AdornerLayer? adornerLayer, Control adorned)
{
var offsetX = 0d;
var offsetY = 0d;
var badgeSize = GetBadgePillSize();
if (IsAdornerMode)
if (adornerLayer is not null)
{
offsetX = DesiredSize.Width - badgeSize.Width / 2;
offsetY = -badgeSize.Height / 2;
offsetX += Offset.X;
offsetY += Offset.Y;
adornerLayer.Children.Remove(this);
AdornerLayer.SetAdornedElement(this, adorned);
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)
{
Point origin;
if (AnimationRenderTransformOrigin.HasValue)
{
origin = AnimationRenderTransformOrigin.Value.ToPixels(badgeRect.Size);
}
else
{
origin = RenderTransformOrigin.ToPixels(badgeRect.Size);
}
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));
textOffsetX += formattedText.Width;
}
ApplyShowMotion();
}
private void BuildFormattedTexts(bool force = false)
internal void DetachFromTarget(AdornerLayer? adornerLayer, bool enableMotion = true)
{
if (_formattedTexts.Count == 0 || force)
if (enableMotion)
{
_formattedTexts.Clear();
if (_countText is not null)
ApplyHideMotion(() =>
{
if (Count > _overflowCount)
if (adornerLayer is not null)
{
// 生成一个即可
_formattedTexts.Add(BuildFormattedText(_countText));
}
else
{
// 没有数字都生成一个
foreach (var c in _countText)
{
_formattedTexts.Add(BuildFormattedText(c.ToString()));
}
adornerLayer.Children.Remove(this);
}
});
}
else
{
if (adornerLayer is not null)
{
adornerLayer.Children.Remove(this);
}
}
}
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 Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls.Badge;
[ControlThemeProvider]
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()
: 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)
{
return;
}
_dotBadgeAdorner.DetachFromTarget(_adornerLayer);
_dotBadgeAdorner.DetachFromTarget(_adornerLayer, enableMotion);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
HideAdorner();
HideAdorner(false);
}
private void SetupTokenBindings()
@ -186,32 +186,17 @@ public class DotBadge : Control
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == IsVisibleProperty)
if (e.Property == IsVisibleProperty ||
e.Property == BadgeIsVisibleProperty)
{
var badgeIsVisible = e.GetNewValue<bool>();
if (badgeIsVisible)
{
if (_adornerLayer is not null)
{
return;
}
PrepareAdorner();
}
else
{
HideAdorner();
}
}
else if (e.Property == BadgeIsVisibleProperty)
{
if (BadgeIsVisible)
{
PrepareAdorner();
}
else
{
HideAdorner();
HideAdorner(true);
}
}

View File

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

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 BadgeRibbonTextPadding = new TokenResourceKey("Badge.BadgeRibbonTextPadding", "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