
简介百度地图在线/离线双模式下的实时轨迹绘制源码面向需要在 Web 端展示车辆行驶路线的 GIS 开发者与地图应用学习者。代码实现轨迹点动态连线、车头方向随路线实时旋转并支持角度偏移修正解决在线与离线场景下轨迹回放不连贯、方向指示不准等常见问题。压缩包共 2000 个文件以 png 图片和 js 脚本为主辅以 css 样式、gif 动图、html 页面及字体图标等资源整包约 120.47MB覆盖地图控件、标注样式与离线所需的静态文件。已有 3853 人学习下载适合有一定 JavaScript 基础、希望快速搭建地图轨迹功能的开发者参考。资源为作者百度地图系列博客配套源码读者可对照在线效果地址直接运行调试也可基于现成代码做二次开发节省从零研究地图 API 与调试定位逻辑的时间。1. 项目背景为什么要做实时轨迹绘制做运动类App或者车联网项目轨迹记录基本是标配功能。用户打开地图看着自己的位置一点点画出一条线这个交互既直观又有成就感。百度地图作为国内使用率最高的地图服务之一其Android SDK提供了完整的定位和地图渲染能力在线模式下可以轻松实现轨迹绘制。但一旦到了野外、地下车库或者信号盲区网络断掉之后在线定位和在线地图全都罢工这时候离线能力就成了救命稻草。我这次做的这个项目核心目标很明确在Android端实现一套同时支持在线和离线两种模式的轨迹记录方案。在线模式走百度定位SDK加网络地图瓦片离线模式走GPS纯定位加本地预加载的离线地图包两条链路共用一套轨迹绘制逻辑尽量做到无缝切换。整个项目以源码形式交付方便二次开发和集成。适合谁参考如果你正在做运动健康类App、户外工具类应用、车辆管理后台或者单纯想研究百度地图SDK的定位与绘图机制这篇文章应该能帮你省掉不少自己踩坑的时间。我下面会把整体设计思路、核心代码实现、常见坑位一次讲清楚。2. 整体设计在线离线双链路如何架构2.1 在线与离线的本质区别先说清楚在线和离线到底差在哪里。在线模式下定位靠的是网络定位加GPS混合地图瓦片实时从百度服务器拉取坐标系转换、路网匹配这些重活都交给服务端客户端只需要拉数据、画数据。离线模式下地图瓦片需要提前下载到本地定位只能依赖GPS模块或者设备已有的基站缓存坐标系转换也得在本地完成。这两个模式的核心差异决定了架构必须分层。最底层是定位服务中间是地图数据源上层是轨迹渲染。定位服务这一层要做成可切换的抽象接口地图数据源要能感知当前网络状态轨迹渲染则是纯UI逻辑不管数据从哪来画线的方式是一致的。2.2 模块划分与技术选型整个工程我分成了四个模块定位模块封装百度定位SDK同时保留系统GPS回调作为离线备用地图模块管理MapView生命周期处理在线离线地图源切换轨迹模块维护轨迹点集合负责Polyline渲染和局部刷新状态管理监听网络变化自动切换定位策略和地图源技术选型上定位用的百度定位SDK版本用的当前比较新的release版本地图用的百度地图SDK轨迹绘制直接用SDK自带的Polyline接口不需要引入额外的绘图库。SQLite用于轨迹点的本地缓存防止应用被杀掉丢数据。选百度这套方案而不是高德或者Google主要是因为百度离线地图包的支持力度和国内开发者文档的完整度都不错。尤其是离线地图这块百度提供了城市维度的离线包下载接口省去了自己切瓦片的麻烦。2.3 源码工程结构一览工程是标准的Android Studio项目Gradle构建核心源码目录大概长这样app/src/main/java/com/example/trackdemo/ ├── location/ │ ├── LocationService.java // 定位服务封装 │ ├── OnlineLocationStrategy.java // 在线定位策略 │ └── OfflineLocationStrategy.java// 离线定位策略 ├── map/ │ ├── MapManager.java // 地图生命周期管理 │ └── OfflineMapHelper.java // 离线地图包管理 ├── track/ │ ├── TrackRecorder.java // 轨迹记录器 │ ├── TrackPoint.java // 轨迹点数据模型 │ └── TrackOverlay.java // 轨迹覆盖物渲染 └── MainActivity.java // 主界面整个结构不算复杂核心就两百多KB的源码量关键在于链路打通后各模块各司其职后续加功能也好扩展。3. 核心实现定位模块全解析3.1 百度定位SDK接入基础百度定位SDK的接入不算复杂但有几个容易忽略的细节。第一步在Gradle里引入依赖然后把申请的AK配置到AndroidManifest里同时声明定位权限。这里要注意Android 6.0以上动态权限申请不能省我习惯在Application启动阶段就检查一遍缺权限直接弹申请框省得后面定位失败排查半天是权限问题。核心初始化代码如下// 在Application或MainActivity中初始化 public void initLocation(Context context) { LocationClient.setAgreePrivacy(true); // 隐私合规这个不写定位会失败 mLocationClient new LocationClient(context); LocationOption option new LocationOption(); option.setLocationMode(LocationMode.Hight_Accuracy); option.setCoorType(bd09ll); // 使用百度经纬度坐标 option.setScanSpan(1000); // 1秒回调一次 option.setNeedNewVersionRgc(true); option.setOpenGnss(true); // 开启GPS option.setLocationNotify(true); mLocationClient.setLocOption(option); }setAgreePrivacy(true)这是新版SDK强制要求的忘记写的话定位会直接返回错误码这个坑在新手里面出现率极高。定位回调监听器里做的事情就比较机械了拿到BDLocation判断定位类型把经纬度、时间、速度、方向这些字段打包成TrackPoint丢给轨迹模块。3.2 离线GPS定位兜底方案在线定位好说百度SDK一套全搞定。但如果网络断了百度定位的精度会明显下降因为网络定位失效纯靠GPS的话SDK内部的某些优化逻辑也会受影响。为了稳妥我在离线场景下直接绕开百度SDK的混合定位走系统LocationManager的GPS Provider。public void startOfflineLocation() { LocationManager lm (LocationManager) getSystemService(LOCATION_SERVICE); if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) { lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 2, mOfflineListener); } }注意requestLocationUpdates的第二个参数是时间间隔、第三个是距离间隔我这里设置的是1秒和2米适合步行和骑行速度如果是车载场景距离间隔可以放宽到5米甚至10米减少无效回调省电。离线定位的回调拿到的是Location对象坐标系是WGS84而百度地图用的是BD09这里必须做坐标转换否则轨迹画出来会偏几十米到几百米。这个转换算法百度官方文档有网上也有现成的工具类建议直接copy官方实现别自己拍脑袋写公式精度验证成本太高。3.3 在线离线自动切换逻辑自动切换的逻辑我放在一个LocationStrategySwitcher里它监听网络状态变化发现网络断开就停掉百度定位Client切换成GPS监听网络恢复后反向操作。切换时要处理的细节不少停掉旧策略要释放监听器防止回调重叠切换瞬间的坐标可能跳变轨迹绘制时要做点间距过滤百度定位Client的重启有耗时要保证新策略先启动再停旧的这块我用了一个简单的状态机ONLINE、OFFLINE、SWITCHING三种状态切换中收到的定位数据先缓存但不绘制切换完成后再把缓存数据一次性补绘保证轨迹连续不断线。4. 轨迹绘制Polyline渲染与性能调优4.1 轨迹覆盖物的基本用法百度地图SDK画轨迹非常直接Polyline覆盖物配坐标点列表就能出线。基础用法是定义一个OverlayOptions把点列表塞进去再add到Map上。public void drawTrack(ListLatLng points, MapView mapView) { if (points null || points.size() 2) return; OverlayOptions options new PolylineOptions() .points(points) .width(8) .color(0xFF3385FF) .alpha(0.8f); mapView.getMap().addOverlay(options); }但这里面有个问题如果每秒都往Map上加一次Overlay轨迹稍微长一点就会卡。因为每次addOverlay都相当于让地图渲染层从头计算一遍整条线的绘制点越多越卡。我之前测试过跑到上百个点之后帧率掉得很明显。4.2 增量绘制只画新增的那一段解决卡顿的思路是增量绘制。不要把整段轨迹每次全量重画而是只把新增点连成线段追加到地图上。网上很多教程没讲清楚这点最后做出来的轨迹Demo玩玩可以真上了生产环境必卡。我的做法是维护一个mSegmentCount计数器每次定位回调拿到新点位就尝试连接新线段独立add一个Polyline覆盖物。public void appendPoint(LatLng point) { if (mTempPoints.size() 0) { mTempPoints.add(point); return; } mTempPoints.add(point); // 攒够2个点就画一条新线段 if (mTempPoints.size() 2) { OverlayOptions segment new PolylineOptions() .points(mTempPoints) .width(8) .color(0xFF3385FF); mMap.addOverlay(segment); mSegments.add(segment); // 保留最后一个点作为下一条线段的起点 LatLng last mTempPoints.get(1); mTempPoints.clear(); mTempPoints.add(last); } }这样做的好处是每次地图渲染只多算一条两点线段开销几乎可以忽略。缺点是地图上有大量Overlay对象如果是超长时间记录两小时以上Overlay数量会破万。不过实际测试下来百度地图SDK对轻量Overlay的承载能力还可以只要别同时添加复杂Overlay一万个以内问题不大。4.3 轨迹点抽稀与坐标漂移过滤GPS数据不是每一条都能直接画的静止时卫星信号抖动会造成坐标小范围漂移画出来的轨迹会变成锯齿状。这里必须做两个处理漂移过滤和抽稀。漂移过滤的逻辑简单粗暴计算两点之间距离如果小于阈值比如1米就说明是原地抖动直接丢弃如果大于一个最大阈值比如100米说明可能发生了跳点也丢弃等待下一帧补偿。抽稀用经典的Douglas-Peucker算法这个算法网上有现成实现把一条轨迹的冗余点去掉保证视觉形状不变的同时大幅减少点数量。不过要注意抽稀只能用于轨迹展示不能用于轨迹记录因为记录的原始轨迹是给回放、统计用的抽稀后数据就不准了。提示如果做的运动类App需要计算配速、里程一定要在原始轨迹上算展示层才用抽稀后的数据。5. 离线地图离线包的下载与管理5.1 离线包策略城市维度还是自定义区域百度地图SDK自带了离线地图功能按城市维度下载离线包。这种方案的优势是接入简单MKOfflineMap几行代码搞定缺点是灵活性差城市包动辄几十MB而且跨城市边界就断图。我自己的项目场景是有固定活动区域比如某个园区、某个景区所以我用的是自定义离线区域方案。通过MKOfflineMap的createOfflineMap手动指定下载范围把指定矩形区域的地图数据拉到本地活动范围外再走在线模式。5.2 离线包加载与使用离线包的下载和管理在OfflineMapHelper里实现public void downloadOfflineRegion(LatLng southwest, LatLng northeast) { MKOfflineMap offlineMap new MKOfflineMap(); // 指定下载范围单位是经纬度 MKOLUpdateElement element offlineMap.downloadRegion(southwest, northeast); // 监听下载进度回调 offlineMap.setOnOfflineMapListener((type, state) - { if (type MKOfflineMap.MKOfflineMapDownloadStateFinished) { runOnUiThread(() - Toast.makeText(context, 离线包下载完成, Toast.LENGTH_SHORT).show()); } }); }这里需要注意自定义离线区域的下载不是瞬间完成的用户在弱网环境下可能需要等很久。我建议在下载页面显示进度条并且把下载任务放到WorkManager里做避免应用切后台下载被系统杀掉。离线包下载完成后地图SDK会自动优先使用本地数据不需要手动切换渲染源。这一点百度做得还是不错的开发者只需要确保MapView在初始化的时候设置了正确的MapStatus离线数据就能正常显示。5.3 离线模式下的轨迹补偿离线模式最容易出问题的是轨迹断裂比如GPS信号丢失三十秒再恢复时位置已经跳出去几百米。如果直接连线轨迹就会穿墙过河丑得没法看。我采用的补偿策略是信号丢失超过15秒就认为断线恢复后不直接连线而是画一条虚线连接断点并在虚线上标注“信号中断”这样用户可以直观看到轨迹哪一段是估算的。虚线用百度SDK的DashLine类型Polyline设置setDashLine(true)即可。这种细节虽然看起来小但用户对轨迹记录的信任度很大程度上就是这些细节堆出来的。你要让用户知道哪段路是真实走过的哪段是估算的不能糊弄。6. 权限、兼容性与其他硬骨头6.1 不同手机定位功能测试实录做定位类应用最头疼的就是手机兼容性。不同厂商对GPS和网络定位的处理逻辑差异极大尤其是国产ROM对后台定位的限制五花八门。我在这项目里用四台设备做了实测结果如下手机型号系统版本在线定位精度离线GPS精度后台定位表现小米12Android 123-5米5-8米需要加后台定位权限华为P40HarmonyOS 22-4米4-6米需设置应用为电池优化白名单三星S21Android 123-5米6-10米正常一加9Android 122-4米5-7米偶尔被杀需加前台服务实测下来后台定位被系统杀掉是个绕不开的坎。华为和小米都对长时间后台定位做了限制解决思路只有一个开前台服务给用户一个常驻通知栏的通知告诉系统这个任务还在跑。虽然这个体验有点烦但这是当前Android生态下的最优解。另外还有一个细节小米手机在开发者选项里有一个“开启GPS定位时同时使用网络定位”的开关部分机型默认是关闭的会导致离线定位模式下完全没有网络辅助信息冷启动定位时间会拉长到三十秒以上。这个问题属于系统级应用层没法强制修改只能在用户引导里提示一下。6.2 常见问题速查表问题现象可能原因解决方案定位一直回调error 61没调setAgreePrivacy(true)检查隐私合规接口离线地图加载白屏离线包下载不完整删除离线包重新下载轨迹断线后有飞线没做信号丢失检测用15秒阈值断线并画虚线轨迹整体偏移几十米适配了GCJ02坐标确认SDK坐标类型是bd09llApp切后台轨迹断掉系统杀了后台进程拉起前台服务保活多设备同时记录轨迹错乱相同AK并发限制轨迹数据增加设备ID字段6.3 运行性能实测数据最后放一组我这个项目的实测数据方便你对性能有个概念。设备是小米12Android 12系统记录时间90分钟骑行场景原始轨迹点采集量约5400个抽稀后展示点数约1200个渲染Overlay数量约1300个线段级90分钟平均帧率50fps地图界面内存占用增加约35MB主要是地图纹理缓存90分钟耗电量约11%亮屏GPS地图渲染总体来说这个方案在性能和体验上达到了一个相对均衡的状态。如果你需要记录更长时间比如全天巡逻或者长途货运耗电这块还要再优化比如降低定位频率、关掉不必要的传感器。7. 源码使用说明与二次扩展建议7.1 快速跑通源码的步骤拿到源码之后最快跑通整个项目的步骤是这样用Android Studio打开工程等待Gradle同步完成在百度地图开放平台申请AK注意选择Android平台填对包名和SHA1值将申请到的AK填入AndroidManifest.xml的meta-data节点中在真机上运行首次启动会请求定位权限全部允许看到地图加载后点击“开始记录”按钮移动设备观察轨迹绘制整个跑通过程五分钟内可以完成。如果卡在同步阶段大概率是Gradle版本或仓库地址问题对照自己的AS版本微调build.gradle即可。7.2 二次开发的方向这套代码除了直接使用还预留了几个扩展点。如果你想把轨迹记录做成独立功能集成到现有项目TrackRecorder和MapManager可以单独拿出来用。想做轨迹回放在TrackOverlay里加一个ReplayController就行。想做历史轨迹管理SQLite存储层已经准备好了再加个查询界面就可以了。我建议你在实际使用之前先把定位策略切换的阈值调一调。网络断多久判定为离线GPS信号弱到什么程度切换到网络定位这些参数在不同场景下最优值差异很大要结合业务场景反复测试。这一步是打磨体验的关键也是后面用户反馈最多的地方。我做这套工程的初衷就是希望有人能把这块基础能力直接拿去用少走一点我当时踩过的弯路。本文还有配套的精品资源点击获取