找回密码
 立即注册
首页 业界区 业界 Maui 实践:自制轻量级通知组件 NoticeView

Maui 实践:自制轻量级通知组件 NoticeView

神泱 2025-8-4 16:33:39
原创 夏群林 2025.8.4
显示弹出消息,Microsoft.Maui.Controls 命名空间下的 Page 类,提供了 DisplayAlert / DisplayActionSheet / DisplayPromptAsync 三种方法,满足一般的对话交互需要,但必须点击类似 "OK" / "Cancel" 的按钮关闭窗口才能结束对话。也可以自定义一个 ContenPage 页,有点麻烦,除非特殊要求,否则没必要。如果单纯显示消息,发送后不管,官方提供了 Snackbar / Toast,会在可配置的持续时间后被消除。
但在我的应用中,发现 Snackbar / Toast 不好用,经常显示反应不及时。更麻烦的是,当有系列的提示密集发送,主体流程早已结束,消息还在慢吞吞的往外嘣,体验很不好。
于是我自己做了一个 NoticeView, 作为 MAUI 应用中的轻量级通知组件,核心目标是实现多优先级消息的有序管理与灵活显示,其解决的关键问题,即后续消息到来,前面不太重要的消息,要让路,要清空。这样,保证消息的及时显示。
具体需求包括:

  • 支持多级优先级(Low/Medium/High/Critical),高优先级消息需“插队”优先显示,低优先级消息无条件让路;
  • 每条消息需保证最小显示时长(避免闪显),无后续消息时按默认时长显示;
  • 自动清理低优先级消息,限制队列最大长度,防止内存溢出;
  • 支持多线程环境下的消息发送,确保队列操作安全;
  • 提供手动清空功能,资源释放时自动清理,避免内存泄漏。
一、架构设计

1. 核心组件

NoticePriority:枚举定义优先级,Critical 为最高级,权压一切,比如,用Critical级确保清空所有消息。
  1. public enum NoticePriority { Low, Medium, High, Critical }
  2. public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical);
复制代码
NoticeMessage:消息载体,包含内容(Value)和优先级(Priority);
  1. public class NoticeMessage(string value, NoticePriority priority = NoticePriority.Medium)
  2. {
  3.     public string Value { get; } = value;
  4.     public NoticePriority Priority { get; } = priority;
  5. }
复制代码
NoticeDisplayOptions:配置类,控制显示时长(MinDisplayDuration/DisplayDuration)、队列最大长度(MaxQueueLength)。提供了默认显示样式,包括字体、颜色、旋转、阴影,我喜欢浮雕斜上如同惊鸿一瞥的效果,作为默认配置。各人可自行定义。
  1. public class NoticeDisplayOptions
  2. {
  3.     public TimeSpan DisplayDuration { get; set; } = TimeSpan.FromSeconds(3);
  4.     public TimeSpan MinDisplayDuration { get; set; } = TimeSpan.FromMilliseconds(500);
  5.     public int MaxQueueLength { get; set; } = 50;
  6.    
  7.     public Color FontColor { get; set; } = Colors.DarkOrchid;
  8.     public float FontSize { get; set; } = 16F;
  9.     public float RotationAngle { get; set; } = -20;
  10.     public bool HasShadow { get; set; } = true;
  11. }
复制代码
QueuedNotice:内部队列消息类,封装优先级、入队序号(Order)和内容,实现 IComparable 接口用于排序;
  1. private class QueuedNotice(NoticePriority priority, int order, string message) : IComparable<QueuedNotice>
  2. {
  3.      public NoticePriority Priority { get; } = priority;
  4.      public int Order { get; } = order;
  5.      public string Message { get; } = message;
  6.      public int CompareTo(QueuedNotice? other)
  7.      {
  8.          if (other is null) return 1;
  9.          // 优先级顺序:Critical > High > Medium > Low
  10.          int priorityCompare = other.Priority.CompareTo(Priority);
  11.          return priorityCompare != 0 ? priorityCompare : Order.CompareTo(other.Order);
  12.      }
  13. }
复制代码
NoticeView:核心控件,继承 GraphicsView,实现 IDrawable,用于绘制UI;和 IDisposable,便于资源释放。NoticeView 负责消息接收、队列管理、显示控制。  也提供了发送消息和清除消息的静态方法。
  1. public partial class NoticeView : GraphicsView, IDrawable, IDisposable
  2. {
  3.     private readonly NoticeDisplayOptions _options;
  4.     private readonly ConcurrentDictionary<int, QueuedNotice> _messageQueue = new();
  5.     private readonly Lock _queueSync = new();
  6.     private QueuedNotice? _currentMessage;
  7.     private DateTimeOffset _currentMessageStartTime;
  8.     private int _messageOrder = 0;
  9.     private bool _isTimerRunning;
  10.     private IDispatcherTimer? _timer;
  11.     public NoticeView(NoticeDisplayOptions? options = null)
  12.     {
  13.         _options = options ?? new NoticeDisplayOptions();
  14.         Drawable = this;
  15.         VerticalOptions = LayoutOptions.Fill;
  16.         HorizontalOptions = LayoutOptions.Fill;
  17.         InputTransparent = true;
  18.         WeakReferenceMessenger.Default.Register<NoticeMessage>(this, (_, m) => OnMessageArrive(m));
  19.     }
  20.    
  21.     // ...
  22.    
  23. }
复制代码
2. 整体架构图
  1. ┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
  2. │   外部调用者    │──┬──>│  NoticeMessage  │──┬──>│   NoticeView    │
  3. └─────────────────┘  │   └─────────────────┘  │   └────────┬────────┘
  4.                      │                        │            │
  5.                      │                        │            ▼
  6.                      │                        │   ┌─────────────────┐
  7.                      │                        └──>│  消息队列管理   │
  8.                      │                             └────────┬────────┘
  9.                      │                                      │
  10.                      └──────────────────────────────────────┘
  11.                                   发送消息
复制代码
二、关键实现与核心代码

1. 优先级与排序机制

通过自定义排序规则保证消息按“优先级降序+同优先级入队顺序升序”排列,确保高优先级消息先显示,同优先级消息按发送顺序显示。
实现细节

  • 自定义 QueuedNotice 类并实现 IComparable 接口,排序规则:

    • 先比较优先级(Critical > High > Medium > Low);
    • 优先级相同时,比较入队序号(Order),序号越小(入队越早)越优先。

  • 入队序号通过 Interlocked.Increment 生成,保证多线程环境下的原子性和唯一性,避免序号冲突。
  1. // 生成唯一入队序号(多线程安全)
  2. int enqueueOrder = Interlocked.Increment(ref _messageOrder);
复制代码
设计考量

  • 用 IComparable 封装排序逻辑,避免分散在业务代码中,提高可维护性;
  • 原子操作生成序号,解决多线程并发下的序号重复问题,确保同优先级消息的顺序性。
2. 队列管理策略

通过线程安全容器存储消息,自动清理低优先级消息,控制队列长度,确保高优先级消息“无障碍”显示。
实现细节

  • 采用 ConcurrentDictionary 作为队列容器,以入队序号(Order)为键,支持并发读写,减少锁竞争;
  • 新消息入队时,根据优先级清理低优先级消息:

    • 非 Critical 级:移除队列中所有优先级低于新消息的消息;
    • Critical 级:直接清空队列+清除当前显示的消息(权压一切);

  • 队列长度超过 MaxQueueLength 时,循环移除“最低优先级中最早入队的消息”,避免队列无限增长。
  1. private void OnMessageArrive(NoticeMessage message)
  2. {
  3.     int enqueueOrder = Interlocked.Increment(ref _messageOrder);
  4.     var enqueueNotice = new QueuedNotice(message.Priority, enqueueOrder, message.Value);
  5.     bool needImmediateSwitch = false;
  6.     lock (_queueSync) // 加锁保证原子性
  7.     {
  8.         if (message.Priority == NoticePriority.Critical)
  9.         {
  10.             // Critical级:清空队列+清除当前消息,直接抢占显示
  11.             _messageQueue.Clear();
  12.             _currentMessage = null;
  13.             needImmediateSwitch = true;
  14.         }
  15.         else
  16.         {
  17.             // 非Critical级:移除队列中所有低优先级消息
  18.             var lowerPriorityItems = _messageQueue.Values
  19.                 .Where(m => m.Priority < message.Priority)
  20.                 .ToList();
  21.             foreach (var item in lowerPriorityItems)
  22.                 _ = _messageQueue.TryRemove(item.Order, out _);
  23.             // 关键:当前显示的消息若优先级更低,立即清除并标记切换
  24.             if (_currentMessage != null && _currentMessage.Priority < message.Priority)
  25.             {
  26.                 _messageQueue.TryRemove(_currentMessage.Order, out _);
  27.                 _currentMessage = null;
  28.                 needImmediateSwitch = true;
  29.             }
  30.         }
  31.         // 队列长度控制:超过上限时,移除最低优先级中最旧的消息
  32.         while (_messageQueue.Count >= _options.MaxQueueLength)
  33.         {
  34.             var lowestPriority = _messageQueue.Values.Min(m => m.Priority);
  35.             var oldestLow = _messageQueue.Values
  36.                 .Where(m => m.Priority == lowestPriority)
  37.                 .OrderBy(m => m.Order)
  38.                 .FirstOrDefault();
  39.             if (oldestLow != null)
  40.                 _messageQueue.TryRemove(oldestLow.Order, out _);
  41.             else
  42.                 break; // 极端情况防止死循环
  43.         }
  44.         _messageQueue.TryAdd(enqueueNotice.Order, enqueueNotice);
  45.     }
  46.     // 高优先级消息立即切换显示,无需等待当前消息时长
  47.     if (message.Priority == NoticePriority.Critical || needImmediateSwitch)
  48.         MainThread.BeginInvokeOnMainThread(SwitchToNextMessage);
  49.     else
  50.         MainThread.BeginInvokeOnMainThread(UpdateDisplay);
  51. }
复制代码
设计考量

  • 用 ConcurrentDictionary 减少简单操作,如单条添加 / 移除的锁竞争,提高并发性能;
  • 复合操作,如批量清理+添加,用 lock 保证原子性,避免数据不一致;
  • Critical 级消息直接清空队列和当前消息,确保“最高优先级”的绝对权威性;
  • 队列长度控制优先移除“最低优先级中最旧的消息”,平衡了优先级和时效性。
3. 显示与切换逻辑

通过定时器监控消息显示时长,根据“是否有后续消息”动态调整显示时长,高优先级消息可强制打断当前消息显示。
实现细节

  • 定时器采用 DispatcherTimer,绑定UI线程,间隔100ms高频检查,确保时长判断精准;
  • 显示时长规则:  有后续消息时,当前消息至少显示 MinDisplayDuration,避免闪显;  无后续消息时,显示时长为 MinDisplayDuration 与 DisplayDuration 的最大值,保证用户能看清;
  • 切换逻辑:当满足时长条件或收到更高优先级消息时,移除当前消息,显示队列中优先级最高的下一条消息。
  1. private void Timer_Tick(object? sender, EventArgs e)
  2. {
  3.     if (_currentMessage == null)
  4.     {
  5.         StopTimer();
  6.         return;
  7.     }
  8.     // 计算当前消息已显示时长(从开始显示时起算)
  9.     var elapsed = DateTimeOffset.Now - _currentMessageStartTime;
  10.     bool hasNextMessage;
  11.     lock (_queueSync)
  12.     {
  13.         // 检查是否有除当前消息外的其他消息
  14.         hasNextMessage = _messageQueue.Count > (_currentMessage != null ? 1 : 0);
  15.     }
  16.     // 动态计算最大显示时长
  17.     var maxDuration = hasNextMessage
  18.         ? _options.MinDisplayDuration
  19.         : TimeSpan.FromMilliseconds(Math.Max(
  20.             _options.MinDisplayDuration.TotalMilliseconds,
  21.             _options.DisplayDuration.TotalMilliseconds));
  22.     // 满足时长条件则切换消息
  23.     if (elapsed >= maxDuration)
  24.         MainThread.BeginInvokeOnMainThread(SwitchToNextMessage);
  25. }
  26. // 切换到下一条消息
  27. private void SwitchToNextMessage()
  28. {
  29.     lock (_queueSync)
  30.     {
  31.         // 移除当前显示的消息
  32.         if (_currentMessage != null)
  33.             _messageQueue.TryRemove(_currentMessage.Order, out _);
  34.         // 显示下一条消息(队列中优先级最高的)
  35.         var nextNotice = !_messageQueue.IsEmpty
  36.             ? _messageQueue.Values.OrderBy(m => m).FirstOrDefault()
  37.             : null;
  38.         if (nextNotice != null)
  39.         {
  40.             _currentMessage = nextNotice;
  41.             _currentMessageStartTime = DateTimeOffset.Now; // 从显示时开始计时
  42.             Notice = nextNotice.Message;
  43.             Invalidate(); // 触发UI重绘
  44.         }
  45.         else
  46.         {
  47.             ClearCurrentMessage(); // 队列空则清除显示
  48.         }
  49.     }
  50. }
复制代码
设计考量

  • _currentMessageStartTime 仅在消息开始显示时赋值,确保计时起点与用户可见时间一致;
  • 高频定时器(100ms)减少时长判断的延迟,提升用户体验;
  • 动态调整显示时长(区分有/无后续消息),既避免消息闪显,又防止无后续消息时显示过短。
4. 线程安全与资源管理

通过锁机制和线程隔离保证多线程安全,通过 IDisposable 接口和手动清理方法确保资源释放。
实现细节

  • 线程安全:

    • 简单操作(单条消息的添加/移除)依赖 ConcurrentDictionary 的线程安全特性;
    • 复合操作(批量清理、队列长度控制)用 lock (_queueSync) 保证原子性;
    • 所有UI操作(如刷新显示、切换消息)通过 MainThread.BeginInvokeOnMainThread 限制在主线程,避免跨线程异常。

  • 资源管理:

    • 实现 IDisposable 接口,释放时注销消息订阅、停止定时器、清空队列;
    • 提供 ClearQueue 手动清空方法,支持主动清理;
    • ClearNotice 方法通过发送 Critical 级空消息,利用其“清空一切”特性快速清理。

  1. public void Dispose()
  2. {
  3.     // 注销消息订阅,避免内存泄漏
  4.     WeakReferenceMessenger.Default.UnregisterAll(this);
  5.     StopTimer(); // 停止定时器
  6.     ClearQueue(); // 清空队列和当前消息
  7.     GC.SuppressFinalize(this);
  8. }
  9. // 手动清空队列
  10. public void ClearQueue()
  11. {
  12.     lock (_queueSync)
  13.     {
  14.         _messageQueue.Clear();
  15.         ClearCurrentMessage();
  16.     }
  17. }
  18. // 清空通知(利用Critical级特性)
  19. public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical);
复制代码
设计考量

  • 最小化锁范围(仅复合操作加锁),平衡线程安全和性能;
  • 消息订阅使用弱引用 WeakReferenceMessenger,配合 Dispose 注销,避免组件销毁后仍接收消息导致的内存泄漏;
  • ClearNotice 复用 Critical 级消息的清理逻辑,减少代码冗余,同时保证清理彻底性。
三、使用示例

使用很方便。在需要显示的页面,放一个 NoticeView 实例即可:
  1. public partial class SearchPage : ContentPage
  2. {
  3.     public SearchPage(SearchViewModel viewModel)
  4.     {
  5.         InitializeComponent();
  6.         this.contentPageLayout.Children.Add(new NoticeView() { ZIndex = 3, InputTransparent = true });
  7.     }
  8.    
  9.         // ...
  10. }
复制代码
并且,可在多个显示页面放置 NoticeView 实例。
在任何需要的地方调用:
  1. NoticeView.DisplayNotice($"Neither found nor suggested",NoticePriority.High);
复制代码
四、得意之处

通过“优先级排序+队列动态清理+即时切换+线程安全”的设计,NoticeView 实现了高优先级消息优先显示、低优先级消息自动让路的核心需求。其设计亮点在于:

  • 用 IComparable 和原子序号保证了多线程下的消息顺序;
  • 区分简单/复合操作的线程安全策略,平衡了性能和安全性;
  • 动态时长控制和强制切换机制,兼顾了用户体验和优先级权威性;
  • 完善的资源清理机制,避免了内存泄漏风险。
该组件可直接集成到 MAUI 应用中,支持自定义样式和时长,适用于各类轻量级通知场景,如操作提示、状态更新等。
本方案源代码开源,按照 MIT 协议许可。地址 xiaql/Zhally.Toolkit: Practices on specific tool kit in MAUI。

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册