神泱 发表于 2025-8-4 16:33:39

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

原创 夏群林 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级确保清空所有消息。
public enum NoticePriority { Low, Medium, High, Critical }

public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical); NoticeMessage:消息载体,包含内容(Value)和优先级(Priority);
public class NoticeMessage(string value, NoticePriority priority = NoticePriority.Medium)
{
    public string Value { get; } = value;
    public NoticePriority Priority { get; } = priority;
}NoticeDisplayOptions:配置类,控制显示时长(MinDisplayDuration/DisplayDuration)、队列最大长度(MaxQueueLength)。提供了默认显示样式,包括字体、颜色、旋转、阴影,我喜欢浮雕斜上如同惊鸿一瞥的效果,作为默认配置。各人可自行定义。
public class NoticeDisplayOptions
{
    public TimeSpan DisplayDuration { get; set; } = TimeSpan.FromSeconds(3);
    public TimeSpan MinDisplayDuration { get; set; } = TimeSpan.FromMilliseconds(500);
    public int MaxQueueLength { get; set; } = 50;
   
    public Color FontColor { get; set; } = Colors.DarkOrchid;
    public float FontSize { get; set; } = 16F;
    public float RotationAngle { get; set; } = -20;
    public bool HasShadow { get; set; } = true;
}QueuedNotice:内部队列消息类,封装优先级、入队序号(Order)和内容,实现 IComparable 接口用于排序;
private class QueuedNotice(NoticePriority priority, int order, string message) : IComparable<QueuedNotice>
{
   public NoticePriority Priority { get; } = priority;
   public int Order { get; } = order;
   public string Message { get; } = message;

   public int CompareTo(QueuedNotice? other)
   {
         if (other is null) return 1;
         // 优先级顺序:Critical > High > Medium > Low
         int priorityCompare = other.Priority.CompareTo(Priority);
         return priorityCompare != 0 ? priorityCompare : Order.CompareTo(other.Order);
   }
}NoticeView:核心控件,继承 GraphicsView,实现 IDrawable,用于绘制UI;和 IDisposable,便于资源释放。NoticeView 负责消息接收、队列管理、显示控制。也提供了发送消息和清除消息的静态方法。
public partial class NoticeView : GraphicsView, IDrawable, IDisposable
{
    private readonly NoticeDisplayOptions _options;
    private readonly ConcurrentDictionary<int, QueuedNotice> _messageQueue = new();
    private readonly Lock _queueSync = new();
    private QueuedNotice? _currentMessage;
    private DateTimeOffset _currentMessageStartTime;
    private int _messageOrder = 0;
    private bool _isTimerRunning;
    private IDispatcherTimer? _timer;

    public NoticeView(NoticeDisplayOptions? options = null)
    {
      _options = options ?? new NoticeDisplayOptions();
      Drawable = this;
      VerticalOptions = LayoutOptions.Fill;
      HorizontalOptions = LayoutOptions.Fill;
      InputTransparent = true;

      WeakReferenceMessenger.Default.Register<NoticeMessage>(this, (_, m) => OnMessageArrive(m));
    }
   
    // ...
   
}2. 整体架构图

┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
│   外部调用者    │──┬──>│NoticeMessage│──┬──>│   NoticeView    │
└─────────────────┘│   └─────────────────┘│   └────────┬────────┘
                     │                        │            │
                     │                        │            ▼
                     │                        │   ┌─────────────────┐
                     │                        └──>│消息队列管理   │
                     │                           └────────┬────────┘
                     │                                    │
                     └──────────────────────────────────────┘
                                  发送消息二、关键实现与核心代码

1. 优先级与排序机制

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

[*]自定义 QueuedNotice 类并实现 IComparable 接口,排序规则:
[*]先比较优先级(Critical > High > Medium > Low);
[*]优先级相同时,比较入队序号(Order),序号越小(入队越早)越优先。

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

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

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

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

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

[*]队列长度超过 MaxQueueLength 时,循环移除“最低优先级中最早入队的消息”,避免队列无限增长。
private void OnMessageArrive(NoticeMessage message)
{
    int enqueueOrder = Interlocked.Increment(ref _messageOrder);
    var enqueueNotice = new QueuedNotice(message.Priority, enqueueOrder, message.Value);
    bool needImmediateSwitch = false;

    lock (_queueSync) // 加锁保证原子性
    {
      if (message.Priority == NoticePriority.Critical)
      {
            // Critical级:清空队列+清除当前消息,直接抢占显示
            _messageQueue.Clear();
            _currentMessage = null;
            needImmediateSwitch = true;
      }
      else
      {
            // 非Critical级:移除队列中所有低优先级消息
            var lowerPriorityItems = _messageQueue.Values
                .Where(m => m.Priority < message.Priority)
                .ToList();
            foreach (var item in lowerPriorityItems)
                _ = _messageQueue.TryRemove(item.Order, out _);

            // 关键:当前显示的消息若优先级更低,立即清除并标记切换
            if (_currentMessage != null && _currentMessage.Priority < message.Priority)
            {
                _messageQueue.TryRemove(_currentMessage.Order, out _);
                _currentMessage = null;
                needImmediateSwitch = true;
            }
      }

      // 队列长度控制:超过上限时,移除最低优先级中最旧的消息
      while (_messageQueue.Count >= _options.MaxQueueLength)
      {
            var lowestPriority = _messageQueue.Values.Min(m => m.Priority);
            var oldestLow = _messageQueue.Values
                .Where(m => m.Priority == lowestPriority)
                .OrderBy(m => m.Order)
                .FirstOrDefault();
            if (oldestLow != null)
                _messageQueue.TryRemove(oldestLow.Order, out _);
            else
                break; // 极端情况防止死循环
      }

      _messageQueue.TryAdd(enqueueNotice.Order, enqueueNotice);
    }

    // 高优先级消息立即切换显示,无需等待当前消息时长
    if (message.Priority == NoticePriority.Critical || needImmediateSwitch)
      MainThread.BeginInvokeOnMainThread(SwitchToNextMessage);
    else
      MainThread.BeginInvokeOnMainThread(UpdateDisplay);
}设计考量:

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

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

[*]定时器采用 DispatcherTimer,绑定UI线程,间隔100ms高频检查,确保时长判断精准;
[*]显示时长规则:有后续消息时,当前消息至少显示 MinDisplayDuration,避免闪显;无后续消息时,显示时长为 MinDisplayDuration 与 DisplayDuration 的最大值,保证用户能看清;
[*]切换逻辑:当满足时长条件或收到更高优先级消息时,移除当前消息,显示队列中优先级最高的下一条消息。
private void Timer_Tick(object? sender, EventArgs e)
{
    if (_currentMessage == null)
    {
      StopTimer();
      return;
    }

    // 计算当前消息已显示时长(从开始显示时起算)
    var elapsed = DateTimeOffset.Now - _currentMessageStartTime;
    bool hasNextMessage;

    lock (_queueSync)
    {
      // 检查是否有除当前消息外的其他消息
      hasNextMessage = _messageQueue.Count > (_currentMessage != null ? 1 : 0);
    }

    // 动态计算最大显示时长
    var maxDuration = hasNextMessage
      ? _options.MinDisplayDuration
      : TimeSpan.FromMilliseconds(Math.Max(
            _options.MinDisplayDuration.TotalMilliseconds,
            _options.DisplayDuration.TotalMilliseconds));

    // 满足时长条件则切换消息
    if (elapsed >= maxDuration)
      MainThread.BeginInvokeOnMainThread(SwitchToNextMessage);
}

// 切换到下一条消息
private void SwitchToNextMessage()
{
    lock (_queueSync)
    {
      // 移除当前显示的消息
      if (_currentMessage != null)
            _messageQueue.TryRemove(_currentMessage.Order, out _);

      // 显示下一条消息(队列中优先级最高的)
      var nextNotice = !_messageQueue.IsEmpty
            ? _messageQueue.Values.OrderBy(m => m).FirstOrDefault()
            : null;

      if (nextNotice != null)
      {
            _currentMessage = nextNotice;
            _currentMessageStartTime = DateTimeOffset.Now; // 从显示时开始计时
            Notice = nextNotice.Message;
            Invalidate(); // 触发UI重绘
      }
      else
      {
            ClearCurrentMessage(); // 队列空则清除显示
      }
    }
}设计考量:

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

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

[*]线程安全:

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

[*]资源管理:

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

public void Dispose()
{
    // 注销消息订阅,避免内存泄漏
    WeakReferenceMessenger.Default.UnregisterAll(this);
    StopTimer(); // 停止定时器
    ClearQueue(); // 清空队列和当前消息
    GC.SuppressFinalize(this);
}

// 手动清空队列
public void ClearQueue()
{
    lock (_queueSync)
    {
      _messageQueue.Clear();
      ClearCurrentMessage();
    }
}

// 清空通知(利用Critical级特性)
public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical);设计考量:

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

使用很方便。在需要显示的页面,放一个 NoticeView 实例即可:
public partial class SearchPage : ContentPage
{

    public SearchPage(SearchViewModel viewModel)
    {
      InitializeComponent();
      this.contentPageLayout.Children.Add(new NoticeView() { ZIndex = 3, InputTransparent = true });
    }
   
        // ...
}并且,可在多个显示页面放置 NoticeView 实例。
在任何需要的地方调用:
NoticeView.DisplayNotice($"Neither found nor suggested",NoticePriority.High);四、得意之处

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

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

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除
页: [1]
查看完整版本: Maui 实践:自制轻量级通知组件 NoticeView