news 2026/9/15 13:29:01

轨迹回放核心:基于高德地图JS API打造平滑播放与进度控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
轨迹回放核心:基于高德地图JS API打造平滑播放与进度控制

第一次做轨迹回放的时候,我就是那个“给客户演示时车直接从起点瞬移到终点”的倒霉蛋。产品经理在会议室当着所有人的面问我:“这是回放,不是闪现。”后来我老老实实把高德地图上的车辆Marker移动、消息框速度展示、轨迹同步这三个功能拆开重做了一遍,才算摸清这套玩法。最近这个版本又升级了播放速度调节和回放进度拖拽,直接把最初那版“按点跳帧”的实现方式推翻重来,踩了不少坑,也把轨迹回放模块沉淀成了可复用的方案,这里完整整理出来。

这个功能最终达到的效果是:地图上有一辆车的Marker,按照历史轨迹平滑行驶;车辆上方有一个消息框,框里实时显示车辆速度,并且消息框跟随车辆移动;用户可以通过按钮切换0.5倍、1倍、2倍、4倍播放速度,也可以拖拽进度条跳转到任意时间点然后继续播放。整套方案基于高德地图JS API 2.0实现,文末会聊到移动端、小程序和性能优化的差异,做地图回放类功能的朋友可以参考。

先说明一点,我这边没有拿到完整接口文档,下面所有代码结构和参数都是基于常见实践补充的标准实现,回到你项目里改改类名和数据结构基本能直接跑。

1. 先理清楚:轨迹回放的本质是一个“带时间轴的播放器”

1.1 需求拆解:除了“车在动”,用户还要什么

最早的需求其实很简单:把车辆某段时间的行驶轨迹画到地图上。但随着产品经理和客户来回沟通,需求逐渐变成了三句话:

  1. 车要在地图上按真实轨迹移动,不能一下蹦过去;
  2. 车辆旁的消息框要显示当前速度,而且要跟着车走;
  3. 用户可以调整播放速度,拖进度条看任意时间点的位置。

看起来就是三个点,实际上每一条都是独立的子系统。“车动起来”涉及轨迹插值和Marker位置更新;“消息框跟车”涉及InfoWindow的定位与内容刷新;“调速度和拖进度”则要求回放核心不能是“顺序取点”,必须引入时间模型。这三件事合在一起,才是一个能接真实业务的地图轨迹回放模块。

1.2 两种实现思路:按点跳帧为什么撑不起升级需求

最直觉的做法是按点跳帧:把轨迹点放进一个数组,setInterval每500ms取下一个点,塞给Marker。这个方案十分钟能跑通,但它有个致命缺陷——进度条根本没法做。因为“下一个点”和“真实时间”没有对应关系,用户拖到进度条50%,你不知道该把车放到哪个时间点;想支持1倍、2倍、4倍速,唯一办法是把定时器间隔改小,逻辑越写越乱。

所以升级版必须改成基于时间戳的播放器模型。轨迹点本身带time字段,我们把整个回放当成一个虚拟时钟:当前播放时间 = 起始时间 + 播放时长 × 倍率。车辆位置永远通过“当前时间点落在哪两个轨迹点之间”计算出来,插值得到坐标。这样进度条、倍速、暂停、跳转,全部统一到一个时间变量上,所有交互都变成改这个变量、然后重新渲染。这也是这次升级最核心的架构改动。

1.3 轨迹数据准备:数据源、坐标系和预处理

轨迹数据一般来自车载终端,格式是JSON数组,每个点至少包含经纬度和时间戳,如果终端本身能上报速度,也可以加上speed字段:

const rawTracks = [ { lng: 116.397428, lat: 39.90923, time: 1710000000000 }, { lng: 116.398654, lat: 39.909875, time: 1710000001000 }, { lng: 116.399782, lat: 39.910564, time: 1710000002000 } ];

实际采集频率通常是1到5秒一个点,点与点之间的距离可能只有几十米,也可能因为信号原因跳几百米。

这里要特别强调坐标系问题。高德地图用的是GCJ-02火星坐标,如果拿到的轨迹是GPS设备上报的WGS-84原始坐标,直接画到地图上会偏移几十米到上百米。这种情况要先把整段轨迹用AMap.convertFrom转换到高德坐标系,再进入播放器。我见过不少团队在这上面栽跟头,轨迹和实际道路始终对不上,排查很久才发现是坐标没转换。

如果轨迹点特别多,比如一天24小时、每秒一个点就是86400个点,播放前还一定要做抽稀,否则地图覆盖物数量会直接拖垮渲染。常规做法是用Douglas-Peucker算法做轨迹抽稀,既保住道路形状,又把点数降下来。

2. 把“车”和“消息框”同时放到地图上:Marker与InfoWindow的配合细节

2.1 车辆Marker的创建和锚点设置

车辆的Marker就是一个普通的AMap.Marker,但图标和锚点要处理好。车辆图片锚点一般取图片中心偏下的位置,这样车头方向旋转时才不会偏。我的实现是:

const marker = new AMap.Marker({ map: map, position: startPos, anchor: 'bottom-center', icon: new AMap.Icon({ size: new AMap.Size(38, 38), image: 'vehicle-icon.png', imageSize: new AMap.Size(38, 38) }), offset: new AMap.Pixel(-19, -19) });

车辆图标如果是带方向的,每次移动后要调用marker.setAngle(angle)让车头对准行进方向。这个角度计算在高德JS API里没有现成的方位角接口,需要自己算,后面专门讲。如果你用的是不带方向的圆点或默认图标,这一步可以跳过。

2.2 消息框用自定义InfoWindow,别用默认气泡

消息框使用AMap.InfoWindow,但必须设置isCustom: true,用自定义HTML来渲染。原因很简单:默认气泡样式由高德内部渲染,每次更新内容,它都会重新计算气泡尺寸和位置,播放速度一快就会闪;改成自定义内容后,消息框本质上是我们自己控制的DOM元素,性能好得多,样式也能完全贴合设计稿。

const infoWindow = new AMap.InfoWindow({ isCustom: true, offset: new AMap.Pixel(0, -46), content: `<div class="speed-panel"> <span class="speed-num">0</span> <span class="speed-unit">km/h</span> </div>` });

offset控制消息框相对锚点的偏移。车辆图标高38px,消息框想显示在车上方,offset的y取负的“车高加间距”即可。CSS里建议给.speed-panel设置transform: translate(-50%, -100%),保证消息框相对车辆居中对齐,无论Marker怎么旋转都保持稳定。

2.3 每帧只更新position和content,不要重建实例

播放过程中,车子每移动到新位置,消息框也要同步过去,内容里的速度还得跟着变。核心操作是下面这段,我建议封装成一个renderFrame函数,每一帧调用一次:

function renderFrame(pos, speed, angle) { marker.setPosition(pos); if (angle !== null) marker.setAngle(angle); infoWindow.setPosition(pos); infoWindow.setContent(buildSpeedHtml(speed)); } function buildSpeedHtml(speed) { return `<div class="speed-panel"> <span class="speed-num">${speed}</span> <span class="speed-unit">km/h</span> </div>`; }

一定不要在播放过程中重复new AMap.InfoWindow(),那会造成消息框不断销毁重建,地图上肉眼可见的闪烁和延迟。正确做法就是创建一个实例,只更新它的content和position。这个教训我在第一版实现中吃过亏,后来把重复创建改成单实例更新,闪烁问题立刻缓解了一大半。

2.4 为什么不用DOMOverlay或两个Marker

有人会问,消息框直接用自定义Overlay不是更好吗?确实可行,但InfoWindow帮我们处理了地图缩放、平移时的定位逻辑,自己在DOMOverlay里要实现的东西太多,包括坐标转换、事件穿透、层级管理,一旦地图交互复杂起来就容易出问题。

还有人会把速度和车做成两个Marker,一个车一个气泡框,也能走,但两个Marker之间需要手动维护相对位置,尤其是车旋转时气泡还要保持在正上方,坐标偏移计算比较折腾。我实测下来的结论是:一个自定义Marker加一个自定义InfoWindow,是最省心、效果最稳的组合。

3. 消息框里的速度:从两个轨迹点到一组稳定数字

3.1 球面距离计算:GeomUtils.distance与异常情况

速度的本质是距离除以时间。距离方面,高德JS API提供了AMap.GeometryUtils.distance(p1, p2),计算的是球面距离,单位是米,短距离场景下精度足够,直接用:

const distance = AMap.GeometryUtils.distance( new AMap.LngLat(prev.lng, prev.lat), new AMap.LngLat(next.lng, next.lat) );

不过有一点要提醒:如果轨迹被抽稀过,两个相邻轨迹点的距离可能非常远,抽稀后间隔几百米甚至几公里。这时候用原始轨迹算瞬时速度,只能代表这段路径的平均速度,不代表车辆在某个瞬间的实时车速。所以如果业务很在意速度精度,最好在抽稀之前先计算每一段的速度值,把它作为属性存进轨迹点,播放时直接取。

3.2 时间差、单位换算和段速度计算

时间直接用轨迹点的时间戳差值,单位是毫秒。速度等于距离除以时间,再把米每秒转成常用车速单位公里每小时,乘以3.6即可:

function calcSegmentSpeed(prev, next) { if (!prev || !next) return 0; const distance = AMap.GeometryUtils.distance( new AMap.LngLat(prev.lng, prev.lat), new AMap.LngLat(next.lng, next.lat) ); const seconds = (next.time - prev.time) / 1000; if (seconds <= 0) return 0; return Math.round((distance / seconds) * 3.6); }

这里要注意,如果两个轨迹点时间差为0,直接返回0,避免出现除零。某些终端设备会因为补发数据导致时间戳相同,这类点必须在预处理阶段过滤掉,不然速度会算成无穷大。

3.3 滑动平均与GPS漂移过滤

直接用相邻轨迹点算出来的速度,在GPS信号差的地方会跳得厉害,一会儿80一会儿120,用户看到会以为程序Bug了。我实际项目里的做法是取“当前点前后各2个点,共5段速度的平均值”做滑动平均。数组边界就取实际存在的点参与计算:

function smoothSpeed(speedArr, index, windowSize = 5) { let sum = 0, count = 0; const half = Math.floor(windowSize / 2); for (let i = Math.max(0, index - half); i <= Math.min(speedArr.length - 1, index + half); i++) { sum += speedArr[i]; count++; } return count ? Math.round(sum / count) : 0; }

另一个处理是过滤速度尖峰。比如两秒内速度从40飙到180,然后又降回50,这种基本是GPS漂移,直接丢弃。正常车辆不会出现这样的瞬时加速度,速度超过200km/h的段也建议丢弃,不考虑赛道场景的话基本可以判定为脏数据。

3.4 速度显示UI:整数、归零、低速状态识别

消息框里的数字建议只显示整数,不要带小数,用户扫一眼就能读数。速度为0时就显示“0 km/h”,不要显示空,否则会让人误以为数据没加载出来。还有一个细节值得做:低速区间,比如低于10km/h时,可以给数字加一个特殊CSS类做颜色变化,提示“停车/缓行”状态。

结合GPS时间戳,还可以进一步判断“静止超过多久”来识别等红灯或靠边停车。比如连续20秒以上速度接近0,基本就是路口停车或临时停靠,这类信息在回放统计里非常有用,可以在UI上打一个“停车”标签,比单纯一个数字直观得多。

4. 升级核心:播放速度调整与回放进度控制的完整实现

4.1 播放器状态收敛到一个player对象

升级版最核心的变化,是把回放状态收敛到一个player对象里:

const player = { startTime: rawTracks[0].time, duration: rawTracks[rawTracks.length - 1].time - rawTracks[0].time, currentTime: 0, speed: 1, playing: false, lastTick: 0 };

这里的currentTime永远是“相对起点的时间”,这样倍率调整、进度条跳转、暂停恢复都能统一操作。比如进度条拖到50%,就是currentTime = duration * 0.5;倍率调到4倍,就是每帧currentTime增量乘以4。整个播放器本质上就是一把时间刻度尺,不需要关心绝对时间戳,这是整套方案能支持升级的基础。

4.2 主循环:requestAnimationFrame + 时间戳定位

播放主循环用requestAnimationFrame而不是setInterval。requestAnimationFrame天然和屏幕刷新率同步,动画更顺滑,而且页面切后台时会自动暂停,不会造成时间累积误差。核心逻辑:

function tick(now) { if (!player.playing) return; const delta = player.lastTick ? (now - player.lastTick) : 0; player.lastTick = now; player.currentTime += delta * player.speed; if (player.currentTime >= player.duration) { player.currentTime = player.duration; renderFrameAt(player.currentTime); stop(); return; } renderFrameAt(player.currentTime); requestAnimationFrame(tick); }

renderFrameAt负责把“当前时间”转换成地图上的位置、速度和角度。这里用了二分查找定位轨迹段,因为轨迹点上万之后,线性遍历每帧都做会浪费性能:

function renderFrameAt(currentTime) { const result = getPositionAt(currentTime); if (!result) return; const { pos, angle, speed } = result; renderFrame(pos, speed, angle); updateProgressUI(currentTime); } function getPositionAt(currentTime) { const targetTime = player.startTime + currentTime; let lo = 0, hi = rawTracks.length - 1; while (lo < hi - 1) { const mid = (lo + hi) >> 1; if (rawTracks[mid].time <= targetTime) { lo = mid; } else { hi = mid; } } const prev = rawTracks[lo]; const next = rawTracks[Math.min(lo + 1, rawTracks.length - 1)]; const segDuration = next.time - prev.time; const ratio = segDuration > 0 ? (targetTime - prev.time) / segDuration : 1; const lng = prev.lng + (next.lng - prev.lng) * ratio; const lat = prev.lat + (next.lat - prev.lat) * ratio; const angle = calcAngle(prev, next); const speed = calcSegmentSpeed(prev, next); return { pos: new AMap.LngLat(lng, lat), angle, speed }; }

4.3 线性插值的边界:什么情况会切直线

上面用的是经纬度线性插值,近距离场景下精度完全够用,车看起来就是走直线。但有个前提:两个轨迹点不能隔太远。如果轨迹点间隔很稀疏,比如1分钟一个点、两点相距好几公里,线性插值会把U形弯道拉成斜线,轨迹走形非常明显。

这种场景就不要硬插值了。解决方案有两个:一是降低抽稀力度,让轨迹点尽量密;二是调用高德路径规划API把两点之间的实际道路补出来,但这样会引入服务端配额和费用问题,一般只在展示精度要求很高的业务中才做。我大多数项目走的是第一条路,简单可靠。

4.4 倍速切换:只改一个数字的几个细节

播放速度调整,实际上就是改player.speed。0.5倍、1倍、2倍、4倍四个挡位足够日常演示用:

speedBtns.forEach(btn => { btn.addEventListener('click', () => { const sp = parseFloat(btn.dataset.speed); player.speed = sp; speedBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); }); });

有个细节:切换倍率时不需要重置lastTick,因为下一次tick的delta会自然以新倍率累加。如果手动把lastTick置0也可以,requestAnimationFrame下一次回调会重新赋值,两种方式都安全。但注意不要直接在倍率切换时大幅修改currentTime,否则会造成明显的跳帧。

4.5 进度条拖拽与界面状态同步

进度条用input[type=range]实现,监听input事件实时更新。这里要区分暂停态和播放态:暂停时拖拽,只要改currentTime并渲染一帧就行;播放中拖拽,除了改currentTime,还要立刻渲染一帧,然后让后续播放从当前时间继续走:

progressEl.addEventListener('input', function () { const ratio = parseFloat(this.value) / 100; player.currentTime = player.duration * ratio; renderFrameAt(player.currentTime); });

input事件在拖拽过程中会不断触发,renderFrameAt内部会调用setPosition和setContent,整体性能要能跟上。如果轨迹段复杂、一帧计算超过几十毫秒,可以考虑节流:只在下一个动画帧渲染一次,而不是每次input都立刻渲染。

进度条旁边一般还会显示当前回放时间和总时长,格式化成HH:mm:ss:

function formatTime(ms) { const totalSec = Math.floor(ms / 1000); const h = Math.floor(totalSec / 3600); const m = Math.floor((totalSec % 3600) / 60); const s = totalSec % 60; return [h, m, s].map(v => String(v).padStart(2, '0')).join(':'); }

4.6 暂停、结束与重新播放的状态处理

暂停就是playing = false,恢复就是playing = true并重新走requestAnimationFrame(tick)。播放到终点有两种处理:直接停,按钮切换成“重新播放”;如果业务要求循环播放,把currentTime归零继续走。

播放结束的瞬间,消息框会固定显示最后一个点的速度,这个需要保证最后一次renderFrameAt正常执行。另外,暂停状态下如果用户拖进度条,要确保按钮状态还是“播放”而不是又触发一次播放,这里建议用一个简单的状态机字段,比如playingpausedfinishedseeking,防止拖拽和播放两个动作互相打架。

5. 实测踩坑记录:文档里不会写清楚的那几个点

5.1 Key、域名白名单与配额问题

高德JS API 2.0必须带Key加载,Key要求配置域名白名单。最常见的问题是本地开发用file协议打开页面,或者在内网IP测试,Key没把本地域名加进白名单,结果地图一片灰。解决办法是开发环境配置多个本地域名,用localhost测试没问题,但手机通过局域网IP访问时,要把当前IP也加进白名单。上线后如果发现地图加载失败,第一件事去控制台看Key的域名白名单和服务状态。

关于收费和配额,JS API个人开发者默认有免费配额,但服务端API,比如路径规划、地理编码这类,有QPS限制,并发一高就会被限流。大部分个人项目够用,但一旦商用或者流量起来,一定要先评估用量,不要等到线上服务被限流后才发现。

5.2 InfoWindow闪烁和内容更新的频率分层

前面说了要用isCustom: true,这还不够。实际项目里发现,每帧调用setContent即使HTML内容一模一样,也会触发重排重绘。优化办法是在setContent之前比较HTML字符串,如果速度值没变就直接跳过setContent,只调用setPosition。

更稳妥的做法是把更新拆成两个频率:Marker的setPosition走requestAnimationFrame的每帧更新,保证移动平滑;消息框的速度值用独立定时器,每秒更新4到10次就行。因为速度变化本身没这么频繁,这样既不影响平滑移动,又减少了不必要的DOM操作。这里能和前面4.2的主循环结合起来,在renderFrame里判断要不要更新速度内容。

5.3 脏轨迹预处理:乱序、重复、漂移点

真实采集数据不像Demo那么干净。有些设备会因网络重传导致同一个点上报两次,时间戳一样;有些点时间戳比前一个还小。播放前必须做预处理,否则会出现“车倒回去”“车在原地转圈”的问题:

function preprocessTracks(list) { const sorted = list .filter(p => p.lng && p.lat && p.time) .sort((a, b) => a.time - b.time); const result = []; for (let i = 0; i < sorted.length; i++) { const curr = sorted[i]; const prev = sorted[i - 1]; if (prev && ( curr.time === prev.time || (curr.lng === prev.lng && curr.lat === prev.lat) )) { continue; } result.push(curr); } return result; }

漂移点的过滤可以放到速度计算阶段,把瞬时速度低于0.1km/h且时间差很短的静止点合并,或者把计算出来的异常高速点直接标记为无效。

5.4 车辆朝向角度的计算与图标初始朝向

车辆Marker如果不旋转,车头永远朝上,看起来很不真实。角度计算我用的是前后两点的方位角:

function calcAngle(prev, next) { const p1 = new AMap.LngLat(prev.lng, prev.lat); const p2 = new AMap.LngLat(next.lng, next.lat); const rad = Math.atan2(p2.getLat() - p1.getLat(), p2.getLng() - p1.getLng()); return (rad * 180 / Math.PI + 90 + 360) % 360; }

这里假设图标车头朝上,也就是北方向。如果图标车头朝右,公式里的+90要删掉,换成直接(angle + 360) % 360。高德setAngle是顺时针旋转,所以从朝北转到朝东是90度。简单用经纬度差算角度,在城市短距离范围内精度足够,但在高纬度或大范围场景会不准,需要先转成墨卡托平面坐标再算。

5.5 地图视角跟随:setFitView和setCenter的抖动问题

回放过程中为了“跟着车走”,新手很容易每帧调用map.setFitView(marker)map.setZoomAndCenter,这两个API会触发放缩和视角切换。如果每帧都调用,地图视角会疯狂跳动,比车还快,用户直接看晕。

正确做法是:回放开始前先setFitView一次,把整条轨迹放进视野;回放中如果希望跟随车辆,平滑移动用map.panTo(pos),或者直接map.setCenter(pos),不要每帧都调FitView。如果轨迹跨度很大,setCenter会有跳跃感,建议加一个简单的缓动函数,在地图中心点上做线性插值,让视角平滑过渡。

5.6 移动端、小程序、React Native和Hermes的兼容差异

如果这个功能要放到微信小程序或App里,坑会变多。小程序没有JS API里这套InfoWindow和DOMOverlay,消息框一般得自己用cover-view或者原生组件叠加在地图WebView上,位置计算要监听地图中心点变化和缩放事件,手动换算屏幕坐标,工作量会大不少。

App端用高德SDK时,InfoWindow用法不同,Android上Marker的旋转angle属性只在部分SDK版本稳定。iOS上定位不准,多半要检查定位权限和精度设置,和轨迹回放本身关系不大,但会导致采集到的轨迹点本身就在飘,回放自然跟着歪。

如果项目用了React Native加Hermes引擎,接入高德地图类SDK要特别注意原生桥和新版JS引擎的兼容性,有些在JSC下正常的库,切到Hermes后要升级版本或重新构建,这个问题遇到一次就够折腾。

5.7 页面切后台再回来:单帧时间跳变

requestAnimationFrame在页面切后台时会暂停,回来后又继续。如果页面在后台停了几分钟,tick的delta会突然变成几分钟,车会“瞬移”到很后面的位置。解决办法是限制单帧增量:

const delta = Math.min(now - player.lastTick, 1000 / 30); player.currentTime += delta * player.speed;

这样最大增量不超过一帧的时间,后台回来后车辆只会往前进一点点,不至于瞬移。如果业务要求切回来直接定位到当前应处的位置,那就另说,但我做过的项目里,大多数客户都更希望回放稳定不跳变。

6. 再往外走一步:这套方案还能扩展出哪些能力

6.1 行程统计:总里程、均速、停车次数

回放本质上是在“浏览一段历史”,顺手就能把行程指标算出来。总里程可以用轨迹段距离累加:

const totalDistance = rawTracks.reduce((acc, p, i) => { if (i === 0) return acc; const prev = rawTracks[i - 1]; return acc + AMap.GeometryUtils.distance( new AMap.LngLat(prev.lng, prev.lat), new AMap.LngLat(p.lng, p.lat) ); }, 0);

平均速度、最高速度、停车次数也都挂在同一次遍历里算。停车次数判定规则是连续静止超过20秒算一次停车,用这个规则能过滤掉红绿灯短停和堵车缓行的干扰。算完后显示在回放页面的信息栏里,客户非常喜欢这类“附加价值”。

6.2 轨迹线与已跑段高亮的联动

轨迹线用Polyline画,最简单的实现是整条轨迹画一条浅色线,已跑过的部分再叠一条高亮线:

const fullLine = new AMap.Polyline({ map: map, path: rawTracks.map(p => [p.lng, p.lat]), strokeColor: '#cccccc', strokeWeight: 4 }); const playedLine = new AMap.Polyline({ map: map, path: [], strokeColor: '#2a7fff', strokeWeight: 6 });

高亮线每次更新path要注意性能,不要每帧setPath上万个点数组。改成只在经过一个新的轨迹点时追加一次点,比如用一个变量记住当前轨迹索引,每次插值越过一个原始点就playedLine.setPath(rawTracks.slice(0, currentIndex + 1)),这样动画播放期间几乎不产生额外CPU占用。

6.3 Loca可视化与Marker叠加的混合方案

高德Loca组件有路径动画能力,适合做“海量轨迹同时播放”的炫酷效果,但它本质是可视化图层,消息框这种DOM内容管不了。我建议的混合方案是:大规模轨迹用Loca做底层动画,重点车辆用Marker加InfoWindow做叠加层,让Loca动画回调去同步重点车辆的位置。这样既能做整体俯瞰,又能做单车详情,两种模式切换起来也自然。

如果同时播放的轨迹超过几十条,DOM类Overlay的性能会急剧下降,这时候一定要把底层轨迹渲染交给Canvas或Loca,不能全用Marker硬扛。

6.4 离线瓦片、弱网和性能优化的取舍

如果业务车辆经常出入地库、隧道、偏远地区,在线地图瓦片加载不动,回放就变成“白板放车”。可以提前把作业区域的瓦片下载到本地,或者接入高德的离线地图包。不过离线包需要按区域下载、做好版本管理,更新不及时的话,会看到新旧瓦片混着显示,比在线还难看。

性能优化方面还有几招可以叠加:轨迹线点数过多时使用Loca或Canvas绘制,而不是Polyline;Marker图标在缩放级别较小时换更小的图片;回放过程中禁用地图旋转、倾斜手势,减少无效重绘。这些细节合在一起,才是真正能上生产环境的回放模块。

我在实际项目里把这套方案落地过三回,一次是乘用车行车记录仪回放,一次是物流车队的干线轨迹,还有一次是做景区观光车的实时调度。每次遇到“消息框不跟车”或者“进度条拖完速度显示不对”的问题,最后都能绕回到同一个结论:回放不是动画片,它是把时间和空间一一对应起来的播放器。把时间模型搞对,倍速、进度拖拽、消息框跟随就都是水到渠成的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 13:28:01

RTSP协议从入门到实现:状态机、RTP推流与流媒体服务器实战

很多做视频流媒体的人接触到的第一个协议就是RTSP&#xff0c;尤其是买了个海康、大华的摄像头&#xff0c;第一件事就是找RTSP地址往VLC里一贴&#xff0c;画面出来了。但如果你只是停留在"拿地址播放"这个阶段&#xff0c;一旦要自己动手写一个RTSP服务器&#xff…

作者头像 李华
网站建设 2026/9/15 13:27:46

抛硬币小程序实战:uni-app动画、随机数公平性与流量主审核全解析

简介&#xff1a;这份资源是一套可直接运行的「抛硬币」微信小程序源码&#xff0c;主要面向想快速上手小程序开发、或希望了解流量主变现方式的开发者。小程序提供随机正反面结果&#xff0c;适合日常决策、趣味互动等场景&#xff0c;结构简洁&#xff0c;无需配置合法域名&a…

作者头像 李华
网站建设 2026/9/15 13:25:08

告别反复重装Anaconda:安装配置与环境管理避坑指南

现在的Anaconda三天两头重装&#xff1f;不是你的错&#xff0c;是装的时候少做了这几步各位做Python开发、搞数据分析、跑深度学习的朋友&#xff0c;摸着良心想想有没有经历过这个剧本&#xff1a;装好了Anaconda3&#xff0c;用了一两个星期&#xff0c;发现conda命令找不到…

作者头像 李华