众所周知,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)。初始化时还要创建消息队列和工作线程,用于按顺序提取并显示消息。
初始化相关代码如下:
{
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。
工作线程相关代码如下:
{
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();
}
调用
{
if (!string.IsNullOrWhiteSpace(message))
{
lock (_lockObj)
{
PopupMessageItem messageItem = new PopupMessageItem
{
IsMainWindow = isMainWindow,
Dispatcher = dispatcher,
Message = message
};
_messageQueue.Enqueue(messageItem);
_waitEvent.Set();
}
}
}
» 转载请注明来源及链接:未来代码研究所
求UWP bilibili 电脑操作优化
似乎获取窗口尺寸比屏幕尺寸靠谱一些吧?窗口尺寸用的是“有效像素”,屏幕尺寸是物理像素,单位不一样。
GetScreenWidth就是调用的Window.Current.Bounds,只不过以前都是Mobile端代码,所以就起这个名。