完成 Notification 关闭功能

This commit is contained in:
polarboy 2024-08-29 23:58:21 +08:00
parent 81c0b7dada
commit 3b8c81678d
11 changed files with 355 additions and 93 deletions

View File

@ -17,15 +17,20 @@ public partial class NotificationShowCase : UserControl
{
base.OnAttachedToVisualTree(e);
var topLevel = TopLevel.GetTopLevel(this);
_windowNotificationManager = new WindowNotificationManager(topLevel) { MaxItems = 5 };
_windowNotificationManager = new WindowNotificationManager(topLevel)
{
MaxItems = 5,
IsPauseOnHover = true
};
}
private void ShowSimpleNotification(object? sender, RoutedEventArgs e)
{
_windowNotificationManager?.Show(new Notification()
{
ShowProgress = true,
Title = "Notification Title",
Content = "This is the content of the notification. This is the content of the notification. This is the content of the notification. This is the content of the notification. This is the content of the notification. This is the content of the notification."
Content = "This is the content of the notification. This is the content of the notification. This is the content of the notification. This is the content of the notification."
});
}
}

View File

@ -317,7 +317,9 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey NotificationMarginEdge = new TokenResourceKey("Notification.NotificationMarginEdge", "AtomUI.Token");
public static readonly TokenResourceKey NotificationProgressBg = new TokenResourceKey("Notification.NotificationProgressBg", "AtomUI.Token");
public static readonly TokenResourceKey NotificationProgressHeight = new TokenResourceKey("Notification.NotificationProgressHeight", "AtomUI.Token");
public static readonly TokenResourceKey NotificationProgressMargin = new TokenResourceKey("Notification.NotificationProgressMargin", "AtomUI.Token");
public static readonly TokenResourceKey NotificationWidth = new TokenResourceKey("Notification.NotificationWidth", "AtomUI.Token");
public static readonly TokenResourceKey NotificationContentMargin = new TokenResourceKey("Notification.NotificationContentMargin", "AtomUI.Token");
public static readonly TokenResourceKey HeaderMargin = new TokenResourceKey("Notification.HeaderMargin", "AtomUI.Token");
}

View File

@ -22,6 +22,11 @@ public interface INotification
/// If the value is <see cref="TimeSpan.Zero"/> then the notification will remain open until the user closes it.
/// </summary>
TimeSpan Expiration { get; }
/// <summary>
/// 显示一个进度条
/// </summary>
bool ShowProgress { get; }
/// <summary>
/// Gets an Action to be run when the notification is clicked.

View File

@ -7,6 +7,7 @@ public class Notification : INotification, INotifyPropertyChanged
{
private string? _title;
private object? _content;
private bool _showProgress;
/// <summary>
/// Initializes a new instance of the <see cref="Notification"/> class.
@ -22,6 +23,7 @@ public class Notification : INotification, INotifyPropertyChanged
object? content,
NotificationType type = NotificationType.Information,
TimeSpan? expiration = null,
bool showProgress = false,
Action? onClick = null,
Action? onClose = null)
{
@ -29,6 +31,7 @@ public class Notification : INotification, INotifyPropertyChanged
_content = content;
Type = type;
Expiration = expiration.HasValue ? expiration.Value : TimeSpan.FromSeconds(5);
ShowProgress = showProgress;
OnClick = onClick;
OnClose = onClose;
}
@ -63,6 +66,18 @@ public class Notification : INotification, INotifyPropertyChanged
}
}
}
public bool ShowProgress
{
get => _showProgress;
set
{
if (_showProgress != value) {
_showProgress = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc/>
public NotificationType Type { get; set; }

View File

@ -6,9 +6,8 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Threading;
namespace AtomUI.Controls;
@ -34,6 +33,9 @@ public class NotificationCard : TemplatedControl
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<NotificationCard, bool>(nameof(IsClosed));
public static readonly StyledProperty<bool> IsShowProgressProperty =
AvaloniaProperty.Register<NotificationCard, bool>(nameof(IsShowProgress), false);
/// <summary>
/// Defines the <see cref="NotificationType" /> property
/// </summary>
@ -46,12 +48,6 @@ public class NotificationCard : TemplatedControl
public static readonly RoutedEvent<RoutedEventArgs> NotificationClosedEvent =
RoutedEvent.Register<NotificationCard, RoutedEventArgs>(nameof(NotificationClosed), RoutingStrategies.Bubble);
/// <summary>
/// Defines the CloseOnClick property.
/// </summary>
public static readonly AttachedProperty<bool> CloseOnClickProperty =
AvaloniaProperty.RegisterAttached<NotificationCard, Button, bool>("CloseOnClick", defaultValue: false);
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<NotificationCard, string?>(nameof(Title));
@ -82,6 +78,12 @@ public class NotificationCard : TemplatedControl
set => SetValue(IsClosedProperty, value);
}
public bool IsShowProgress
{
get => GetValue(IsShowProgressProperty);
set => SetValue(IsShowProgressProperty, value);
}
/// <summary>
/// Gets or sets the type of the notification
/// </summary>
@ -126,59 +128,39 @@ public class NotificationCard : TemplatedControl
#endregion
#region
internal static readonly DirectProperty<NotificationCard, bool> EffectiveShowProgressProperty =
AvaloniaProperty.RegisterDirect<NotificationCard, bool>(nameof(EffectiveShowProgress),
o => o.EffectiveShowProgress);
private bool _effectiveShowProgress;
internal bool EffectiveShowProgress
{
get => _effectiveShowProgress;
set => SetAndRaise(EffectiveShowProgressProperty, ref _effectiveShowProgress, value);
}
#endregion
/// <summary>
/// Gets the expiration time of the notification after which it will automatically close.
/// If the value is null then the notification will remain open until the user closes it.
/// </summary>
public TimeSpan? Expiration { get; set; }
public static bool GetCloseOnClick(Button obj)
{
_ = obj ?? throw new ArgumentNullException(nameof(obj));
return (bool)obj.GetValue(CloseOnClickProperty);
}
public static void SetCloseOnClick(Button obj, bool value)
{
_ = obj ?? throw new ArgumentNullException(nameof(obj));
obj.SetValue(CloseOnClickProperty, value);
}
private bool _isClosing;
static NotificationCard()
{
CloseOnClickProperty.Changed.AddClassHandler<Button>(OnCloseOnClickPropertyChanged);
}
private NotificationProgressBar? _progressBar;
private WindowNotificationManager _notificationManager;
private IconButton? _closeButton;
/// <summary>
/// Initializes a new instance of the <see cref="NotificationCard"/> class.
/// </summary>
public NotificationCard()
public NotificationCard(WindowNotificationManager manager)
{
UpdateNotificationType();
ClipToBounds = false;
}
private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
var button = (Button)d;
var value = (bool)e.NewValue!;
if (value) {
button.Click += Button_Click;
} else {
button.Click -= Button_Click;
}
}
/// <summary>
/// Called when a button inside the Notification is clicked.
/// </summary>
private static void Button_Click(object? sender, RoutedEventArgs e)
{
var btn = sender as ILogical;
var notification = btn?.GetLogicalAncestors().OfType<NotificationCard>().FirstOrDefault();
notification?.Close();
_notificationManager = manager;
}
/// <summary>
@ -201,6 +183,26 @@ public class NotificationCard : TemplatedControl
SetupNotificationIcon();
UpdateNotificationType();
}
_progressBar = e.NameScope.Find<NotificationProgressBar>(NotificationCardTheme.ProgressBarPart);
_closeButton = e.NameScope.Find<IconButton>(NotificationCardTheme.CloseButtonPart);
if (_progressBar is not null) {
if (Expiration is null) {
_progressBar.IsVisible = false;
} else {
_progressBar.Expiration = Expiration.Value;
}
}
if (_closeButton is not null) {
_closeButton.Click += HandleCloseButtonClose;
}
SetupEffectiveShowProgress();
}
private void HandleCloseButtonClose(object? sender, EventArgs args)
{
Close();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
@ -225,6 +227,24 @@ public class NotificationCard : TemplatedControl
SetupContent();
}
}
if (e.Property == IsShowProgressProperty ||
e.Property == IsClosedProperty) {
SetupEffectiveShowProgress();
}
}
private void SetupEffectiveShowProgress()
{
if (!IsShowProgress) {
EffectiveShowProgress = false;
} else {
if (Expiration is not null) {
EffectiveShowProgress = true;
} else {
EffectiveShowProgress = false;
}
}
}
private void UpdateNotificationType()
@ -311,14 +331,42 @@ public class NotificationCard : TemplatedControl
internal bool NotifyCloseTick(TimeSpan cycleDuration)
{
InvalidateVisual();
if (Expiration is null) {
return false;
}
Expiration -= cycleDuration;
if (_progressBar is not null) {
_progressBar.CurrentExpiration = Expiration.Value;
}
if (Expiration.Value.TotalMilliseconds < 0) {
return true;
}
return false;
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
if (_notificationManager.IsPauseOnHover) {
_notificationManager.StopExpiredTimer();
}
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (_notificationManager.IsPauseOnHover) {
_notificationManager.StopExpiredTimer();
}
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
if (_notificationManager.IsPauseOnHover) {
_notificationManager.StartExpiredTimer();
}
}
}

View File

@ -26,6 +26,7 @@ internal class NotificationCardTheme : BaseControlTheme
public const string ContentPart = "PART_Content";
public const string CloseButtonPart = "PART_CloseButton";
public const string LayoutTransformControlPart = "PART_LayoutTransformControl";
public const string ProgressBarPart = "PART_ProgressBar";
public NotificationCardTheme()
: base(typeof(NotificationCard))
@ -58,13 +59,15 @@ internal class NotificationCardTheme : BaseControlTheme
RowDefinitions = new RowDefinitions()
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star)
new RowDefinition(GridLength.Star),
new RowDefinition(GridLength.Auto)
}
};
frameDecorator.Child = mainLayout;
BuildHeader(mainLayout, scope);
BuildContent(mainLayout, scope);
BuildProgressBar(mainLayout, scope);
frameDecorator.RegisterInNameScope(scope);
layoutTransformControl.Child = frameDecorator;
@ -119,6 +122,7 @@ internal class NotificationCardTheme : BaseControlTheme
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
closeIconButton.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(closeIconButton, IconButton.PaddingProperty,
GlobalTokenResourceKey.PaddingXXS, BindingPriority.Template,
o =>
@ -149,8 +153,9 @@ internal class NotificationCardTheme : BaseControlTheme
var contentPresenter = new ContentPresenter()
{
Name = ContentPart,
TextWrapping = TextWrapping.Wrap
TextWrapping = TextWrapping.Wrap,
};
TokenResourceBinder.CreateTokenBinding(contentPresenter, ContentPresenter.MarginProperty, NotificationTokenResourceKey.NotificationContentMargin);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty,
NotificationCard.CardContentProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentTemplateProperty,
@ -160,6 +165,21 @@ internal class NotificationCardTheme : BaseControlTheme
layout.Children.Add(contentPresenter);
}
private void BuildProgressBar(Grid layout, INameScope scope)
{
var progressBar = new NotificationProgressBar()
{
Name = ProgressBarPart
};
progressBar.RegisterInNameScope(scope);
CreateTemplateParentBinding(progressBar, NotificationProgressBar.IsVisibleProperty, NotificationCard.EffectiveShowProgressProperty);
TokenResourceBinder.CreateTokenBinding(progressBar, NotificationProgressBar.MarginProperty, NotificationTokenResourceKey.NotificationProgressMargin);
Grid.SetColumn(progressBar, 0);
Grid.SetRow(progressBar, 1);
Grid.SetColumnSpan(progressBar, 2);
layout.Children.Add(progressBar);
}
protected override void BuildStyles()
{
BuildCommonStyle();
@ -218,40 +238,50 @@ internal class NotificationCardTheme : BaseControlTheme
private void BuildAnimationStyle()
{
var commonStyle = new Style(selector => selector.Nesting());
var moveRightInMotionConfig = MotionFactory.BuildMoveRightInMotion(400, TimeSpan.FromMilliseconds(400), new QuadraticEaseOut(),
FillMode.Forward);
foreach (var animation in moveRightInMotionConfig.Animations) {
commonStyle.Animations.Add(animation);
{
var layoutTransformStyle = new Style(selector => selector.Nesting().Template().Name(LayoutTransformControlPart));
var moveRightInMotionConfig = MotionFactory.BuildMoveRightInMotion(400, TimeSpan.FromMilliseconds(400), new QuadraticEaseOut(),
FillMode.Forward);
foreach (var animation in moveRightInMotionConfig.Animations) {
layoutTransformStyle.Animations.Add(animation);
}
commonStyle.Add(layoutTransformStyle);
}
var isClosingStyle = new Style(selector => selector.Nesting().PropertyEquals(NotificationCard.IsClosingProperty, true));
var layoutTransformStyle = new Style(selector => selector.Nesting().Template().Name(LayoutTransformControlPart));
var moveRightOutMotionConfig = MotionFactory.BuildMoveRightOutMotion(400, TimeSpan.FromMilliseconds(400), new QuadraticEaseOut(), FillMode.Forward);
foreach (var animation in moveRightOutMotionConfig.Animations) {
layoutTransformStyle.Animations.Add(animation);
}
isClosingStyle.Animations.Add(new Animation()
{
Duration = TimeSpan.FromMilliseconds(600),
Easing = new QuadraticEaseOut(),
FillMode = FillMode.Forward,
Children =
var layoutTransformStyle = new Style(selector => selector.Nesting().Template().Name(LayoutTransformControlPart));
var moveRightOutMotionConfig = MotionFactory.BuildMoveRightOutMotion(400, TimeSpan.FromMilliseconds(400), new QuadraticEaseIn(), FillMode.Forward);
foreach (var animation in moveRightOutMotionConfig.Animations) {
layoutTransformStyle.Animations.Add(animation);
}
isClosingStyle.Animations.Add(new Animation()
{
new KeyFrame()
Duration = TimeSpan.FromMilliseconds(600),
Easing = new QuadraticEaseIn(),
FillMode = FillMode.Forward,
Children =
{
Cue = new Cue(1.0),
Setters =
new KeyFrame()
{
new Setter(NotificationCard.IsClosedProperty, true),
new Setter(NotificationCard.MarginProperty, new Thickness(0)),
Cue = new Cue(1.0),
Setters =
{
new Setter(NotificationCard.IsClosedProperty, true),
new Setter(NotificationCard.MarginProperty, new Thickness(0)),
}
}
}
}
});
isClosingStyle.Add(layoutTransformStyle);
});
isClosingStyle.Add(layoutTransformStyle);
}
commonStyle.Add(isClosingStyle);
Add(commonStyle);

View File

@ -0,0 +1,83 @@
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Media;
namespace AtomUI.Controls;
internal class NotificationProgressBar : Control
{
#region
public static readonly StyledProperty<double> ProgressIndicatorThicknessProperty =
AvaloniaProperty.Register<NotificationProgressBar, double>(nameof(ProgressIndicatorThickness));
public static readonly StyledProperty<IBrush?> ProgressIndicatorBrushProperty =
AvaloniaProperty.Register<NotificationProgressBar, IBrush?>(nameof(ProgressIndicatorBrush));
public static readonly StyledProperty<TimeSpan> ExpirationProperty =
AvaloniaProperty.Register<NotificationProgressBar, TimeSpan>(nameof(Expiration));
public static readonly StyledProperty<TimeSpan> CurrentExpirationProperty =
AvaloniaProperty.Register<NotificationProgressBar, TimeSpan>(nameof(CurrentExpiration));
internal double ProgressIndicatorThickness
{
get => GetValue(ProgressIndicatorThicknessProperty);
set => SetValue(ProgressIndicatorThicknessProperty, value);
}
public IBrush? ProgressIndicatorBrush
{
get => GetValue(ProgressIndicatorBrushProperty);
set => SetValue(ProgressIndicatorBrushProperty, value);
}
public TimeSpan Expiration
{
get => GetValue(ExpirationProperty);
set => SetValue(ExpirationProperty, value);
}
public TimeSpan CurrentExpiration
{
get => GetValue(CurrentExpirationProperty);
set => SetValue(CurrentExpirationProperty, value);
}
#endregion
static NotificationProgressBar()
{
AffectsMeasure<NotificationProgressBar>(ProgressIndicatorThicknessProperty);
AffectsRender<NotificationProgressBar>(ProgressIndicatorBrushProperty,
ExpirationProperty,
CurrentExpirationProperty);
}
protected override Size MeasureOverride(Size availableSize)
{
return new Size(availableSize.Width, ProgressIndicatorThickness);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
TokenResourceBinder.CreateTokenBinding(this, ProgressIndicatorThicknessProperty, NotificationTokenResourceKey.NotificationProgressHeight);
TokenResourceBinder.CreateTokenBinding(this, ProgressIndicatorBrushProperty, NotificationTokenResourceKey.NotificationProgressBg);
}
public override void Render(DrawingContext context)
{
var indicatorWidth = 0d;
var total = Expiration.TotalMilliseconds;
if (MathUtils.GreaterThan(total, 0)) {
indicatorWidth = (CurrentExpiration.TotalMilliseconds / total) * Bounds.Width;
}
var offsetY = Bounds.Height - ProgressIndicatorThickness;
var indicatorRect = new Rect(new Point(0, offsetY), new Size(indicatorWidth, ProgressIndicatorThickness));
context.FillRectangle(ProgressIndicatorBrush!, indicatorRect);
}
}

View File

@ -64,11 +64,21 @@ internal class NotificationToken : AbstractControlDesignToken
/// </summary>
public double NotificationProgressHeight { get; set; }
/// <summary>
/// 进度条外边距
/// </summary>
public Thickness NotificationProgressMargin { get; set; }
/// <summary>
/// 提醒框宽度
/// </summary>
public double NotificationWidth { get; set; }
/// <summary>
/// 内容外边距
/// </summary>
public Thickness NotificationContentMargin { get; set; }
/// <summary>
/// 标题栏的外边距
/// </summary>
@ -77,15 +87,17 @@ internal class NotificationToken : AbstractControlDesignToken
internal override void CalculateFromAlias()
{
base.CalculateFromAlias();
NotificationPadding = new Thickness(_globalToken.PaddingLG, _globalToken.PaddingMD);
NotificationProgressHeight = 2;
NotificationProgressMargin = new Thickness(0, 0, 0, 1);
NotificationContentMargin = new Thickness(0, 0, 0, _globalToken.PaddingMD);
NotificationPadding = new Thickness(_globalToken.PaddingLG, _globalToken.PaddingMD, _globalToken.PaddingLG, 0);
NotificationBg = _globalToken.ColorToken.ColorNeutralToken.ColorBgElevated;
NotificationIconSize = _globalToken.FontToken.FontSizeLG * _globalToken.FontToken.LineHeightLG;
NotificationCloseButtonSize = _globalToken.HeightToken.ControlHeightLG * 0.55;
NotificationMarginBottom = new Thickness(0, 0, 0, _globalToken.Margin);
NotificationMarginEdge = new Thickness(_globalToken.MarginLG, _globalToken.MarginLG, _globalToken.MarginLG, 0);
AnimationMaxHeight = 150;
NotificationProgressHeight = 2;
NotificationProgressBg = new LinearGradientBrush()
{
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),

View File

@ -23,28 +23,37 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
private IList? _items;
private Queue<NotificationCard> _cleanupQueue;
private DispatcherTimer _dispatcherTimer;
private DispatcherTimer _cardExpiredTimer;
private DispatcherTimer _cleanupTimer;
public static readonly StyledProperty<NotificationPosition> PositionProperty =
AvaloniaProperty.Register<WindowNotificationManager, NotificationPosition>(
nameof(Position), NotificationPosition.TopRight);
public static readonly StyledProperty<int> MaxItemsProperty =
AvaloniaProperty.Register<WindowNotificationManager, int>(nameof(MaxItems), 5);
public static readonly StyledProperty<bool> IsPauseOnHoverProperty =
AvaloniaProperty.Register<WindowNotificationManager, bool>(nameof(IsPauseOnHover), false);
public NotificationPosition Position
{
get => GetValue(PositionProperty);
set => SetValue(PositionProperty, value);
}
public static readonly StyledProperty<int> MaxItemsProperty =
AvaloniaProperty.Register<WindowNotificationManager, int>(nameof(MaxItems), 5);
public int MaxItems
{
get => GetValue(MaxItemsProperty);
set => SetValue(MaxItemsProperty, value);
}
public bool IsPauseOnHover
{
get => GetValue(IsPauseOnHoverProperty);
set => SetValue(IsPauseOnHoverProperty, value);
}
public WindowNotificationManager(TopLevel? host) : this()
{
if (host is not null) {
@ -55,8 +64,8 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
public WindowNotificationManager()
{
UpdatePseudoClasses(Position);
_dispatcherTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50), Tag = this };
_dispatcherTimer.Tick += HandleTimerTick;
_cardExpiredTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50), Tag = this };
_cardExpiredTimer.Tick += HandleCardExpiredTimer;
_cleanupTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50), Tag = this };
_cleanupTimer.Tick += HandleCleanupTimerTick;
_cleanupQueue = new Queue<NotificationCard>();
@ -79,12 +88,12 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
}
}
private void HandleTimerTick(object? sender, EventArgs eventArgs)
private void HandleCardExpiredTimer(object? sender, EventArgs eventArgs)
{
if (_items is not null) {
foreach (var item in _items) {
if (item is NotificationCard card) {
if (card.NotifyCloseTick(_dispatcherTimer.Interval)) {
if (card.NotifyCloseTick(_cardExpiredTimer.Interval)) {
if (!_cleanupQueue.Contains(card)) {
_cleanupQueue.Enqueue(card);
if (!_cleanupTimer.IsEnabled) {
@ -116,9 +125,9 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
{
if (_items is not null) {
if (_items.Count > 0) {
_dispatcherTimer.Start();
_cardExpiredTimer.Start();
} else {
_dispatcherTimer.Stop();
_cardExpiredTimer.Stop();
}
}
}
@ -130,12 +139,13 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
var onClose = notification.OnClose;
Dispatcher.UIThread.VerifyAccess();
var notificationControl = new NotificationCard
var notificationControl = new NotificationCard(this)
{
Title = notification.Title,
CardContent = notification.Content,
NotificationType = notification.Type,
Expiration = expiration == TimeSpan.Zero ? null : expiration
Expiration = expiration == TimeSpan.Zero ? null : expiration,
IsShowProgress = notification.ShowProgress
};
// Add style classes if any
@ -210,4 +220,14 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
PseudoClasses.Set(TopCenterPC, position == NotificationPosition.TopCenter);
PseudoClasses.Set(BottomCenterPC, position == NotificationPosition.BottomCenter);
}
internal void StopExpiredTimer()
{
_cardExpiredTimer.Stop();
}
internal void StartExpiredTimer()
{
_cardExpiredTimer.Start();
}
}

View File

@ -44,6 +44,7 @@ internal class WindowNotificationManagerTheme : BaseControlTheme
var topRightStyle = new Style(selector => selector.Nesting().Class(WindowNotificationManager.TopRightPC));
{
var itemsStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPart));
itemsStyle.Add(ReversibleStackPanel.ReverseOrderProperty, true);
itemsStyle.Add(ReversibleStackPanel.HorizontalAlignmentProperty, HorizontalAlignment.Right);
itemsStyle.Add(ReversibleStackPanel.VerticalAlignmentProperty, VerticalAlignment.Top);
topRightStyle.Add(itemsStyle);

View File

@ -394,8 +394,42 @@ public static partial class MotionFactory
Value = offset
};
startFrame.Setters.Add(translateXSetter);
var scaleYSetter = new Setter()
{
Property = ScaleTransform.ScaleYProperty,
Value = 0.0
};
startFrame.Setters.Add(scaleYSetter);
}
animation.Children.Add(startFrame);
var middleFrame = new KeyFrame()
{
Cue = new Cue(0.7)
};
{
var opacitySetter = new Setter()
{
Property = Visual.OpacityProperty,
Value = 0.0
};
middleFrame.Setters.Add(opacitySetter);
var translateXSetter = new Setter()
{
Property = TranslateTransform.XProperty,
Value = offset
};
middleFrame.Setters.Add(translateXSetter);
var scaleYSetter = new Setter()
{
Property = ScaleTransform.ScaleYProperty,
Value = 1.0
};
middleFrame.Setters.Add(scaleYSetter);
}
animation.Children.Add(middleFrame);
var endFrame = new KeyFrame()
{
@ -414,6 +448,13 @@ public static partial class MotionFactory
Value = 0.0
};
endFrame.Setters.Add(translateXSetter);
var scaleYSetter = new Setter()
{
Property = ScaleTransform.ScaleYProperty,
Value = 1.0
};
endFrame.Setters.Add(scaleYSetter);
}
animation.Children.Add(endFrame);
transformOrigin = new RelativePoint(0.0, 0.0, RelativeUnit.Relative);