很多应用的都会使用到地图控件,我们目前使用到地图的场景是规划无人机的飞行路线。最常见的是规划一块要飞行的区域,然后生成巡航的路线,最后用拍到的照片进行建模。有了模型后就可以获得场地的一些数字化信息。
我们有些客户要建模的区域在郊区,因此地图的信息准确对于我们规划飞行区域的帮助很大。我们调研后发现高德和百度的图源质量各有长短,因此我们决定同时接入多个地图源。客户可以根据现场的具体情况选择地图源。
我们的地图控件在规划路线时有很多的自定义 UI 需要展示,下面是我们的规划正射飞行的一张设计图:
如果我们使用地图 SDK 提供的绘制方法,同样的自定义 UI 在不同的地图源上就要重复的实现一遍。如果我们支持三个地图源,那么就要针对三个地图源提供的方法实现三套 UI。这显然不是一个可以接受的方案。因此我们决定将自定义 UI 绘制抽离出来,这样最复杂的自定义 UI 绘制只要维护一份代码,地图源当做组件可以替换。
最后在手机上看起来就像下图这样,地图源在最底层,自定义 UI 在地图上层。这样更换地图源只是最底层的视图发生了变化:
数据流
下面展示自定义 UI 和地图源映射的过程:
如果要添加一个新元素,那么调用地图的接口获取这个标注的地理坐标在地图上的视图坐标,接着在自定义视图上添加标注。显然如果地图的展示区域变了,标注的坐标也需要一起改变。因此需要监听地图的区域变动事件,一旦改变,需要重新计算标注的平面坐标,更新标注位置。
换成伪代码是这样的:
除了自定义的 UI 的展示外,有时自定义的 UI 还要支持交互。在我眼前的业务场景里,需要交互的元素交互时都是处在视图的最顶层。如果可交互元素和展示元素放在一个视图里,就需要有状态区分哪些元素会响应交互事件。因为最底层的地图也要接收交互事件(用户可能会调整地图)。因此视图的 hittest 处理起来就会有些杂乱,因此我将可交互的元素都统一放在一个图层里。这个图层里所有的元素都响应用户交互事件。其他的交互事件最后都会传给底部的地图。
整体的视图结构大概就是这样:
缺陷
这个方案的优势是将自定义 UI 的实现与具体的地图 SDK 隔离,当地图源切换时,自定义 UI 可以直接复用。但是这个方案也有一个固有缺陷:当地图持续响应用户交互,自定义的 UI 绘制会略有卡顿。
如果将 UI 通过地图 SDK 添加到地图上,自定义 UI 的绘制与地图的渲染发生在同一帧里。因此任意一帧里,自定义 UI 和地图的位置总是对应的,从用户的角度看就是图形的变换很顺滑。但是我们的选择的方案,自定义的 UI 变换前有两个步骤:收到地图区域变化通知,重新计算图形在地图上的平面坐标。假设手机没有性能问题,那么地图原生图层的绘制频率是 60Hz。当用户进行连续缩放时,地图的图层每秒绘制 60 次。用户感觉很顺滑。地图区域变化的外界通知的频率由于性能考虑会低于 60Hz,因此即便重新计算图形坐标、绘制可以在 1/60 秒完成,自定义 UI 的渲染频率还是会低于 60Hz。给用户的体验就是连续移动时,自定义 UI 没有那么跟手。
至于自定义 UI 的绘制延迟是否可以接受这就看各自的场景了。但是从我的测试结果看,虽然性能会差一些,但是大部分场景不会影响到用户的使用。