ARKit 中如何使用传统的二维手势?

1,171

AR 中的手势操作

可能大家看了标题,都没明白:AR 中的二维手势是个什么玩意?

其实就是给 AR 物体贴图,在贴图中放一个 UIButton 或 UIScrollView,又或者自定义 View, 看它们能不能正常响应手势,以及能响应哪些手势。

关于贴图可以看 SCNNode到底应该怎么贴图?UIImage,UIImageView,CALayer用哪个?

需要注意的是:苹果官方目前(至 iOS13)都并不推荐贴图使用 UIView 类,可能会有潜在的布局问题,内存问题等。苹果论坛上官方员工的回复forums.developer.apple.com/message/340…

哪些手势还可以使用?

经过测试发现,平时我们使用各种手势,在 AR 中基本都是可以使用的。包括:UIButton 的各种手势,UIScrollView 拖拽手势,UITapGestureRecognizer 手势, 甚至是 UICollectionView 的选中 cell 手势都可以正常工作。

如下图:浅灰色背景是 UIView;左上角放置两个 UIButton,黑色和深灰色;右下角大块的是 UICollectionView,三个 cell 颜色不同。

需要注意的是:当给整个 View 添加了 UITapGestureRecognizer 手势后, UICollectionView 的选中 cell 手势就不再响应了。如下图:

触发问题

在开发中遇到常见问题,一个是 AR手势与屏幕手势优先级问题,这个下面再讲。另一个是 AR 中 UIButtontouchUpInside手势,有时难以触发的问题。

尤其是在距离 AR 平面比较远(2 米外),没有正面对着平面时(斜着点击),非常严重。经常可以看到按钮已经被按下并处于高亮状态了,但是闪一下后又退出了高亮状态,而手势并没有被触发。

我想可能是因为touchUpInside要求手指按下时和离开时,都要在按钮内部,这样才会触发。而 AR 中的按钮要想被点击,需要一只手拿着手机,另一只手点击屏幕中 AR 平面的区域,这样造成的点击时抖动太大。

这样在 AR 中的按钮看来:手指按下了按钮(touchDown),然后抖动严重,跑到了按钮外面(outside),这样是不符合 touchUpInside 定义的,所以不会触发。

为了按钮手势更好的触发,将按钮触发条件改为touchDown,可以有效提高 AR 按钮点击响应的概率。

还有一个类似的问题,是 AR 中 UIScrollView 的拖拽手势在拖动时经常会中断和抖动,尤其是从侧面进行拖拽时。好在这个问题不太严重,也没有什么太好的处理方法。

iOS 13 中 UICollectionView 的新问题

原本在 iOS 12 中只有 UIButton 的touchUpInside手势会出现难以响应的问题,更新 iOS 13 后,UICollectionView 选中 cell 手势(didSelectItemAt)也出现了几乎一模一样的选中困难问题,严重时点击 10 次都会触发一次。

为了缓解这个问题,不得不给整个 UICollectionView 添加了 UITapGestureRecognizer 手势,来代替 didSelectItemAt 事件。

手势的优先级问题

经过测试,发现优先级:AR 贴图手势 > 屏幕手势,哪怕你的 ARSCNView 在下面,而屏幕手势被添加的 UIView 层级更高,也仍然是 AR 中的手势先触发。

具体来说:AR 中 UIButton 手势(touchUpInside/touchDown) > AR 中 UIScrollView 拖拽手势 > AR 中的 UITapGestureRecognizer 手势 > AR 中的 UICollectionView 选中 cell 手势(didSelectItemAt) > 屏幕上的 UIButton 手势 > 屏幕中的 UITapGestureRecognizer 手势....

下面,我们给屏幕中添加一个 UIButton,然后测试点击它,注意观察当它后面有 AR 内容时,和没有 AR 内容时,响应是不一样的。只有当 screenButton 后面的 AR 物体不能响应时,screenButton 的手势才能触发(arButton2 在录制时没点击到)

如果把屏幕上的 UIButton 换成对整个 View 添加 UITapGestureRecognizer 手势,结果也是一样的

即使你在控制器界面实现touchesBegan方法也是一样,低优先级被触发:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}

思考与总结

实践一番下来,苹果的各种手势基本都能在 AR 贴图中使用,即使个别有问题也可能通过变通的方式来解决。

真正麻烦的是 AR 贴图手势与屏幕手势之间的优先级问题。一旦贴图中有了手势,它就会永远处于第一级响应者,导致我们在屏幕上的 UI 控件无法响应。这无疑给我们开发时增加了困难。

方案一

为了兼容两种手势事件,不得不在屏幕上有控件需要点击时,临时禁用 AR 贴图中的手势。屏幕控件消失后,再启用贴图中的手势

方案二

但是如果屏幕上确实有控件需要一直显示并等待点击,而 AR 贴图中也需要一直显示并等待点击,该怎么办呢?比如在 AR 贴图中有个按钮,屏幕上也有个按钮,当用户把两个按钮重合时,点击一下,响应肯定的是 AR 中的按钮,这却并不是我们想要的结果。

暂时想到的方法是,用下面的方法来把 AR 中按钮的 3D 位置,投影到屏幕上,看看屏幕上对应位置有没有需要响应的按钮

func projectPoint(_ point: SCNVector3) -> SCNVector3

但是这样也会有很多复杂步骤:

  • 先获取 AR 贴图中按钮被点击位置的 2D 坐标;
  • 再根据贴图尺寸,以及按钮在贴图中的 2D 坐标计算其在贴图中的相对坐标;
  • 根据 SCNNode 的几何形状,计算贴图平面在 3D 空间的位置;
  • 根据上面两步的 3D 位置与 2D 坐标,计算点在 3D 空间中的真实位置;
  • 将 3D 位置坐标投影到ARSCNView中;
  • ARSCNView 坐标位置处是否有屏幕按钮;
  • 点击事件需要传递处理......

建议

所有这些都严重增加了开发的复杂度,造成得不偿失。所以还是建议少在 AR 贴图中使用手势,如果必须要使用就注意减少与屏幕控件的手势冲突。

另外,使用 UIView 作为 SCNNode 的贴图,疑似存在内存泄露的问题:View 和 Node 都销毁了,但 app 的总内存却不断上涨。需要大家在开发中注意,暂无好的解决办法。