iOS9 中的高级触摸处理:合并和预测

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

即使您的应用程序以每秒 60 帧的速度流畅运行,高端 iOS 设备的触摸扫描频率为 120 赫兹,并且使用标准的开箱即用触摸处理,您也很容易错过用户一半的触摸动作。

幸运的是,Apple 在 iOS 9 中为 UIEvent 引入了一些新功能,以减少触摸延迟并提高时间分辨率。有一个演示应用程序与帖子一起使用,您可以在 我的 GitHub 存储库 中找到它。

触摸合并

第一个特性是触摸合并。如果您的应用正在使用重写的 touchesMoved() 对触摸进行采样,那么您将获得的最大触摸采样数是每秒 60 次,如果您的应用正在考虑在主线程上进行一些奇特的计算,那么它很可能少于那。

触摸合并允许您访问在 touchesMoved() 调用之间可能发生的所有中间触摸。例如,当您只收到一个 UIEvent 时,这允许您的应用程序绘制由六个点组成的平滑曲线。

语法非常简单。在我的演示应用程序中,我有一些 CAShapeLayer 实例可供我使用。第一层(mainDrawLayer,包含每个顶点都有圆圈的蓝线)是使用来自 touchesMoved() 的主 UIEvent 的信息绘制的:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first, event = event else
    {
        return
    }

    let locationInView = touch.locationInView(view)

    mainDrawPath.addLineToPoint(locationInView)
    mainDrawPath.appendPath(UIBezierPath.createCircleAtPoint(locationInView, radius: 4))
    mainDrawPath.moveToPoint(locationInView)

    mainDrawLayer.path = mainDrawPath.CGPath
    [...]


如果您想知道 createCircleAtPoint() 方法从何而来,这是我写给 UIBezierPath 的一个小扩展,它返回给定点的圆路径:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first, event = event else
    {
        return
    }

    let locationInView = touch.locationInView(view)

    mainDrawPath.addLineToPoint(locationInView)
    mainDrawPath.appendPath(UIBezierPath.createCircleAtPoint(locationInView, radius: 4))
    mainDrawPath.moveToPoint(locationInView)

    mainDrawLayer.path = mainDrawPath.CGPath
    [...]



如果用户在屏幕上移动手指的速度足够快,他们会看到一条锯齿状的线条,这可能不是他们想要的。为了访问中间触摸,我使用事件的 coalescedTouchesForTouch 方法返回一个 UITouch 数组:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first, event = event else
    {
        return
    }

    let locationInView = touch.locationInView(view)

    mainDrawPath.addLineToPoint(locationInView)
    mainDrawPath.appendPath(UIBezierPath.createCircleAtPoint(locationInView, radius: 4))
    mainDrawPath.moveToPoint(locationInView)

    mainDrawLayer.path = mainDrawPath.CGPath
    [...]



在这里,我循环遍历这些触摸(我跟踪触摸次数到控制台以获取信息)并通过将 oncoalescedDrawLayer 绘制为叠加层来创建一条黄线。我在 touchesMoved 方法中添加了一个可怕的循环来减慢速度:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first, event = event else
    {
        return
    }

    let locationInView = touch.locationInView(view)

    mainDrawPath.addLineToPoint(locationInView)
    mainDrawPath.appendPath(UIBezierPath.createCircleAtPoint(locationInView, radius: 4))
    mainDrawPath.moveToPoint(locationInView)

    mainDrawLayer.path = mainDrawPath.CGPath
    [...]


如果你在你的 iOS 9 设备上运行这个应用程序并涂鸦你可以看到两个结果:一条锯齿状的蓝线和一条基于所有那些可能被错过的中间触摸事件的漂亮平滑的黄线。在每个触摸位置,我添加了一个小圆圈,它清楚地说明了基于合并数据的黄色曲线的分辨率增加。

预测触摸

可能更聪明的是触摸预测,它可以让你抢占用户手指(或 Apple Pencil)在不久的将来可能出现的位置。预测触摸使用一些高度调整的算法,并根据 iOS 预计用户在未来大约一帧内触摸的位置不断更新。这意味着您可以在实际需要帮助减少延迟之前开始准备用户界面组件(例如开始淡入淡出或实例化某些对象)。

在我的演示应用程序中,我将预测的触摸显示为带有“尾巴”的小白点,这些“尾巴”源于它们的预测触摸。语法与合并触摸没有什么不同:事件有一个新方法,predictedTouchesForTouch(),它返回一个 UITouch 数组:


 override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?)
    {
        super.touchesMoved(touches, withEvent: event)
    guard let touch = touches.first, event = event else
    {
        return
    }

    let locationInView = touch.locationInView(view)

    mainDrawPath.addLineToPoint(locationInView)
    mainDrawPath.appendPath(UIBezierPath.createCircleAtPoint(locationInView, radius: 4))
    mainDrawPath.moveToPoint(locationInView)

    mainDrawLayer.path = mainDrawPath.CGPath
    [...]



当您运行该应用程序时,当您在屏幕上移动手指时,预测的小触摸点会指示 iOS 认为您的下一次触摸位置。对于锯齿状运动,预测效果不佳,您可以将这些点视为跟随触摸动量的小蝌蚪。


您可以在上面的屏幕截图中看到一些灰色圆圈,其中显示了 iOS 预测我会完成螺旋形的两次触摸——我实际上从未这样做过!幽灵般的!


结论

由于用户需要更少的延迟和更高的触摸分辨率,触摸合并和预测使我们的应用程序能够支持这一点。无论我们的应用程序的帧速率如何,我们都可以响应用户的所有手势,甚至抢占其中的一些!

此演示应用程序的源代码可在 我的 GitHub 存储库 中找到。