众所周知,UWP提供了强大的样式可定制化的Toast通知功能,然而这些消息都是和系统集成的,屏幕显示位置固定,而且和系统操作中心深度集成。然而有时候我们只想让app提示一些短暂的消息,比如用户添加收藏,app提示“收藏已添加”。这种消息只需要显示1到2秒左右,而且不需要用户交互。尽管在UWP中我们可以把这种消息放到某种状态栏中,但是类似Android系统提供的那种Toast通知方式效果更好。这里我们实现一个PopupManager来提供Toast通知的队列和显示功能。

先上效果图:

消息显示原理很简单,我们首先获得目标页面所属Window的UI线程的Dispatcher,然后收到消息时在对应的Dispatcher中设置并显示Popup,同时对Popup应用动画效果(淡入淡出)。需要Dispatcher的原因是:由于我们的PopupManager支持消息队列顺序显示,而UWP支持多窗口,每个Window拥有且只拥有一个UI线程,所以在消息队列中提取并显示时,我们需要在消息所属Window的UI线程中操作。

初始化

为了提高效率,我们可以在PopupManager初始化时,预先初始化当前UI线程(即 main Window 的UI线程)用到的相关组件(主要是Popup和Storyboard)。初始化时还要创建消息队列和工作线程,用于按顺序提取并显示消息。

初始化相关代码如下:

internal struct PopupMessageItem
{
    public bool IsMainWindow;
    public CoreDispatcher Dispatcher;
    public string Message;
}

public static void Init()
{
    if (!_isInited)
    {
        _waitEvent = new ManualResetEvent(false);
        _aniCompleteEvent = new AutoResetEvent(false);
        CreatePopup(out _mainTextBlock, out _mainPopup, out _mainStoryboard);
        _messageQueue = new Queue<PopupMessageItem>();
        Task.Run(new Action(DoWork));
        _isInited = true;
    }
}

private static void CreatePopup(out TextBlock textBlock, out Popup popup, out Storyboard storyboard)
{
    textBlock = new TextBlock
    {
        FontSize = FontSizeManager.FontSize18,
        Foreground = new SolidColorBrush(Colors.White)
    };

    Border border = new Border
    {
        Background = new SolidColorBrush(Colors.Black),
        CornerRadius = new CornerRadius(15, 15, 15, 15),
        Padding = new Thickness(12, 4, 12, 4),
        Child = textBlock
    };

    DiscreteDoubleKeyFrame keyFrame1 = new DiscreteDoubleKeyFrame
    {
        KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)),
        Value = 0
    };
    LinearDoubleKeyFrame keyFrame2 = new LinearDoubleKeyFrame
    {
        KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300)),
        Value = 0.7
    };
    DiscreteDoubleKeyFrame keyFrame3 = new DiscreteDoubleKeyFrame
    {
        KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1800)),
        Value = 0.7
    };
    LinearDoubleKeyFrame keyFrame4 = new LinearDoubleKeyFrame
    {
        KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(2400)),
        Value = 0
    };

    DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();
    animation.KeyFrames.Add(keyFrame1);
    animation.KeyFrames.Add(keyFrame2);
    animation.KeyFrames.Add(keyFrame3);
    animation.KeyFrames.Add(keyFrame4);
    Storyboard.SetTarget(animation, border);
    Storyboard.SetTargetProperty(animation, "Opacity");

    storyboard = new Storyboard();
    storyboard.Children.Add(animation);

    popup = new Popup { Child = border };
}

PopupManager.Init方法可以在应用启动时调用。

工作线程

工作线程(即初始化函数中指定的DoWork方法)实现了一个简单的消息循环,在消息队列(即_messageQueue)为空时进入睡眠,消息来临时被唤醒,从消息队列中Dequeue消息。获得消息后首先要判断它是否属于 main Window,如果是的话,由于我们已经初始化好了 main Window 所用的Popup,这时直接设置Popup用到的TextBlock的Text、然后调用 main Window 所用的Storyboard即可。如果不属于 main Window 的话,我们需要在对应的Dispatcher中调用CreatePopup方法重新创建所需要的组件。调用Dispatcher的RunAsync方法后,我们等待对应Popup的显示动画结束事件,然后再尝试Dequeue下一条消息,若没有消息了则重新进入睡眠。

在设置Popup.IsOpen来显示Popup之前,我们还需要注册Popup.Opened事件,在这个事件中我们获得窗口尺寸和Popup尺寸,然后重新调整Popup的位置,以实现Popup居中显示的效果。当然这里也可以实现屏幕底部显示等效果,原理相同。

最后我们在Storyboard.Completed事件中关闭Popup。

工作线程相关代码如下:

private static void DoWork()
{
    while (true)
    {
        _waitEvent.WaitOne();

        PopupMessageItem messageItem;
        lock (_lockObj)
        {
            if (_messageQueue.Count == 0)
            {
                _waitEvent.Reset();
                continue;
            }
            messageItem = _messageQueue.Dequeue();
        }

        bool isMainWindow = messageItem.IsMainWindow;
        string message = messageItem.Message;
        messageItem.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            try
            {
                if (isMainWindow)
                {
                    _mainTextBlock.Text = message;
                    _currentPopup = _mainPopup;
                    _mainPopup.Opened += Popup_Opened;
                    _mainPopup.IsOpen = true;
                    _mainStoryboard.Completed += StoryboardOnCompleted;
                    _mainStoryboard.Begin();
                }
                else
                {
                    CreatePopup(out TextBlock textBlock, out Popup popup, out Storyboard storyboard);
                    textBlock.Text = messageItem.Message;
                    popup.Opened += Popup_Opened;
                    _currentPopup = popup;
                    popup.IsOpen = true;
                    storyboard.Completed += StoryboardOnCompleted;
                    storyboard.Begin();
                }
            }
            catch (Exception ex)
            {
                // Maybe isMainWindow is not correct?
            }
        });

        _aniCompleteEvent.WaitOne();
    }
}

private static void Popup_Opened(object sender, object e)
{
    Popup popup = (Popup)sender;
    popup.Opened -= Popup_Opened;
    FrameworkElement childElement = (FrameworkElement)popup.Child;
    childElement.UpdateLayout();
    double childWidth = childElement.ActualWidth;
    double childHeight = childElement.ActualHeight;
    double windowWidth = ScreenHelper.GetScreenWidth(); // 这里可以用Window.Current.Bounds等方法获得窗口尺寸
    double windowHeight = ScreenHelper.GetScreenHeight();
    double horizontalOffset = (windowWidth - childWidth) / 2;
    double verticalOffset = (windowHeight - childHeight) / 2;
    popup.HorizontalOffset = horizontalOffset > 0 ? horizontalOffset : 0;
    popup.VerticalOffset = verticalOffset > 0 ? verticalOffset : 0;
}

private static void StoryboardOnCompleted(object sender, object o)
{
    ((Storyboard)sender).Completed -= StoryboardOnCompleted;
    _currentPopup.IsOpen = false;
    _aniCompleteEvent.Set();
}

调用

public static void PostMessage(CoreDispatcher dispatcher, string message, bool isMainWindow = true)
{
    if (!string.IsNullOrWhiteSpace(message))
    {
        lock (_lockObj)
        {
            PopupMessageItem messageItem = new PopupMessageItem
            {
                IsMainWindow = isMainWindow,
                Dispatcher = dispatcher,
                Message = message
            };
            _messageQueue.Enqueue(messageItem);
            _waitEvent.Set();
        }
    }
}
» 转载请注明来源及链接:未来代码研究所

Related Posts:

3 Responses to “UWP开发从入门到入坟(1): 使用Popup仿造Android样式的Toast消息提示”

  • 小学生 says:

    求UWP bilibili 电脑操作优化

  • walterlv says:

    似乎获取窗口尺寸比屏幕尺寸靠谱一些吧?窗口尺寸用的是“有效像素”,屏幕尺寸是物理像素,单位不一样。

    • 暗影吉他手 says:

      GetScreenWidth就是调用的Window.Current.Bounds,只不过以前都是Mobile端代码,所以就起这个名。

Leave a Reply

World Line
Time Machine
Online Tools