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

原创 夏群林

显示弹出消息,Microsoft.Maui.Controls 命名空间下的 Page 类,提供了 DisplayAlert / DisplayActionSheet / DisplayPromptAsync 三种方法,满足一般的对话交互需要,但必须点击类似 "OK" / "Cancel" 的按钮关闭窗口才能结束对话。也可以自定义一个 ContenPage 页,有点麻烦,除非特殊要求,否则没必要。如果单纯显示消息,发送后不管,官方提供了 Snackbar / Toast,会在可配置的持续时间后被消除。

但在我的应用中,发现 Snackbar / Toast 不好用,经常显示反应不及时。更麻烦的是,当有系列的提示密集发送,主体流程早已结束,消息还在慢吞吞的往外嘣,体验很不好。

于是我自己做了一个 NoticeView, 作为 MAUI 应用中的轻量级通知组件,核心目标是实现多优先级消息的有序管理与灵活显示,其解决的关键问题,即后续消息到来,前面不太重要的消息,要让路,要清空。这样,保证消息的及时显示。

具体需求包括:

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

一、架构设计

1. 核心组件

NoticePriority:枚举定义优先级,Critical 为最高级,权压一切,比如,用Critical级确保清空所有消息。

c# 复制代码
public enum NoticePriority { Low, Medium, High, Critical }

public static void ClearNotice() => DisplayNotice(string.Empty, NoticePriority.Critical); 

NoticeMessage:消息载体,包含内容(Value)和优先级(Priority);

c# 复制代码
public class NoticeMessage(string value, NoticePriority priority = NoticePriority.Medium)
{
    public string Value { get; } = value;
    public NoticePriority Priority { get; } = priority;
}

NoticeDisplayOptions:配置类,控制显示时长(MinDisplayDuration/DisplayDuration)、队列最大长度(MaxQueueLength)。提供了默认显示样式,包括字体、颜色、旋转、阴影,我喜欢浮雕斜上如同惊鸿一瞥的效果,作为默认配置。各人可自行定义。

c# 复制代码
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 接口用于排序;

c# 复制代码
 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 负责消息接收、队列管理、显示控制。 也提供了发送消息和清除消息的静态方法。

c# 复制代码
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. 整体架构图

plaintext 复制代码
┌─────────────────┐      ┌─────────────────┐      ┌─────────────────┐
│   外部调用者    │──┬──>│  NoticeMessage  │──┬──>│   NoticeView    │
└─────────────────┘  │   └─────────────────┘  │   └────────┬────────┘
                     │                        │            │
                     │                        │            ▼
                     │                        │   ┌─────────────────┐
                     │                        └──>│  消息队列管理   │
                     │                             └────────┬────────┘
                     │                                      │
                     └──────────────────────────────────────┘
                                  发送消息

二、关键实现与核心代码

1. 优先级与排序机制

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

实现细节

  • 自定义 QueuedNotice 类并实现 IComparable<QueuedNotice> 接口,排序规则:
    1. 先比较优先级(Critical > High > Medium > Low);
    2. 优先级相同时,比较入队序号(Order),序号越小(入队越早)越优先。
  • 入队序号通过 Interlocked.Increment 生成,保证多线程环境下的原子性和唯一性,避免序号冲突。
csharp 复制代码
// 生成唯一入队序号(多线程安全)
int enqueueOrder = Interlocked.Increment(ref _messageOrder);

设计考量

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

2. 队列管理策略

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

实现细节

  • 采用 ConcurrentDictionary<int, QueuedNotice> 作为队列容器,以入队序号(Order)为键,支持并发读写,减少锁竞争;
  • 新消息入队时,根据优先级清理低优先级消息:
    • 非 Critical 级:移除队列中所有优先级低于新消息的消息;
    • Critical 级:直接清空队列+清除当前显示的消息(权压一切);
  • 队列长度超过 MaxQueueLength 时,循环移除"最低优先级中最早入队的消息",避免队列无限增长。
csharp 复制代码
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,避免闪显; 无后续消息时,显示时长为 MinDisplayDurationDisplayDuration 的最大值,保证用户能看清;
  • 切换逻辑:当满足时长条件或收到更高优先级消息时,移除当前消息,显示队列中优先级最高的下一条消息。
csharp 复制代码
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 级空消息,利用其"清空一切"特性快速清理。
csharp 复制代码
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 实例即可:

c# 复制代码
public partial class SearchPage : ContentPage
{

    public SearchPage(SearchViewModel viewModel)
    {
        InitializeComponent();
        this.contentPageLayout.Children.Add(new NoticeView() { ZIndex = 3, InputTransparent = true });
    }
    
	// ...
}

并且,可在多个显示页面放置 NoticeView 实例。

在任何需要的地方调用:

c# 复制代码
NoticeView.DisplayNotice($"Neither found nor suggested",NoticePriority.High);

四、得意之处

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

  • IComparable 和原子序号保证了多线程下的消息顺序;
  • 区分简单/复合操作的线程安全策略,平衡了性能和安全性;
  • 动态时长控制和强制切换机制,兼顾了用户体验和优先级权威性;
  • 完善的资源清理机制,避免了内存泄漏风险。

该组件可直接集成到 MAUI 应用中,支持自定义样式和时长,适用于各类轻量级通知场景,如操作提示、状态更新等。

本方案源代码开源,按照 MIT 协议许可。我会放在地址 https://github.com/xiaql/Zhally.Toolkit,敬请关注。