大多数浏览器和
Developer App 均支持流媒体播放。
-
应对触控板和鼠标输入
在你通过触控板和鼠标间接输入这样的方式优化 iPad 或 Mac Catalyst app 时,可提供更加通用的体验。了解如何使你的 app 响应这些设备中的新事件。学习如何处理指针移动,启用指针锁定,处理滚动输入和触控板手势,以及接受或拒绝手势识别器上的事件。我们还将向你展示如何实现高级功能,例如使用键盘修改器或指点设备按钮来更改手势行为以便取悦专业用户,并为你的 app 带来更丰富的体验。 为了进一步了解有关基于指针的交互的更多信息并充分利用本次会议,我们建议你观看“为 iPadOS 指针建模”,“将键盘和鼠标游戏带入 iPad ”和“支持在 app 中使用硬件键盘”。
资源
相关视频
WWDC22
WWDC20
WWDC19
-
下载
(你好 WWDC 2020) 你好 欢迎来到 WWDC (应对触控板和鼠标输入) 我是 Steve Moseley 一名 UIKit 工程师 欢迎观看《应对触控板和鼠标输入》讲座 在本视频中 我们会讲解 如何让你的 app 更灵敏地响应间接输入机制 比如触控板和鼠标等 我们在 macOS Catalina 和 iPadOS 13.4 中引入了相关内容 有适用于所有 app 的一些普通更新 也有一些高级更新 供想要再进一步的 app 使用
关于普通更新 我们会讲解如何处理光标活动 锁定光标、处理滚动输入和触控板手势
关于高级更新 我们会讲解如何处理 按钮遮盖、键盘修改器 用全新 UIGestureRecognizer 和 UIGestureRecognizer- Delegate 方法接受或拒绝事件 区分触摸和间接输入设备 以及通过 info.plist 文件中的 键加入新功能
让我们先看看普通更新吧 (iPad Pro 你的下一台电脑 何必是电脑) 有了鼠标或者触控板 用户希望在不触摸屏幕的情况下 与你的 app 进行互动 请注意看 当光标移动到指定区域时 Safari 浏览器如何显示工具栏 或者当光标移动到标签页时 它如何显示标签页关闭按钮 Safari 浏览器通过 UIHoverGesture- Recognizer 来响应光标活动
UIHoverGestureRecognizer 是和 Mac Catalyst 一起在 Catalina 上推出的 它现在可以在 iPadOS 上使用了 它是一个普通的手势识别器 可以像在 Mac 上一样在 iPad 上运行 如果你要覆写 UIApplication.sendEvent 你会发现它现在由一个全新的 事件类型驱动 即 EventType.hover
你可以通过目标和操作实例化 UIHoverGestureRecognizer 就和你实例化其他手势识别器一样 在操作回调里 你会切换手势状态 然后执行相应的操作
请注意 手势状态“开始” 会映射到进入你手势视图范围内的光标
“结束”则会映射到 离开你手势视图范围的光标 在这里 我们会根据光标是否进入视图 来展现视频回放控件
如果你仔细看一下 iPad 或者 Mac Catalyst app 上的触摸操作 就会发现我们新增了几个阶段 来追踪光标活动
这些阶段会映射到窗口内的整体光标活动 RegionEntered 代表光标进入了窗口 RegionMoved 代表光标在窗口内 但还没有进行点按 RegionExited 代表它离开了窗口
请注意 这些阶段并总是和我们刚才说的 UIHoverGestureRecognizer 的 手势状态保持一致 UIHoverGestureRecognizer 状态 只会映射到你手势视图范围内的光标活动 而这些阶段则会映射窗口内的光标活动 用 UIHoverGestureRecognizer 来响应光标活动 或者隐藏和展现内容 就像我们刚才看到的 Safari 浏览器示例
请不要用它来更改光标的外观 或者应用悬停效果 你要是想这么做的话 请使用 UIPointerInteraction
如需了解 UIHoverGesture- Recognizer 更多信息 请观看 我们去年的视频 《iPad App 移植到 Mac 迈向新阶段》 如果你想更改光标的外观 请观看《构建 iPadOS 光标》讲座
除了响应光标活动外 有些 app 比如游戏会想要锁定光标活动 在 iPadOS 14 和 Mac Catalyst Big Sur 上 我们推出了新的 API 帮你实现上述目的 非常容易使用
用 UIViewController 设置锁定偏好 然后通过全新的 UIPointerLockState 观察解析值 就是这么简单 光标是共享资源 所以最终系统会决定是否要锁定光标 这意味着你设置的光标锁定状态 并不一定会应用 让我们看看这两个 API 是如何配合的
你的视图控制器设置 prefersPointerLocked 值为“真” 只要满足某些要求 系统也会把你的场景锁定值设置为“真” 场景的 UIPointerLockState 会反映该状态
如果你想要展现内容 禁用锁定的话呢? 打个比方 你的游戏提示网络错误 然后你想显示 UIAlertController 用户希望用光标与该内容进行互动 就像他们在系统其他部分中互动一样
没问题 默认的 prefersPointerLocked 值 是“假” 当 UIAlertController 显示时 系统会观察其 prefersPointerLocked 值 然后禁用光标锁定
显示或关闭视图控制器时 场景的光标锁定值会自动更新 也就是说你不用一直注意这个状态
如果你想在场景中锁定光标 只需覆写视图控制器的 prefersPointerLocked 属性 把它改为“真”
如果你想禁用或更改这个值 只需调用 setNeedsUpdate- OfPrefersPointerLocked
如果 app 的某一部分 需要查看当前锁定状态 你可以从场景中获取光标锁定状态 然后查看 isLocked 属性 这里 我们有个对象想要在它场景的 光标锁定状态改变时收到通知
它会获取 pointerLockState 对象 然后注册观察该对象
当 UIPointerLockState. didChangeNotification 发布之后 会执行闭包 然后 UIPointerLockState 的 isLocked 值 会被传送至 app 的另一部分
如图表所示 你的场景需要满足某些要求 才能让你的偏好光标锁定值被采用 每个平台的要求都不一样 我们先从 iPadOS 开始说起
首先 你的场景必须是全屏 这意味着 app 不能是分屏浏览或侧拉模式 这也意味着侧拉模式下不可以有其他 app
这里的“全屏”不是指 使用 UIRequiresFullScreen info.plist 键 而是指你的场景必须占据整个屏幕
接下来 你的场景必须处于 foregroundActive 激活状态 这意味着它不会因任何原因而停用 比如当控制中心或通知中心出现时
在 Mac Catalyst 里 你的 app 必须是最前端的 app 才可以让系统采用你的 prefersPointerLocked 值
如果你同时有多个窗口 你想要锁定光标的那个窗口 就应该移到最前端
如果你的 app 没有满足这些要求 那么光标锁定就会被禁用 在 iPadOS 上 如果有侧拉 app 或者在 macOS 上 你的 app 不是在最前端的位置 isLocked 就会变成“假” 然后你会通过 UIPointerLockState- DidChangeNotification 收到通知
不过你无需进行任何操作来重新锁定光标 系统会持续地评估这些要求 当情况改变时 你场景里的光标锁定状态也会随之改变 你无需调用 setNeedsUpdate- OfPrefersPointerLocked
请记住 光标锁定与否是由系统决定的 这些要求是会改变的 并且会受到用户操作的影响 所以你的 app 不应该认为 你的 prefersPointerLocked 值 会一直被系统所采用 你应该始终观察 isLocked 的变化 并在 app 中采取相应的行动来响应改变
最后 光标锁定并不适用于所有场景 对于不适用的场景 UIScene 上的 pointerLockState 属性会返回“零” 表示锁定不可用 当锁定光标时 你也需要注意触控板和鼠标的相关活动
欲知更多信息 请观看《为 iPad 游戏 加入键盘和光标操作》讲座
让我们来谈谈如何处理滚动输入吧
请务必确保 app 的所有区域 都能够正确响应关联的指向设备 如果用户能够通过一个手指进行平移 那他们就会认为 也能通过两个手指 或鼠标滚轮平移相同的内容
这里我们可以看到控制中心的自定义控件 已经进行了更新 允许在触控板上用两指轻扫进行平移
你可以通过更新 UIPanGestureRecognizer 的 allowedScrollTypesMask 属性 来处理滚动输入 直接输入你想要处理的滚动类型即可 这会为你的手势 启用 EventType.scroll 支持
UIScrollView 的平移手势识别器 更新了 allowedScrollTypesMask 来处理各种类型的滚动输入 但是标准的 UIPanGesture- Recognizers 在默认情况下是没有遮盖的 因此 你会希望 为 app 的平移手势更新这项属性
打个比方 你有一个 app 在主视图的一侧隐藏了内容 用户可以通过横向轻扫来显示该内容 它通过平移手势来实现 你的设计师认为 用滚轮来显示内容并不是很自然 所以你只会让该手势支持持续滚动类型
为此 将平移手势的 allowedScrollTypesMask 更新成 UIScrollTypeMaskContinuous 即可 也许你的 app 支持自定义下拉刷新互动 它也是通过平移手势实现 对于该手势 你可能希望它响应所有滚动输入类型 为此 直接把它的 allowed- ScrollTypesMask 属性 更新成 UIScrollTypeMaskAll 即可
处理触控板的捏合和旋转手势就更简单了 只需使用 UIPinchGestureRecognizer 和 UIRotationGestureRecognizer 为确保每款 app 都可以处理这些间接手势 这些识别器采用了兼容模式 默认情况下 它们是由模拟手势的触摸来实现 UIKit 以固定距离创建了这些触摸 并根据触摸表面的活动来模拟它们的活动
从 iPadOS 13.4 和 macOS Catalina 10.15.4 起 app 可以将这些手势移出兼容模式 并响应新的事件类型 即 EventType.transform
此事件类型直接来自输入设备 它允许精准捏合手势和旋转手势 正如用户所期望的一样
要想获取这项新的事件类型 并把这些手势移出兼容模式 你必须在 app 的 info.plist 文件中加入一个键 稍后我们会详细讲解
好处是 这些情况都不需要用到额外的代码 无论有没有该键 UIPinchGestureRecognizer 和 UIRotationGestureRecognizer 都知道如何处理这类输入 而当你确实 在 app info.plist 文件中加入该键时 就不需要编写代码来处理新的事件类型了 这样就可以了
但请注意 如果你采用了该 info.plist 键 在触控板输入时 触摸操作就不会触发这些手势了 在这种情况下 numberOfTouches 会返回零 而 locationOfTouch 和视图则可能会提示异常
以上是适用于所有 app 的普通更新
下面我们继续介绍一些高级更新 为你的专业用户打造惊喜和愉快的体验 按钮遮盖和修饰键 是为 app 添加高级功能的绝佳方式 上下文菜单使用按钮遮盖 来识别两指轻触或辅助点按 以便提供更简洁的用户界面 Numbers 表格使用修饰键 因此我可以用光标 和 Shift 修饰键同时选择多行 就和用手指一样
UIEvent.ButtonMask 是 iOS 上的新类型 它是在用指向设备点击时按下的按钮组合
它在 UIEvent 和 UIGesture- Recognizer 上都显示为 buttonMask 让你轻松地只响应设备的首要按钮 创建响应两指轻触 和辅助鼠标按钮的功能 或者设置高位数鼠标按钮 请注意 UIGestureRecognizer 上的按钮遮盖 来自上一个处理事件
如果你想通过简单的方式 在开始前请求特定的按钮遮盖 我们已经通过 buttonMaskRequired 更新了 UITapGestureRecognizer 你只需要给它一个按钮遮盖就行了 ButtonMask 上甚至还有一个便利函数 可以为高位数按钮返回合适的遮盖值 和 buttonMaskRequired 一起使用 把高位数按钮作为 app 高级功能的 加速键就会变得很容易
如果你用过 UICommand 或 UIPointerInteraction 对 UIKeyModifierFlags 就会比较熟悉 它是在事件中被按下的一组键盘修饰键 我们把 UIKeyModifierFlags 作为 modifierFlags 引入到了 UIEvent 和 UIGestureRecognizer 中 可以在手势回调时使用这项属性 改变响应事件的方式 例如 在 Safari 浏览器中 当你按住 Command 键并点击链接时 链接就会在新标签页中打开 和 buttonMask 一样 UIGesture- Recognizer 的 modifierFlags 是由上一个处理事件填充的
如需了解 如何提升 app 键盘体验的更多信息 请观看《在 app 中支持物理键盘》讲座
按钮遮盖和修饰键使用起来很容易 设置第三方鼠标按钮 与使用 buttonMask.button 获取合适的遮盖 和在 UITapGestureRecognizer. buttonMaskRequired 上 设置结果一样容易 就是这么简单
让我们回到先前的 UIHoverGestureRecognizer 示例 看看修饰符标记的工作原理 刚才我们看到了每当光标进入手势视图时 视频回放控件出现的方式 如果我们希望每当 UIKeyModifierAlternate 被按下时 选择性地显示 chapter-selection 控件 我们只需检查 modifierFlags 是否包含该值 当按钮遮盖和修饰符标记 与 UIGestureRecognizerDelegate 和 UIGestureRecognizer 子类的 新 API 结合到一起时 它们就会变得特别强大 这些方法只会根据 你手势处理的事件而调用 因此 UIPinchGestureRecognizer 不 会收到有关 EventType.scroll 的询问 有了这些方法 你可以根据 按钮遮盖、修饰符标记或其他属性 来接受或拒绝那些事件 请注意 这些方法在 手势完全处理完事件之前使用 因此 UIGestureRecognizer 的 buttonMask 和 modifierFlags 属性 不包括事件中发现的新值 如果你想审查这些方法中的任一属性 请注意 UIEvent 上的值 而不是 UIGestureRecognizer 上的值 像 UIPanGestureRecognizer 和 UI- PinchGestureRecognizer 这样的手势 会响应多种基于非触摸操作的事件 因此 你应该把类似 gestureRecognizer shouldReceive 触摸 这类方法中任何和事件有关的代码 迁移至上述两种新方法之一
下面我们通过一些例子 介绍如何在 app 中使用该方法 我们有一个 UIGestureRecognizer 子类 它只想要接收 带有辅助 buttonMask 的事件 你可能会为专门由两指轻触 或辅助鼠标按钮点击驱动的功能 进行这样的设置
可以先从覆写手势子类方法 shouldReceiveEvent 开始 对于该方法 你只需检查事件上的 buttonMask 是否完全等同于辅助点按 如果是 我们就接受该事件 如果不是 我们就拒绝 正如先前所说 虽然 UIGesture- Recognizer 上也有 buttonMask 可我们不应该考虑此方法中的这项属性 shouldReceiveEvent 在手势 完全处理完事件之前发生 因此 UIGestureRecognizer 的 buttonMask 在这个时候并不是最新的
普遍来讲 允许点按和 Control 修饰键 执行相同操作作为辅助点按 我们也可为此更新我们的示例 只需更改我们的 shouldReceive 方法即可 首先 确定 buttonMask 是否恰好是首要的
如果是 我们就查看 modifierFlags 是否等同于 UIKeyModifierControl
如果是辅助点按或 Control 点按 我们会接受该事件 如果不是 我们就拒绝
下面我们来看下之前的视频示例 我们想在视频上添加另一个悬停手势 来显示隐藏式字幕控件 目前用户可以从设置菜单中开启这项功能 但我们想通过提供修饰键和悬停手势 来让他们更快速地打开字幕
我们可以像之前那样 实例化 HoverGestureRecognizer
这次我们会把自己设置为委托 并且实施 gestureRecognizer shouldReceive 事件方法
在该方法中 当按下 UIKeyModifier- Alternate 时 我们会接受事件 如果没有按下 我们就会拒绝该事件
如果你在考虑改进 app 的光标支持 那么你也许想辨别来自指向设备的触摸 和来自手指的触摸 尤其当你的 app 中 有很多自定义点击测试代码时 那你就应该好好考虑这点
由于光标比手指更精准 你可以减少这些 触摸的扩展点击测试区域 来提供更精细的体验 来自指向设备的触摸 新增了全新的 indirectPointer TouchType 如果你选择加入 UIApplication- SupportsIndirectInputEvents 这一 info.plist 键
你可以用现有的 API 使用该触摸类型 比如 UIGestureRecognizer. allowedTouchTypes 这样你就可以获得只响应光标点按的手势 或者获得只响应基于手指触摸的手势
下面我们来详细介绍 UIApplication- SupportsIndirectInputEvents 这是添加到 app info.plist 文件中的 Boolean 键 启动光标互动 按钮点按、滚动输入 或触控板手势都不需要该键 有没有该键都不会影响这些功能 它只是需要用来获取 新的间接光标触摸类型 还有 EventType.transform
现有项目没有这个键 所以我们需要添加它 从 iOS 14 和 macOS Big Sur SDK 开始 在新的 UIKit 和 SwiftUI 项目中 这个值设为“真” 在未来的版本中 默认值会改变 我们也不再需要参考这个键的值 让我们看看该键的存在与否 会有什么事情发生吧
如果你把 UIApplication- SupportsIndirectInputEvents 想象成退出兼容模式的话会很有帮助 我们之所以添加这个兼容模式 是为了让用户在 iPadOS 13.4 上的 间接输入初体验变得更好 如果像其他现有项目一样 此键不存在的话 你的 app 就会处于兼容模式
来自指向设备的点按是 TouchType.direct 其他基于手指的触摸也是一样 所以你无法区分两者 触控板上的捏合手势和旋转手势 会触发模拟手势的触摸 这可能会不小心激活其他手势
如果该键存在并且被设为“真”的话 你的 app 就会退出兼容模式 并且启用新的功能 来自指向设备的点按是 TouchType.indirectPointer 通过它你可以为精准指向设备 设置和改变功能 触控板上的捏合手势和旋转手势 会直接从输入设备 触发新的事件类型 即 EventType.transform 这会带来精准的捏合和旋转手势 它们不会意外激活其他识别器
有了此键 你就在 iPadOS 和 Mac Catalyst 上完全踏入了 间接输入的新世界 但有几件事需要注意 新的事件类型比如 EventType.scroll 或者 EventType.transform 不是基于触摸的 所以在使用与触摸有关的 手势识别器 API 时需要注意 当 UIPanGestureRecognizer UIPinchGestureRecognizer 或者 UIRotationGestureRecognizer 由这些新的事件类型驱动时 numberOfTouches 会返回零 而 locationofTouchInView 可能会提示异常
此外 请注意 在手势的 shouldReceivetouch 委托方法中的任何代码 不会在受这些事件驱动时运行 当你加入此键后 UIPinchGestureRecognizer 和 UIRotationGestureRecognizer 会从兼容模式中移除 任何在此模式下偶然激活的手势 都不会再被触发了 在这个全新的间接输入世界中 手势会响应多种事件类型 正因如此 你可能会发现 找出手势识别器响应的事件会非常有用 你可以使用我们先前提到的 shouldReceive 方法帮助你找到该事件 当响应 EventType.touches 时 你可以使用 numberOfTouches 或 locationofTouchInView 等 API 如果你响应的是其他事件 就应该避免使用这些方法 你可以通过几个简单的方法 用触控板和鼠标输入 来让你的 app 更出色 (更新你的 app) 为平移手势启用滚动输入 通过隐藏或展现内容来响应光标活动
把 info.plist 键添加到 app 中 以获取新的 TouchType 和 EventType 它可以让你为基于光标的触摸自定义功能 并且让你的 app 拥有 更精准的捏合和旋转手势
使用新的事件属性和手势识别器 API 提供按钮点按和键盘修饰键的替代响应 进而打造用户满意的体验 谢谢观看 我非常期待你更新后的 app!
-
-
1:49 - UIHoverGestureRecognizer
let controlsHover = UIHoverGestureRecognizer(target: self, action: #selector(handleHover)) @objc func handleHover(_ recognizer: UIHoverGestureRecognizer) { switch recognizer.state { case .began: // Pointer entered our view - show controls self.showsPlaybackControls = true case .ended: // Pointer exited our view - hide controls self.showsPlaybackControls = false default: break } }
-
5:33 - prefersPointerLocked
class GameViewController: UIViewController { var shouldLockPointer: Bool = true override var prefersPointerLocked: Bool { return self.shouldLockPointer } func disablePointerLock() { self.shouldLockPointer = false self.setNeedsUpdateOfPrefersPointerLocked() } }
-
5:53 - UIPointerLockState.isLocked
if let pointerLockState = self.window.windowScene?.pointerLockState { self.observer = notificationCenter.addObserver(forName: UIPointerLockState.didChangeNotification, object: pointerLockState, queue: OperationQueue.main) { (note) in guard let lockState = note.object as? UIPointerLockState else { return } gameEngine.performExpensiveOperationWhile(lockState.isLocked) } }
-
9:54 - UIPanGestureRecognizer.allowedScrollTypesMask
// Enable scroll input for touch surface devices self.drawerPan.allowedScrollTypesMask = [.continuous] // Enable scroll input for scroll wheel devices as well self.pullToRefreshPan.allowedScrollTypesMask = [.all]
-
14:48 - Requiring a 3rd mouse button click
self.thirdMouseButtonTap.buttonMaskRequired = .button(3)
-
15:07 - Changing response for .alternate keyboard modifier
func handleHover(_ recognizer: UIHoverGestureRecognizer) { // Show chapter controls if alt is pressed let showChapterControls = recognizer.modifierFlags.contains(.alternate) // ... }
-
16:38 - Only handle secondary clicks
class SecondaryClickGesture: UIGestureRecognizer { override func shouldReceive(_ event: UIEvent) -> Bool { // Must look at the event’s mask, not the gesture’s return event.buttonMask == .secondary } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { // Touch handling code ... } }
-
17:36 - Only handle secondary clicks or control clicks
class SecondaryClickGesture: UIGestureRecognizer { override func shouldReceive(_ event: UIEvent) -> Bool { // Must look at the event’s properties, not the gesture’s let secondaryClick = event.buttonMask == .secondary let controlClick = event.buttonMask == .primary && event.modifierFlags == .control return secondaryClick || controlClick } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { // Touch handling code ... } }
-
18:10 - Only receive hover events with the .alternate modifier pressed
let ccHover = UIHoverGestureRecognizer(target: self, action: #selector(handleClosedCaptionHover)) ccHover.delegate = self func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool { if gestureRecognizer == self.closedCaptionHover { return event.modifierFlags.contains(.alternate) } return true }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。