Touch Bar 上一个图标要承担三种操作:切换、隐藏、退出。看起来只是三个手势,但手势的边界情况最多,也最容易做出“手感不对”的软件。

这一篇讲三个手势各自怎么设计,以及其中一个原计划没做成的功能。

单击和双击:第一下不等第二下

常见的做法是等一小会儿,看有没有第二下再决定是单击还是双击,代价是每次单击都会延迟零点几秒。DockTouchBar 反过来:第一下照常切换,不等待;如果同一个图标在很短的时间内再点一次,第二下就隐藏它。双击的净效果是“切过去再藏起来”,而单击没有任何延迟。

双击的第二下会稍等一点点再执行隐藏。原因是第一下刚发出激活请求,如果目标 App 在隐藏之后才处理这个请求,它会又被显示出来。

长按:可以反悔的进度条

长按用来退出 App,这是最危险的操作,所以设计成“可以反悔”。按住约零点三五秒之后,图标变暗,下方出现一条红色进度条,走满才真正退出;中途松手,就当作一次普通单击;手指滑开,就当作是在滚动列表,取消。时长可以在菜单里选 1、2、3、5 秒,也可以关掉。

技术上,长按是挂在 NSScrubber 上的 NSPressGestureRecognizer,只接受直接触摸,并且和 scrubber 自己的滑动、点按同时识别,不去抢它的触摸。访达不能被退出,所以长按访达不会出现进度条。

边界情况:写文档时对照代码才发现的

第一个边界:长按中途松手时,NSScrubber 不一定会回调选中,因为手势已经被识别了。如果不处理,就会出现“按得稍久一点就点不动”。解法是松手后等一小会儿,没收到回调就自己补一次点击。

第二个边界是写文档、对照代码时才发现的:长按退出一个没有固定在 Dock 里的 App,它的图标会消失,后面的图标往前挪,正在进行的按压记录随之作废。松手时手指下面已经是相邻的另一个图标,有可能被误当成点击。修复是加了一个“手指抬起之前忽略点击”的标记。

手感参数:都很小,都为了同一件事

手感靠几个很小的数字决定:按住 0.35 秒之后才开始出现长按进度条,比这更短的按压都当作点击;双击的两下之间最多隔 0.35 秒;手指移动超过 10 个点,就认为是在滑动,取消长按;松手之后等 50 毫秒,没收到选中回调再补一次点击;长按完成之后,直到手指抬起再过 0.3 秒,都忽略选中回调。

这些数字每一个都是为了同一件事:让误触更少,让反悔更容易。它们不是一次定准的,而是在使用中一点点调出来的。

手势之外:反馈和防误触

点击时图标会短暂高亮一下,这个反馈由图标视图自己来画,而不依赖 scrubber 的选中态,因为选中态每次都要立刻清掉,连续点同一个图标才能再次触发。只有正在运行的 App 才会出现长按进度条,没在运行的图标,松手后照常当作点击。

当 App 启动或退出导致图标位置变了,正在进行的长按会被作废,避免退出错的 App。另外整个软件只允许运行一个实例,两个实例会互相抢 Touch Bar,后启动的那个会自己退出。

把三个手势都做成可开关、可调时长,是为了让每个人可以把它收得更简洁:不喜欢双击隐藏的人关掉它,双击就等于两次单击;不想要长按退出的人把时长设成“不启用”,长按就只算单击。

没做成的:双击最小化

双击原本的需求是“缩小”,也就是最小化窗口。实测发现:开着台前调度(Stage Manager)时,用辅助功能把窗口的 AXMinimized 设为 true,接口返回成功,窗口却纹丝不动;改成按窗口上的最小化按钮,同样返回成功、同样无效;换成系统自带的计算器,结果一样。

最后改成隐藏,也就是等同 ⌘H。它在任何设置下都有效,也不需要任何权限,效果和“缩小”很接近。这里还有一个小细节:隐藏接口的返回值不可靠,窗口明明已经隐藏了,它仍然可能返回失败,所以代码不看返回值。如果以后要支持最小化,只能针对关闭了台前调度的系统做成可选项。