【UWP】借助GestureRecognizer:一种滑动手势动效的实现

2023-11-10 11:10

本文主要是介绍【UWP】借助GestureRecognizer:一种滑动手势动效的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

播放器项目中存在设计:播放详情视图 PlaybackDetailView 单独写成 Page 嵌套在一个 Frame 中浮动在根视图的NavigationView 上层。(此前试着用过页面导航的方式切换,但效果较差,且不易与 NavigationView 的历史导航结合使用,故放弃。)后通过 Microsoft 提供的 Windows Community Toolkit,使用 Microsoft.Toolkit.Uwp.UI.Animations 提供的ImplicitAnimations 实现了动画控制 Frame 的展开和关闭。代码如下:

xmlns:anim="using:Microsoft.Toolkit.Uwp.UI.Animations"<Frame Visibility="Collapsed"><anim:Implicit.ShowAnimations><anim:TranslationAnimation From="..." To="0" Duration="..." /></anim:Implicit.ShowAnimations><anim:Implicit.HideAnimations><anim:TranslationAnimation To="..." From="0" Duration="..." /></anim:Implicit.HideAnimations>
</Frame>

其中两个 TranslationAnimation 中的 From / To 值采用数据绑定,在窗口调整时变为窗口尺寸对应的数值。

本次更改需求如下:Windows 11更新了大量对平板设备的支持,笔者也在项目各处优化触摸屏设备的操作,因此想在此处加入手势处理,在触摸屏设备上实现下滑收起面板的功能(,让手里那块Surface Pro 6发光发热)。

相关资料

首先当然还是 UWP 官方文档。这里注意:Windows 11发布后微软美国官网开发者板块立即上线了相关支持,原来的UWP支持已弃用,新的UWP开发文档可直接参考Desktop板块:
美区开发者文档

而中国区仍是原来的样子:
中国区UWP文档

甚至当你通过 Windows 开发文档首页进入 Desktop 文档支持:
404的中国区Desktop文档

好了回归正题。对于手势识别,我们参考 Design and UI - Input and interactions - Touch 模块下的介绍。对于手势事件,文档里只有短短一句话:

For details about individual controls, see Controls list.

而所谓这个Controls list,包含的也不过是系统提供的一些控件(其中部分为WinUI控件)。所以到头来,微软并没有提供直接的手势事件供开发者使用。

那就只剩下其他两种手段了:Pointer events 和 Manipulation events。Pointer events 指针事件,咱们常用的PointerEnteredPointerPressedPointerReleased 等都归位指针事件,鼠标、触控板、触摸屏幕、触控笔都可以触发指针事件。而Manipulation events 操纵交互事件则应用于多点触控场景,或是我们需要追踪移动速度数据的时候。

GestureRecognizer

在 Bing 搜索关键词 UWP gesture,排序第一的即是本文探讨的核心:GestureRecognizer 。奇怪的是,微软并没有将此技术放在UWP文档交互相关的显眼位置,不知是因为什么原因(而且Windows 10 2004版本还对此进行了更新,当然也可能是笔者村通网)。

GestureRecognizer的使用逻辑很简单。它基于控件的指针事件实现用户手势的识别。使用时只需注册空间指针事件,并在其中调用GestureRecognizer的相关方法即可。大致流程如下:

using Windows.UI.Xaml.Input;

在类中定义 GestureRecognizer 对象, 命名为 _recognizer

private GestureRecognizer _recognizer;

假定控件名为 element, 在构造方法或 Loaded 处理方法中完成 _recognizer 的初始化, 并加入控件指针事件的处理。

_recognizer = new GestureRecognizer()
{GestureSettings = GestureSettings.ManipulationTranslateY// 此处为手势识别器需要识别的手势, 这里只需识别纵向滑动
};element.PointerPressed += OnPointerPressed;
element.PointerMoved += OnPointerMoved;
element.PointerReleased += OnPointerReleased;
element.PointerCanceled += OnPointerCanceled;

类中定义指针事件处理方法。对于手势处理, 我们只需捕获指针按下、释放、移动、取消的事件。如果需要限制输入设备,例如仅限触摸操作、笔操作响应手势,可以通过判断 args.Pointer.PointerDeviceType 完善以下代码:

void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{element.CapturePointer(args.Pointer);_recognizer.ProcessDownEvent(args.GetCurrentPoint(reference));
}void OnPointerMoved(object sender, PointerRoutedEventArgs args)
{_recognizer.ProcessMoveEvents(args.GetIntermediatePoints(reference));
}void OnPointerReleased(object sender, PointerRoutedEventArgs args)
{_recognizer.ProcessUpEvent(args.GetCurrentPoint(reference));element.ReleasePointerCapture(args.Pointer);
}void OnPointerCanceled(object sender, PointerRoutedEventArgs args)
{_recognizer.CompleteGesture();element.ReleasePointerCapture(args.Pointer);
}

接着在构造方法中完成手势事件的处理。

_recognizer.ManipulationUpdated += Recognizer_ManipulationUpdated;
_recognizer.ManipulationCompleted += Recognizer_ManipulationCompleted;

具体的实现待会儿再说。这里没有选择响应 CrossSliding 事件,是因为我们需要实时对用户手势做出响应,而不是等待手势完成后触发相关动画,那样做会造成视觉上的拖沓感。

主体实现

我们的 Frame 元素业务逻辑是从窗口底部向上推入,直至占满全屏。笔者的做法是在XAML中先将其 Visibility 设置为 Collapsed ,在用户第一次激活播放详情视图时将 Visibility 设置为 Visible ,并加载从窗口底部推入的动画。动画主体使用 CompositeTransform 实现,XAML代码如下:

<Frame Visibility="Collapsed"><Frame.RenderTransform><CompositeTransform /></Frame.RenderTransform>
</Frame>

对于 Frame 的展开和收起,我们在页面类定义公开方法 SetPlaybackDetailFrameVisibility

// 由于第一次展开 Frame 后不再对 Frame 的 Visibility 做任何处理, 我们直接采用一个私有变量存储面板的开闭状态
private bool _playbackDetailFrameOpened = false;// 该方法同时用于播放详情页 “返回” 按钮的调用, 主页面导航至新页面后的详情视图折叠, 以及前文 Manipulation 事件的自动状态校正
public async void SetPlaybackDetailFrameVisibility(bool IsVisible)
{// 避免公开方法被非 UI 线程调用, 直接采用 Dispatcher 执行相关代码await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>{// 当播放器中未加载歌曲时打开也没意义if (IsVisible == true && PlaybackEngine.Current.CurrentItem != null){PlaybackDetailFrame.Visibility = Visibility.Visible;var openStoryboard = new Storyboard();var openAnimation = new DoubleAnimation();// 此处 DoubleAnimation 的定义和XAML不同的是其操作对象直接引用, 而不是采用 TargetName 的方式Storyboard.SetTarget(openAnimation, PlaybackDetailFrame);Storyboard.SetTargetProperty(openAnimation, "(UIElement.RenderTransform).(CompositeTransform.TranslateY)");openAnimation.To = 0;openAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));openStoryboard.Children.Add(openAnimation);openStoryboard.Begin();}else{var foldStoryboard = new Storyboard();var foldAnimation = new DoubleAnimation();Storyboard.SetTarget(foldAnimation, PlaybackDetailFrame);Storyboard.SetTargetProperty(foldAnimation, "(UIElement.RenderTransform).(CompositeTransform.TranslateY)");// 该方法在 Page 内定义, ActualHeight 为页面的实际高度foldAnimation.To = ActualHeight;foldAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));foldStoryboard.Children.Add(foldAnimation);foldStoryboard.Begin();}// 展开 / 收起页面时分别注册 / 注销页面的相关事件, 以优化浏览时的占用// 完成状态记录的切换if (IsVisible == true && PlaybackEngine.Current.CurrentItem != null && !_playbackDetailFrameOpened){PlaybackDetailView.CurrentPageInstance.PlaybackEngineEventRegister();_playbackDetailFrameOpened = true;}else if (IsVisible == false && _playbackDetailFrameOpened){PlaybackDetailView.CurrentPageInstance.PlaybackEngineEventUnregister();_playbackDetailFrameOpened = false;}});
}

其实也可以优化一下。

private bool _playbackDetailFrameOpened = false;public async void SetPlaybackDetailFrameVisibility(bool IsVisible)
{await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>{bool visibilityResult = IsVisible && PlaybackEngine.Current.CurrentItem != null;if (visibilityResult) PlaybackDetailFrame.Visibility = Visibility.Visible;var translationStoryboard = new Storyboard();var translationAnimation = new DoubleAnimation();Storyboard.SetTarget(translationAnimation, PlaybackDetailFrame);Storyboard.SetTargetProperty(translationAnimation, "(UIElement.RenderTransform).(CompositeTransform.TranslateY)");translationAnimation.To = visibilityResult ? 0 : ActualHeight;translationAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(300));translationStoryboard.Children.Add(translationAnimation);translationStoryboard.Begin();if (visibilityResult && !_playbackDetailFrameOpened){PlaybackDetailView.CurrentPageInstance.PlaybackEngineEventRegister();_playbackDetailFrameOpened = true;}else if (!visibilityResult && _playbackDetailFrameOpened){PlaybackDetailView.CurrentPageInstance.PlaybackEngineEventUnregister();_playbackDetailFrameOpened = false;}});
}

窗口的 SizeChanged 事件处理中加上

if (!_playbackDetailFrameOpened) (PlaybackDetailFrame.RenderTransform as CompositeTransform).TranslateY = e.NewSize.Height;

播放详情展开时无需调整,收起时总放置在窗口底部。

手势事件的处理

回头处理前面两个事件的响应方法。

ManipulationUpdated 事件中,首先获取控件当前的 Translation (为保证统一逻辑,未使用 UIElement.Translation 属性),与操纵交互更新的增量 Delta 相加得到一个新的结果。为了将控件限制在可操作范围内,将结果(TranslationY)限制在 0ActualHeight 之间(正常情况下后者的溢出应该不会发生,但是保险起见,you know)。最终应用这个结果到 CompositeTransform 上。

private void Recognizer_ManipulationUpdated(GestureRecognizer sender, ManipulationUpdatedEventArgs args)
{double destY = (PlaybackDetailFrame.RenderTransform as CompositeTransform).TranslateY + args.Delta.Translation.Y;if (destY <= 0) destY = 0;if (destY >= ActualHeight) destY = ActualHeight;(PlaybackDetailFrame.RenderTransform as CompositeTransform).TranslateY = destY;
}

ManipulationCompleted 则简单得多。获取手势操作完成后 TranslationY 的值,与我们设定的阈值比较,完成手势操作动画即可。注意:这里直接调用了 SetPlaybackDetailFrameVisibility 。与之对应的是,我们在该方法的 DoubleAnimation 中未指定 From 的大小。也就是说,我们的动画是从 Frame当前位置开始的。

private void Recognizer_ManipulationCompleted(GestureRecognizer sender, ManipulationCompletedEventArgs args)
{double destY = (PlaybackDetailFrame.RenderTransform as CompositeTransform).TranslateY;SetPlaybackDetailFrameVisibility(destY <= ActualHeight * 0.4);
}

至此,手势操作全部完成。

浮动层指针事件的处理

播放详情视图作为浮动层,其中同样包含了诸多元素,用户在这些控件上操作时应该是这些控件发生响应,而手势识别不应触发。因此我们需要在浮动层指针路由事件的响应方法中将 Handled 设置为 True。例:笔者的项目中歌词滚动面板的 ScrollViewer 容器响应 DirectManipulationStartedDirectManipulationCompleted 事件,而这两事件均非 PointerRoutedEvent 。无法设定其 Handled 状态。考虑到 Windows 平台上大多数形式的交互均与指针事件有关,我们尝试在播放详情页构造方法中添加该 ScrollViewerPointerPressed 事件的响应,直接将 Handled 设置为 True 。经尝试方案可行。

失败的尝试

在使用 Storyboard 之前,根据先前的思路,尝试过 Microsoft.Toolkit.Uwp.UI.Animations 提供的 TranslationAnimation ,大体方向没问题,但实现结果一团乱,直接放弃,采用了传统的实现方式。

结语

有事没事钻一钻 Documentation,还挺有意思。还有,交互不好做啊。最后,希望UWP能在Windows 11的时代接着商店盘活的势头再度起来。

这篇关于【UWP】借助GestureRecognizer:一种滑动手势动效的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/382281

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja