ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue+高德地图JSAPI实现车辆轨迹巡航与动态路线绘制

Vue+高德地图JSAPI实现车辆轨迹巡航与动态路线绘制

做地图应用的人应该都遇到过这类需求:后台给一条路线,前端要像导航一样把小车从头开到尾,期间还要显示已走过的路径、剩余里程、预计时间。这种效果在高德地图 API 里有个专门的方向,叫轨迹巡航。最近我在一个 Vue 项目里用高德地图 JSAPI 2.0 实现了完整的动态绘制路线巡航功能,从路线规划、坐标加密、巡航动画到进度同步都完整走了一遍。这篇就把整个实现过程、踩过的坑和性能优化思路记录下来,给同样需要做轨迹巡航(轨迹回放)的读者一个可以直接参考的落地样例。

1. 轨迹巡航到底在解决什么问题,以及为什么这么选型

1.1 业务场景先对齐:你要的到底是"轨迹回放"还是"路线巡航"

很多需求文档里会把轨迹巡航写得很含糊,但落到开发侧,本质只有两种:

  • 轨迹回放:车辆已经跑完了,后台拿到的是设备上报的历史 GPS 坐标序列,前端把这段轨迹按时间顺序动态画出来,小车顺序走一遍。
  • 路线巡航:还没有实际行驶,用户规划了一条从 A 到 B 的路线,前端要把这条规划好的路线模拟成车辆一路开过去的效果,通常伴随剩余里程、预计时间的展示。

这两个需求在高德地图里的实现思路高度相似:核心都是"拿一长串经纬度坐标,驱动一个 Marker 沿着坐标序列匀速移动"。差别在于坐标来源:一个是设备上报的真实轨迹,一个是AMap.Driving路线规划插件返回的规划路线。下面这套 Vue 方案,两条路都能走通,只是坐标预处理环节稍有不同。

我最近做的这个项目就是第二种:一个跑腿配送的接单大屏,调度员选择配送起点和终点后,系统给出规划路线,大屏上模拟一辆小车从起点开到终点,同时显示已行驶路段和剩余里程。这个场景就是标准的高德地图动态绘制路线加轨迹巡航。

1.2 为什么直接用 MoveAnimation,而不是自己写定时器移动 Marker

最早我确实想过自己写:拿到路线坐标后,用setInterval每隔 100ms 把 Marker 的 position 往后挪一个点。写起来很简单,但做起来会迅速碰到三个问题:

  1. 转弯不平滑。路线坐标里直道上的点距离可能几百米,而转弯处的点比较密,如果不做插值,小车在直道上像瞬移,转弯处又一顿一顿。
  2. 方向不会自动旋转。Marker 默认只有一个图标,不会按照行驶方向转向,你得自己算航向角,处理 360 度回绕。
  3. 暂停、继续、快进、回放这些交互全靠自己实现,状态一多就容易乱。

高德 JSAPI 2.0 提供的AMap.MoveAnimation插件把上面这些事都封装好了。它接收一条 path 坐标数组,内部按速度推进,Marker 可以在动画过程中自动旋转方向,还提供了start、pause、resume、stop等控制方法。对于巡航场景来说,这是最省力也最不容易出错的做法。

1.3 整体技术链路预览

先给一个完整流程,后面每一节都是这个流程里的一个环节:

  1. 在 Vue 组件里初始化高德地图实例。
  2. 使用AMap.Driving做路线规划,拿到路线数据。
  3. 把路线数据中的多个 step 里的坐标点合并成一条完整的坐标数组。
  4. 对坐标数组做等距插值加密,避免巡航动画卡顿。
  5. 创建AMap.MoveAnimation实例并传入加密后的坐标数组。
  6. 创建巡航 Marker,绑定到 MoveAnimation 实例上。
  7. 调用start()开始巡航,通过定时器推算已走里程、剩余里程,同步进度条。
  8. 使用两条 Polyline 叠加,实现"已行驶路段高亮、未行驶路段灰暗"的动态绘制效果。
  9. 组件销毁时清理动画、地图实例,防止内存泄漏。

2. Vue 项目接入高德地图:从环境准备到实例生命周期

2.1 两种接入方式的取舍

高德地图 JSAPI 在 Vue 项目里的接入方式主要有两种,我推荐直接使用官方维护的@amap/amap-jsapi-loader。

第一种是传统的 CDN 方式,在index.html里写<script src="https://webapi.amap.com/maps?v=2.0&key=你的key">,然后在组件里直接使用全局变量AMap。这种方式的问题在于:Vue 首屏加载时这个外部脚本会阻塞页面解析,而且插件的加载很难按需控制,密钥和版本参数写死在 HTML 里也不好维护。

第二种是通过@amap/amap-jsapi-loader动态加载。这个 loader 会在调用load()时加载对应的 JSAPI 脚本,并且支持按需传入plugins参数,类似前端模块化的思路:

import AMapLoader from '@amap/amap-jsapi-loader'; const AMap = await AMapLoader.load({ key: '你的Key', version: '2.0', plugins: ['AMap.Driving', 'AMap.MoveAnimation', 'AMap.Scale'] });

同一个项目里的多个地图组件可以共享同一个 AMap 实例引用,不会重复加载脚本。如果项目里有多个页面都需要地图功能,可以抽一个公共模块统一处理加载逻辑。

2.2 密钥与安全密钥的配置

高德 JSAPI 2.0 对安全校验的要求比 1.x 严格。除了常规的申请开发者 Key,还需要配置安全密钥。最常见的方式是页面加载前给window._AMapSecurityConfig赋值:

window._AMapSecurityConfig = { securityJsCode: '你的安全密钥' };

如果使用@amap/amap-jsapi-loader,这个配置同样要在 load 之前完成。我的做法是在入口文件或者工具模块里先设置_AMapSecurityConfig,再调用AMapLoader.load。需要注意的是,安全密钥是敏感性信息,不建议明文暴露在前端代码里。实际生产可以用后端代理转发的方式把密钥下发到页面,或者使用服务端代理调用方式规避前端密钥暴露。

提示:地图容器如果没有设置高度,或者父容器高度为 0,地图只会显示空白。初始化前务必确认容器有显式高度,比如height: 100%或者固定 px。

2.3 Vue 组件里地图实例的生命周期

地图实例和 Vue 组件的生命周期绑定是一个很容易被忽略的点。地图初始化必须在 DOM 渲染完成之后,也就是onMounted里调用。在 Vue 3 组合式 API 下,我建议用一个非响应式的普通变量保存地图实例,或者用markRaw包裹,避免把地图实例放进reactive响应式系统里。

为什么这么说?因为地图实例对象里包含大量内部状态和方法,一旦放进 Vue 的响应式代理里,访问任何属性都会被拦截,性能开销非常大,而且 Vue 的响应式系统无法正确处理地图内部循环引用的数据结构。团队里之前就有同事把map放进了ref,结果地图拖动都卡顿,排查了半天才发现是响应式代理造成的。

组件销毁时一定要手动释放地图资源。Vue 3 的onBeforeUnmount钩子里,把巡航动画停掉、定时器清掉,然后调用map.destroy():

onBeforeUnmount(() => { if (moveAnimation) moveAnimation.stop(); if (timer) clearInterval(timer); if (map) map.destroy(); map = null; });

地图不销毁的话,即使组件从页面上移除,图层和事件监听仍然挂在全局,内存会持续增长,对长时间运行的单页应用影响很明显。

3. 路线数据的获取与坐标预处理:决定巡航流畅度的关键一步

3.1 Driving 路线规划返回的数据结构

先说结论:AMap.Driving返回的数据并不直接适合丢给MoveAnimation,中间必须做坐标拼接与加密。

使用方式是在实例化 Driving 后调用search(start, end, callback):

const driving = new AMap.Driving({ map: map, policy: AMap.DrivingPolicy.LEAST_TIME, showTraffic: false }); driving.search(startLngLat, endLngLat, (status, result) => { if (status === 'complete' && result.routes.length) { const route = result.routes[0]; // route 就是我们要的路线数据 } });

回调里的result.routes是一个数组,正常情况下第一条就是推荐路线。每条 route 包含几个关键字段:

字段含义备注
distance路线总距离,单位米展示剩余里程用
time预计耗时,单位秒展示预计时间用
steps每一段行驶指引的数组每个 step 里有一段路线的坐标点

这里最关键的是steps。每个 step 代表一个行驶方向上的分段,比如"沿某某路行驶 2.3 公里",每个 step 内部有一个path字段,是这个分段内的坐标点数组。完整路线的坐标序列,就是把所有 step 的 path 依次拼起来。

3.2 把分段坐标拼接成完整路线

这一步的代码很直接,但有一个细节需要注意:坐标点对象。AMap.Driving返回的path里的元素是AMap.LngLat实例,而不是普通的[lng, lat]数组。如果你要自己保存一份纯数据副本、做插值计算或者传给后端,最好统一转成[lng, lat],避免后续 API 参数混用。

const fullPath = []; route.steps.forEach(step => { step.path.forEach(point => { fullPath.push([point.getLng(), point.getLat()]); }); });

如果你需要的是AMap.LngLat对象数组,也可以用point.lng和point.lat构造新的new AMap.LngLat(lng, lat)。不要直接复用原对象做写入操作,因为路线规划结果是一次性数据,后续如果你要做轨迹修正,改对象会影响原始数据。

3.3 为什么必须做等距插值加密

把路径拼完直接传给MoveAnimation,表面上看也能动起来,但效果大概率会让你想删代码。

路线规划的坐标点主要记录的是"哪里拐弯、哪里经过路口",所以直道上的采样点很稀疏,点与点之间可能相隔几百米甚至更多。MoveAnimation 内部是按速度匀速在相邻点之间移动的,点间距越大,Marker 在相邻点之间运动的时间就越长,视觉上就是一卡一卡的"跳点"感,尤其在高速巡航快进的时候。

解决办法是对完整路径做等距插值:每隔固定距离(比如 20 米)插入一个中间点,把相邻两个大跨度点之间用插值点填满,这样整条路径的坐标点密度均匀,Marker 巡航起来就顺滑了。

3.4 等距插值算法的实现与边界处理

在高德 JSAPI 里可以用AMap.GeometryUtil.distance(point1, point2)计算两个坐标间的球面距离,单位是米。然后按间隔距离均匀插值:

function densifyPath(path, gap = 20) { const result = [path[0]]; for (let i = 1; i < path.length; i++) { const prev = result[result.length - 1]; const cur = path[i]; const dist = AMap.GeometryUtil.distance(prev, cur); if (dist <= gap) { result.push(cur); continue; } const count = Math.round(dist / gap); for (let j = 1; j <= count; j++) { const t = j / count; const lng = prev.lng + (cur.lng - prev.lng) * t; const lat = prev.lat + (cur.lat - prev.lat) * t; result.push(new AMap.LngLat(lng, lat)); } } return result; }

这里用的是线性插值。在地球球面上做严格的大圆插值会更精确,但对于城市道路这种几公里范围内的路径,线性插值的误差可以忽略不计。gap 的取值需要平衡:20 米比较平滑,数据量相对可控;如果做超长路线巡航,建议用 50 米,否则几万点的数组也会拖慢前端性能。

还有一个坑:如果cur和prev完全重合,插值点会重复堆积。我通常在循环里加一个判断,距离小于 1 米直接跳过,避免生成的路径里存在大量重叠点。

4. 巡航动画核心:MoveAnimation 的配置与使用

4.1 核心参数解析

AMap.MoveAnimation这个插件在@amap/amap-jsapi-loader里通过plugins参数引入后,直接new一个实例:

const moveAnimation = new AMap.MoveAnimation({ map: map, path: densePath, // 插值加密后的坐标点数组 speed: 60, // 速度,单位 km/h autoRotation: true, // 是否根据移动方向自动旋转 Marker easing: 'linear' // 动画缓动方式,巡航推荐 linear });

这里主要解释两个容易误解的参数:

  • speed的单位是 km/h,不是 m/s,也不是"每秒移动多少个点"。它代表模拟的车辆行驶速度。如果你想让巡航效果更快,比如 5 倍速回放,直接用 60 乘以 5 就行。项目里我做了一个倍速切换,就是动态改这个值。不过要注意,MoveAnimation 文档中 speed 一般是初始化时传入,后续想改速度需要重建实例,或者通过stop()后重新设置。实际项目里我采用重建实例的方式,简单可靠。
  • autoRotation开启后,Marker 的图标会按照行驶方向自动旋转。这个功能很实用,但前提是你的 Marker 图标本身有"朝向"概念,比如车头朝上的车辆图标。如果你用的是一般的圆点、默认红色标记,旋转不旋转视觉上没区别,但如果有朝向图标,关闭这个参数小车就会"横着走"。

路径坐标数组使用AMap.LngLat对象数组,高德官方文档要求 path 是轨迹坐标点数组。如果你手上是[lng, lat]的普通数组,记得先转换一下再传。

4.2 巡航 Marker 的创建与绑定

MoveAnimation 本身不创建 Marker,它只负责管理传入的 Marker 的移动。所以你需要自己先创建 Marker,然后用addMarker绑定:

const cruiseMarker = new AMap.Marker({ position: densePath[0], icon: new AMap.Icon({ size: new AMap.Size(32, 32), image: 'https://你的站点/vehicle-icon.png', offset: new AMap.Pixel(-16, -16) }) }); moveAnimation.addMarker(cruiseMarker);

这里有一个非常容易踩的坑:AMap.Marker在创建时如果传了map参数,或者之后手动调用了map.add(marker),再把它加进 MoveAnimation,巡航时会出现 Marker 在起点闪一下、然后又跑到路径中间去的问题。正确做法是把 Marker 创建出来以后不添加到地图上,直接让moveAnimation.addMarker去托管。刚才的代码里new AMap.Marker只传了position和icon,没有传map,这一点务必注意。

position设为densePath[0]是为了让 Marker 初始停在路线起点,避免动画开始时从别的地方飞过来。图标offset也要处理一下,比如 32x32 的图标,offset设置为new AMap.Pixel(-16, -16)让图标中心对准坐标点,否则小车会出现半个车身悬在路边的情况。

4.3 开始、暂停、继续与重置的控制方法

MoveAnimation 的控制方法相对直观:

// 开始 moveAnimation.start(); // 暂停 moveAnimation.pause(); // 继续 moveAnimation.resume(); // 停止并清空 moveAnimation.stop();

这里我踩过一个性能相关的坑:直接调用stop()后,如果同一路线上马上再调用start(),动画虽然能重新开始,但内部状态偶尔会出现计时错乱,车速感觉比设定值快。原因大概率是 stop 后内部的时间基准没有完全重置。所以我在实现"重新巡航"功能时,不是复用原来的 MoveAnimation 实例,而是先moveAnimation.stop(),再重新执行一遍"创建实例 + addMarker + start"的流程。虽然看起来多写了十几行代码,但稳定性比复用实例好太多。

提示:pause()和resume()组合使用时,如果你的进度条同步是放在独立定时器里算的,暂停时记得同步暂停定时器。否则会出现画面停了,进度条还在走的尴尬情况。

4.4 巡航进度同步方案:理论推算法 vs 坐标读取法

巡航过程中,界面上的剩余里程、进度条百分比怎么跟着小车一起走?我试过两种方式。

第一种是坐标读取法:定期去读cruiseMarker.getPosition(),用AMap.GeometryUtil.distance累加已走里程。这个方案的问题在于,巡航过程中频繁读取坐标会与动画引擎的内部推进产生微小错位,而且累加计算容易产生误差累积,要处理起点校验。实测下来用在展示上还可以,但进度条偶尔会在暂停后出现回退。

第二种是理论推算法:不读坐标,纯按时间和速度推算。已知路线总里程route.distance、当前模拟速度speed、已运行时长,理论已走距离 = 速度 × 时长。我最终采用的是这种方案,稳定可靠:

function startProgressTimer(speedKmh) { const startTime = Date.now(); const speedMs = speedKmh / 3.6; // km/h 转 m/s const totalDistance = route.distance; if (timer) clearInterval(timer); timer = setInterval(() => { const elapsed = (Date.now() - startTime) / 1000; const passed = Math.min(totalDistance, speedMs * elapsed); this.passedDistance = passed; this.progressPercent = Math.round((passed / totalDistance) * 100); }, 200); }

如果之前已经暂停过几分钟,再继续巡航时,定时器的 startTime 要顺延暂停时长。实现时可以在暂停时记录pausedAt,继续时把暂停时长累加到一个pauseTotal变量里,公式变成Date.now() - startTime - pauseTotal。这一点别看小,实际做起来很多人都会漏掉。

5. 动态路线绘制的进阶效果:已行驶路段高亮

5.1 用两条 Polyline 叠加实现"渐亮"效果

标题里说的"动态绘制路线",在巡航场景里一般有两种理解方式:一种是整条路线规划后一次性画出来,另一种是让路线随着小车的行驶逐步"点亮"。第一种很简单,new AMap.Polyline({ path: fullPath })画上去就可以了。但第二种视觉效果好得多,看起来更像导航 App 的真实效果。

实现思路是用两条 Polyline 叠加在同一张地图上:

  • 底层线:显示完整路线,使用灰色,代表"还没走过的路"。
  • 上层线:动态增长,使用高亮色(比如蓝色),代表"已经走过的路"。

两条线初始 path 为空,巡航过程中不断把当前小车经过的坐标点追加到上层线的 path 里。这样视觉上就是:车辆开到哪里,蓝色轨迹就画到哪里。

const passLine = new AMap.Polyline({ path: [], strokeColor: '#1970FF', strokeWeight: 6, strokeOpacity: 0.9, lineJoin: 'round', lineCap: 'round' }); map.add(passLine); const todoLine = new AMap.Polyline({ path: fullPath, strokeColor: '#C0C4CC', strokeWeight: 6, strokeOpacity: 0.6, lineJoin: 'round', lineCap: 'round' }); map.add(todoLine);

这样叠加出来的效果就是一条灰底 + 蓝色光标的轨迹线,动态推进时非常清晰。

5.2 已行驶路径的更新策略

上层线的 path 更新有两种做法。一种是定时器里根据已走距离,从 densePath 里截取前缀:

const passedCount = Math.floor(passedDistance / gap); const passedCoords = densePath.slice(0, passedCount + 1); passLine.setPath(passedCoords);

另一种是轮询 Marker 当前位置,把当前坐标追加到 passLine 的 path 末尾:

const currentPos = cruiseMarker.getPosition(); const existingPath = passLine.getPath(); existingPath.push(currentPos); passLine.setPath(existingPath);

第一种方式更稳定,因为 densePath 本身是等距插值过的,按距离比例截取不会出现点重复或跳变。第二种方式的好处是简单,但频繁setPath时如果坐标间隔太近,path 里会堆积大量冗余点,严重时 paint 耗时明显上升。我建议用第一种。

这里还有一个高德的 API 细节:polyline.setPath()会触发整个线重绘。如果每隔几十毫米就调用一次,地图的重绘压力会很大。实测下来 200ms 左右更新一次是比较平衡的频率,既能看到流畅的轨迹增长,又不会把浏览器帧率拖垮。

5.3 巡航时地图视口如何平滑跟随

巡航时地图要不要跟着小车动?这取决于业务。大屏演示类场景一般希望地图居中跟随,让小车始终处于视野中心;而监控场景可能反而希望固定视野,看整体路线。

高德地图跟随小车有几种实现:

最简单的是每次更新时map.setCenter(currentPosition, true)。第二个参数为 true 表示是否带动画的平滑移动。这个做法的缺点是如果坐标更新频率高,动画移动会叠加,地图像在"抖"。

我实际使用的是按距离阈值触发的方式:只有当小车接近地图视野边缘时才移动视口。实现思路是定期比较小车坐标与当前地图中心坐标的距离,超过某个阈值就平移地图:

const center = map.getCenter(); const dist = AMap.GeometryUtil.distance(currentPos, center); if (dist > 300) { // 超过300米再移动视口 map.setCenter(currentPos, true); }

300 米这个值可以根据缩放级别调整。zoom 在 16 级左右时,300 米大约是视野宽度的四分之一,视觉上不会频繁跳动。如果 zoom 更大,阈值可以调小;zoom 更小,阈值调大。这个参数需要根据你自己的业务页面实际跑一下再定。

6. 实战中踩过的坑,以及性能优化思路

6.1 整条路线巡航不动,或者车跑到路线外面

这种情况我排查的时候发现原因往往很基础。

第一个是插件没加载。AMap.MoveAnimation是插件形式,必须在AMapLoader.load的plugins数组里显式声明。如果漏了,代码里直接new AMap.MoveAnimation()会抛AMap.MoveAnimation is not a constructor,这个错误比较明显。但有一个隐藏情况:项目里其他地方已经加载过高德 JSAPI,组件里再次调用AMapLoader.load时,如果 plugins 里没有包含 MoveAnimation,而全局 AMap 对象上又没有这个类,就会报错。解决办法是保证加载公共模块时就把巡航用到的插件都声明上。

第二个是坐标格式不统一。MoveAnimation 的 path 要求是AMap.LngLat对象数组,如果你传的是普通过数组[ [lng, lat], [lng, lat] ],部分版本会直接不播放动画,部分版本会在内部计算出错。我初始化 path 前统一做了一次类型转换,问题解决。

第三个是 GPS 轨迹坐标偏移。如果是后台传入的设备轨迹数据,大概率是 WGS-84 坐标,而高德地图使用的是 GCJ-02 的加密坐标体系,两者差距几十米到几百米不等。最直观的现象是整条轨迹整体偏移,小车不在路上跑。这种情况有两种处理思路:一是让后台把轨迹数据先通过高德的坐标转换服务转成 GCJ-02 再下发;二是前端把 GPS 原始坐标批量调用高德坐标转换接口转一下再进入巡航流程。前端转换要注意转换接口的配额限制,生产环境尽量让后台处理,避免前端大量塞接口请求。

6.2 动画卡顿和坐标跳变的排查

动画卡顿的元凶,90% 是坐标点数量问题。

我最初做插值加密时把 gap 设成了 5 米,城市内的路线近 20 公里,算下来坐标点接近 4000 个。MoveAnimation 在每个点之间推进时都会计算方向、更新 Marker 位置、触发重绘,再加上上层轨迹线也在刷新,浏览器明显吃力。后来把 gap 调整为 20 米,数据量降到 1000 个左右,动画就顺滑了。实际体验下来,路径点尽量不要超过 2000 个,超过的话建议做抽稀,或者分块处理。

坐标跳变还有一个隐蔽来源:路线里有异常点。例如某一段 path 里突然出现一个离路线偏差很大的点,小车到那个点会"瞬移"过去再瞬移回来。这种一般是数据源的问题,可以在预处理阶段过滤掉:如果一个点和前一个点的距离超过路线平均点间距的 10 倍,就判定为异常点,跳过。

进度条和动画不一致也容易被认为是"跳变"。我调试时发现暂停后再继续,画面里小车继续走了,但进度条停了几秒才跟上,原因就是前面说的:进度定时器没有考虑暂停时长。把暂停时长累加进进度公式之后,这个问题就消失了。

6.3 Vue 响应式系统的陷阱

地图相关对象不放进响应式系统,这个前面已经强调过。补充两个更细的操作层面的注意点。

一是路线坐标大数组同样不要放进reactive。比如插值后的 densePath 有上千个坐标对象,如果被reactive包裹,每次往数组里 push 新点都触发依赖收集,而且数组元素被代理后访问lng、lat属性都会走代理层,性能消耗成倍增加。我的做法是把这些数据放在普通函数作用域里,不经过 Vue 的响应式系统,只在需要展示的数据(比如进度百分比、剩余里程)上用ref或reactive。

二是如果项目用的是 Vue 2,注意不要在data里声明AMap.Map实例。Vue 2 的响应式系统会对data做深度响应式处理,地图实例放进去更糟糕。解决办法是放在created里挂到this上但不要加在data返回对象里,或者在data里用一个普通Object.create(null)容器绕开响应式逻辑。

6.4 一个完整的"重置巡航"应该做什么

做"重新规划路线"或者"重置巡航"功能时,最怕的是界面重置了,但旧的地图物件还残留在地图上。

我总结的完整清理顺序是:

  1. 停掉进度定时器。
  2. 调用moveAnimation.stop(),清除动画。
  3. 移除巡航 Marker,通过moveAnimation.removeMarker()或者直接map.remove(cruiseMarker)。
  4. 清除两条轨迹线,map.remove(passLine)、map.remove(todoLine)。
  5. 把进度数据归零:passedDistance = 0、progressPercent = 0。
  6. 重新执行路线规划到初始化巡航的完整链路。

注意顺序不要反。如果先把轨迹线移除了,再 stop 动画,偶尔会出现动画还在跑、Marker 还在动但底图线没了的情况。先 stop 动画,再移除 Marker,最后清线,顺序稳定。

地图实例销毁时也同样:先停动画,再清定时器,最后 destroy 地图。如果先 destroy 地图,MoveAnimation 内部还在引用 map,控制台会报一些内存相关的 warning,虽然业务不影响,但会让人排查半天。


最后再分享一个实际工程里的经验:轨迹巡航这类功能做完之后,最好留一个"速度倍率"参数,因为业务方在使用大屏演示时经常会说"麻烦让车跑快一点"。这个参数不要写死在 MoveAnimation 的 speed 里,而是在启动时算好传给构造函数,方便后续调参。另外,如果项目里有多个地方都会用到轨迹巡航,把路线规划、坐标加密、巡航初始化、进度控制这四段逻辑抽成独立模块,会比每个页面复制粘贴一套代码强得多。做地图功能,代码洁癖一点,后面维护起来会省很多事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进