处理触控板和鼠标输入
Handle trackpad and mouse input
2020年6月23日
一句话判断
如果你的 App 有自定义的手势处理逻辑(比如自定义滚动、拖拽),你需要了解 iPadOS 的触控板事件是如何转换为 UIEvent 的,否则用户用 Magic Keyboard 操作时会出现各种奇怪的行为。
这场 Session 讲了什么
iPadOS 13.4 引入了触控板和鼠标支持,但很多开发者不知道的是:触控板的手势和手指触摸的手势在 UIKit 中的事件模型是不同的。Session 详细解释了触控板输入的事件链路:手指在触控板上滑动会产生 Scroll 和 Zoom 事件,通过 UIEvent 的 scrollWheel 和 magnify 类型传递。
Session 重点讲解了三个场景。第一,自定义滚动视图:如果你重写了 touchesBegan 等方法但没有处理 scroll 事件,触控板滑动就不会生效。第二,拖拽交互:UIDragInteraction 默认支持触控板的长按拖拽,但自定义的拖拽逻辑需要额外处理。第三,右键菜单:触控板的二指点击或鼠标右键会触发 UIContextMenuInteraction,你需要确保自定义的上下文菜单在所有输入方式下都正常工作。
值得深挖的点
事件响应链的差异性
手指触摸产生的是 UITouch 事件,走 touchesBegan/moved/ended 的响应链。触控板滑动产生的是 UIScrollWheelEvent,走 scrollWheel(with:) 的响应链。如果你的自定义视图只处理了前者,用户用触控板操作时会发现”滑不动”。解决方案是同时覆写 scrollWheel(with:) 方法,或者在 UIScrollView 的子类中确保不阻断触控板的滚动事件传递。
按键修饰符(Modifier Flags)
触控板和鼠标输入支持 Command、Option、Shift 等修饰键。通过 UIEvent 的 modifierFlags 你可以检测用户是否按住了某个修饰键,从而提供不同的交互行为。比如按住 Option 键拖拽可能是复制而不是移动。这是桌面级 App 的标配功能,iPad App 也应该支持。
代码片段
同时支持触摸和触控板的滚动
场景:自定义绘图视图支持触摸和触控板两种滚动方式。
class CanvasScrollView: UIScrollView {
// 触控板滚动事件
override func scrollWheel(with event: UIEvent) {
// 检查是否是触控板事件
if event.type == .scrollWheel {
let momentum = event.momentumPhase
switch momentum {
case .began:
// 触控板滑动开始,可能需要停止当前动画
layer.removeAllAnimations()
case .changed:
// 正在滑动,更新 contentOffset
super.scrollWheel(with: event)
case .ended:
// 滑动结束,系统会自动处理惯性滚动
super.scrollWheel(with: event)
default:
super.scrollWheel(with: event)
}
}
}
}
处理触控板的手势识别
场景:区分触控板点击和触摸屏点击。
class DrawingGestureRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
// 检查触摸来源
if let touch = touches.first {
switch touch.type {
case .direct:
// 手指直接触摸屏幕
handleDirectTouch(touch)
case .indirect:
// 触控板或鼠标的间接触摸
handleIndirectInput(touch)
case .pencil:
// Apple Pencil
handlePencilInput(touch)
default:
break
}
}
}
private func handleDirectTouch(_ touch: UITouch) {
// 手指触摸:较大的绘制笔触
state = .began
}
private func handleIndirectInput(_ touch: UITouch) {
// 触控板输入:精确的绘制
state = .began
}
private func handlePencilInput(_ touch: UITouch) {
// Apple Pencil:压感绘制
state = .began
}
}
支持修饰键的交互行为
场景:按住不同修饰键执行不同的操作。
class DragInteractionHandler: NSObject, UIDragInteractionDelegate {
func dragInteraction(_ interaction: UIDragInteraction,
itemsForAddingTo session: UIDragSession,
with touchAtPoint: CGPoint) -> [UIDragItem] {
// 检查修饰键状态
let modifiers = session.localContext as? UIEvent.ModifierFlags ?? []
if modifiers.contains(.command) {
// Command + 拖拽:创建快捷方式
return createShortcutDragItems()
} else if modifiers.contains(.option) {
// Option + 拖拽:复制
return createCopyDragItems()
} else {
// 普通拖拽:移动
return createMoveDragItems()
}
}
// 处理拖拽预览
func dragInteraction(_ interaction: UIDragInteraction,
previewForLifting item: UIDragItem,
session: UIDragSession) -> UITargetedDragPreview? {
return UITargetedDragPreview(view: interaction.view!)
}
}
正确处理右键上下文菜单
场景:同时支持长按和右键触发菜单。
class DocumentCollectionViewCell: UICollectionViewCell {
func setupContextMenu() {
let interaction = UIContextMenuInteraction(delegate: self)
addInteraction(interaction)
}
}
extension DocumentCollectionViewCell: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
// 无论长按还是右键,都会触发这个方法
return UIContextMenuConfiguration(
identifier: "document-menu" as NSCopying,
previewProvider: { nil },
actionProvider: { suggestedActions in
// 返回上下文菜单
return UIMenu(title: "操作", children: [
UIAction(title: "打开", image: UIImage(systemName: "doc")) { _ in
self.openDocument()
},
UIAction(title: "复制", image: UIImage(systemName: "doc.on.doc")) { _ in
self.copyDocument()
},
UIAction(title: "删除",
image: UIImage(systemName: "trash"),
attributes: .destructive) { _ in
self.deleteDocument()
}
])
}
)
}
}
最佳实践
已有项目:如果你有自定义的手势识别器或触摸处理逻辑,在 iPad 上用 Magic Keyboard 和 Magic Mouse 测试一遍。重点检查滚动、缩放、右键菜单是否正常工作。UITouch.type 属性可以帮你区分触摸来源,对不同类型做差异化处理。
新项目:使用 UIGestureRecognizer 的标准子类来处理交互,它们已经内置了触控板支持。只在需要高度自定义的交互场景才覆写底层触摸方法。始终使用 UIContextMenuInteraction 来提供上下文菜单,不要自定义长按菜单。
还有什么值得关注
UIScrollView.isDirectionalLockEnabled在触控板操作时可以锁定滚动方向。- 触控板的三指拖拽手势可以通过辅助功能设置开启,你的 App 不需要额外处理。
- 在 iPadOS 14 中,按键事件可以通过 UIKeyCommand 更好地集成到菜单栏中。