ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Android导航Demo:定位、路线规划与模拟导航集成实战

Android导航Demo:定位、路线规划与模拟导航集成实战 简介一份面向Android开发者的百度地图导航功能工程资源覆盖从开发包集成、权限声明、位置获取、起终点设置、路线规划到导航引导的完整链路适合需要快速为已有应用接入地图导航模块的中级开发者。压缩包为rar格式共1343个文件约22.55MB内含大量布局与配置文件、图标素材、Java源码、依赖库、AIDL接口、JSON数据、Gradle构建脚本以及可直接运行的调试安装包基本覆盖一个Android项目从源码资源到编译产物的各个层面目录结构清晰。已有520人学习下载。通过该工程可对照理解百度地图位置监听、路线检索、路径绘制、语音播报等核心API的实际用法同时可基于现成的界面与构建配置直接运行调试并便捷地扩展驾车、步行、骑行等出行方式对于排查SDK接入、定位失败、路线不显示等常见问题也有直接参考价值是地图导航模块一个完整的起步模板。1. 导航功能不是从零写的这份Demo把集成路线走通了拿到这份工程之前我也有一个执念App 里做导航要么是调起外部地图应用要么是把路线规划算法搬进项目里自己算。把工程跑起来之后才发现常规做法远没有这么重——地图展示、实时定位、路线规划、导航状态推进每一层都拆成独立模块再通过事件回调串成一条链路。这份 Demo 适合两类人一类是业务系统里需要“路线预览”和“模拟导航”能力的 Android 开发另一类是做物流配送、外勤打卡类项目想快速把定位和导航能力嵌进自己 App 的团队。下面我按工程结构、核心代码、落地细节和避坑记录逐个拆开讲。2. 工程骨架拆解从一个按钮到一条可导航路线的完整链路2.1 目录结构每个包只负责导航链路里的一环工程不是那种“所有代码堆在一个 Activity 里”的老式写法打开根目录就能看出模块边界NavDemo/ ├── app/ │ ├── build.gradle │ └── src/main/ │ ├── AndroidManifest.xml │ ├── java/com/demo/nav/ │ │ ├── MainActivity.kt │ │ ├── map/MapViewHolder.kt │ │ ├── location/LocationHelper.kt │ │ ├── route/RoutePlanService.kt │ │ ├── nav/NavigationManager.kt │ │ └── widget/RouteInfoPanel.kt │ └── res/ └── gradle/逐层说职责。MainActivity只做两件事初始化地图视图把四个子模块组装起来MapViewHolder负责地图生命周期包括创建、销毁和相机控制LocationHelper封装定位 SDK 的启动停止和权限申请对外只暴露一个“位置更新”回调RoutePlanService接收起终点坐标发起路线规划请求并解析结果NavigationManager是整个链路的调度者持有状态机把路线数据交给地图绘制同时接收导航事件回调RouteInfoPanel是底部信息面板展示剩余距离、剩余时间。这样一个包里只有一个文件看着简单但对于接入到自己项目里反而更方便——想删掉语音播报就只动NavigationManager想换定位策略就只改LocationHelper不需要在一千行的 Activity 里找逻辑。2.2 技术选型地图 SDK、定位、路线规划三者怎么协作先说清楚一个常见的误解路线规划不是地图 SDK 的隐藏功能而是独立的服务只是地图 SDK 通常会附带这个能力。这里的 NavMap SDK 泛指国内常见的移动端地图 SDK接口细节可能略有差异但套路一致。工程里三者协作的时序是这样的MainActivity创建地图后先启动定位拿到当前经纬度作为路线起点用户输入终点后RoutePlanService带着起终点坐标去请求路线返回的路线数据被拆成两个部分——一条用于地图绘制的Polyline点串一组用于导航状态机的“转向指令”NavigationManager进入导航中状态后开始监听定位回调把真实位置投射到路线附近同时每走到一个拐弯点就触发一次语音播报和面板更新。用户点击“开始导航” ↓ RoutePlanService.requestRoute(origin, destination) ↓ 返回 DriveRouteResult ↓ NavigationManager.startNavigation(route) ↓ 订阅定位回调 启动模拟导航 ↓ 按路段索引推进播报 → 更新时间 → 判断到达这段链路里最容易忽略的是“定位来源”和“路线坐标”不是同一套坐标系大部分地图 SDK 内部已经做了转换但如果你自己对接第三方定位 SDK就一定要确认坐标系是 GCJ-02 还是 WGS-84否则画出来的路线会整体偏移一百米左右。2.3 生命周期管理导航对象跟着页面走而不是跟着进程走地图是重资源SDK 都要求在 Activity 的onResume、onPause、onDestroy里调用对应生命周期方法。工程里MapViewHolder做了这件事class MapViewHolder(private val mapView: MapView) { fun onResume() { mapView.onResume() locationHelper.start() } fun onPause() { mapView.onPause() locationHelper.stop() } fun onDestroy() { mapView.onDestroy() NavigationManager.instance.release() } }说明定位如果不在onPause停掉退出页面后定位回调还会持续上报造成耗电和内存泄漏。这里把LocationHelper.start/stop和地图生命周期绑在一起可以确保页面不可见时定位链路整体挂起。NavigationManager是单例但加了一个release()方法清理回调监听否则跨页面持有 Activity 里的视图引用是老项目最常见的崩溃来源之一。3. 核心链路代码复盘定位、路线规划与导航状态机3.1 定位封装从权限申请到坐标回调LocationHelper是个典型封装调用方不关心 SDK 细节只接一个回调class LocationHelper(private val context: Context) { private var listener: ((LatLng) - Unit)? null fun setOnLocationChanged(listener: (LatLng) - Unit) { this.listener listener } fun start() { if (!hasPermission()) { requestPermission() return } // 间隔1秒回调一次移动超过10米也触发一次 NavMapSDK.startLocation( interval 1000L, distanceFilter 10f, callback { latLng - listener?.invoke(latLng) } ) } fun stop() { NavMapSDK.stopLocation() } private fun hasPermission(): Boolean { return ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) PackageManager.PERMISSION_GRANTED } }参数说明interval是定位回调的时间间隔单位毫秒导航场景下 1000 毫秒是比较合适的折中太密耗电太疏会导致转弯播报滞后distanceFilter表示移动多少米才触发下一次回调设成 10f 能过滤掉定位漂移带来的抖动。这里有个细节——没有权限时我没有立刻弹申请而是先返回权限结果通过onRequestPermissionsResult再调一次start()。很多新手在这里踩坑权限回调里直接判断granted就继续走忘记重新触发定位启动。3.2 路线规划把起终点坐标变成一条可行驶的路线路线规划核心代码在RoutePlanServiceclass RoutePlanService { fun requestRoute(origin: LatLng, destination: LatLng, callback: (DriveRoute?) - Unit) { NavMapSDK.routeSearch().driverRoute( origin origin, destination destination, wayPoints emptyList(), strategy RouteStrategy.SHORTEST_TIME ) { result, error - if (error ! null) { callback(null) returndriverRoute } // 取第一条推荐路线 val route result?.routes?.firstOrNull() callback(route) } } }说明这里把“选路策略”直接写死了是SHORTEST_TIME实际项目中我一般会把策略做成一个下拉选项让用户选“最快”“最短”“避开高速”。注意wayPoints参数传空列表表示普通起终点导航如果用在外勤系统里这里可以塞途经点列表实现多点配送路径。路线规划返回的DriveRoute不是一个孤立的点串它里面带着latitudeList路线形状点、stepList转向指令列表、distance总长度、duration预计耗时。绘制到地图上只取latitudeList但导航播报必须遍历stepList两者要区别使用拿到对象后别一股脑扔给MapAdapter。3.3 导航状态机空闲、规划中、导航中、到达导航最怕“状态乱”。用户反复点击开始、退出、重新规划时如果不用状态机约束会出现语音重复播报、路线重叠绘制、定位还在跑但页面已经是结果页等怪现象。工程里的NavigationManager用枚举管理状态enum class NavState { IDLE, // 无任务 ROUTING, // 正在请求路线 NAVIGATING, // 导航中 ARRIVED // 已到达 }class NavigationManager private constructor() { private var state NavState.IDLE fun startNavigation(route: DriveRoute) { if (state NavState.NAVIGATING) { stopNavigation() // 先回收旧任务 } state NavState.NAVIGATING routeIndex 0 // 开始模拟导航每秒推进一步 simulateNavigation(route) } fun stopNavigation() { state NavState.IDLE NavMapSDK.stopSimulate() listener?.onNavigationStopped() } }状态机的意义在于任何外部操作都先判断当前状态比如“开始导航”按钮在ROUTING和NAVIGATING状态下可以直接复用不需要针对每个入口加布尔开关。stopNavigation()里我加了一个先导动作确保重复点击时旧任务一定被清理不会出现两个定时器同时在推前进位置。4. 导航体验落地模拟导航、语音播报与实时纠偏4.1 模拟导航没有真机也能演示完整跑线流程开发导航功能最头疼的是测试——总不能在真机上真的开全程车。工程里做了一个很实用的设计模拟导航模式给定一个速度让“假位置”沿路线匀速前进。private fun simulateNavigation(route: DriveRoute) { val points route.latitudeList NavMapSDK.startSimulate( points points, speed 60f, // 模拟行驶速度 km/h onLocationTick { position - // position 是当前在路线上的插值坐标 updateCamera(position) checkAndHandleTurn(position) updateRoutePanel(position) }, onArrived { state NavState.ARRIVED listener?.onArrived() } ) }说明startSimulate是地图 SDK 提供的能力传入整条路线点串SDK 会按照speed参数在点与点之间插值自动产生平滑的位置上报。这里速度我给的是 60km/h适合演示如果你给调度车辆做轨迹预览可以把速度调成 20km/h 看路口细节。onLocationTick回调里的checkAndHandleTurn(position)是播报的关键逻辑每次回调都判断当前位置距离下一个转向点还有多远小于阈值就触发播报并切换索引。4.2 到达判断与纠偏不要只比“距离终点小于N米”到达判断是导航功能里最容易被做假的点。简单实现往往是“距离终点小于 50 米就算到达”但实际路网里终点的 GPS 信号可能漂移或者路线终点和车实际能停的位置有偏差导致导航还没结束就弹出到达页。工程里的到达逻辑参考了 SDK 自带的状态回调自己只做兜底private fun checkArrive(position: LatLng, route: DriveRoute) { val remainDistance route.stepList.sumOf { step - // 当前路段剩余 后续路段总长 step.remainDistance(position) } // 同时满足“剩余距离近似0”和“接近路线终点”两个条件 if (remainDistance 30f position.distanceTo(route.destination) 60f) { state NavState.ARRIVED listener?.onArrived() } }逻辑说明两个条件必须同时成立避免单个条件误判。比如路线终点恰好在一片 GPS 信号遮挡区位置漂移让“距终点”小于 60 米但剩余距离还很长此时不会误报到达。反过来如果剩余距离已经很小但真实位置离终点还有 100 米因为路线结束点在辅路也不会提前结束。这套双条件写法在真实路测里比单一阈值稳定得多。4.3 语音播报把转向指令转成可听懂的路口提示语音播报在工程里没有接大而全的 TTS 服务商而是通过NavigationManager内部维护一个指令队列到点后取出一条文案交给系统 TTSprivate fun checkAndHandleTurn(position: LatLng) { val currentStep steps[routeIndex] if (position.distanceTo(currentStep.turnPoint()) 80f) { // 到播报点朗读指令 tts.speak( 前方${currentStep.action}${currentStep.roadName}, finish { routeIndex listener?.onStepPassed(routeIndex) } ) } }参数说明turnPoint()是转向指令对应的导航点80f是提前播报的半径车速越快这个值要适当放大否则语音还没念完路口已经过去了。路口播报之后立刻滚动routeIndex保证下一次播报用的是下一个转向点。这里有个很容易漏的坑如果用户中途偏航routeIndex必须重置否则播报永远停留在旧路线上——下一章的避坑手册里我把这条列在了第一位。5. 避坑手册权限、生命周期与偏航恢复的五个常见问题5.1 高频故障现象、原因、解决现象原因解决地图正常路线画在地图之外定位坐标与地图 SDK 坐标系不一致常见于先接了一个 WGS-84 定位源再叠加地图确认坐标系统一用地图 SDK 自带的坐标转换接口或换用 SDK 内置定位点击开始导航后没有任何反应路线请求异步返回为空因为终点坐标非法或超出服务区在RoutePlanService回调里先判断route null弹 toast 并回退状态为IDLE不要自动跳到导航退出页面后语音还在播报NavigationManager是单例生命周期没有随页面销毁在页面onDestroy里调用stopNavigation()并注销 TTS 监听模拟导航一到路口就“飞线”定位漂移把位置拉出路线更新相机时直接跟随漂移点对onLocationTick的结果做路线吸附取路线投影点作为展示坐标第二次进入页面出现两个地图Activity 重建时MapViewHolder没有在onCreate里做状态恢复在MapViewHolder保存经纬度与缩放级别重建后调用mapView.onRestoreInstanceState恢复5.2 排查技巧用最小链路定位导航问题导航功能一旦出了问题最忌讳的是直接在真机上开着车看日志。我在跑这个工程时有一个固定流程先确认“定位有没有上报”再确认“路线有没有画出来”最后才看导航状态推进。如果定位回调一直为空优先查权限和 SDK Key如果路线能画但不能开始导航查NavigationManager.state是否停在ROUTING如果模拟导航不动查startSimulate的points是否被传成空列表。一个更省事的做法是在NavigationManager里加一个 debug 日志开关把每次状态切换打印出来private fun logState(from: NavState, to: NavState) { if (BuildConfig.DEBUG) { Log.d(NavTag, state: $from - $to) } }做法说明状态机的好处在这里体现得特别明显——只要状态切换是有序的链路就是健康的如果发现日志里出现NAVIGATING - NAVIGATING这样自己切自己的情况说明有外部调用没有经过stopNavigation()直接定位到那个入口即可不用把整个页面逻辑翻一遍。6. 二次开发把这套导航工程改成自己业务里的轮子拿到工程之后最常见的一个改造诉求是“把路线预览放在下单详情里”。做法是把第 3 章里规划的链路从MainActivity中抽出来改成一组 fagment先请求路线画一条不可交互的Polyline再根据用户点击进入完整导航。这里有一个轻量改法只动NavigationManager把它暴露的监听器扩展成业务需要的回调集合interface NavEventObserver { fun onStepPassed(index: Int) {} fun onArrive() {} fun onProgress(distance: Float, duration: Long) {} }NavigationManager内部维护一个Observer列表业务方按需注册。比如配送 App 只需要onProgress来实时回传位置不需要语音播报那就只实现这一个方法。这样做比在单例里写业务回调更干净也方便单元测试——测试时可以注入一个假的NavEventObserver直接验证状态机每一步推演是否正确。改造前先想清楚两件事第一路线规划是网络请求回调线程可能不在主线程所有 UI 更新要自己post回主线程第二业务系统里如果已经有自己的账号体系和本地缓存不要把定位数据和路线数据也一股脑放进去建议单独建一个NavRepository只负责导航链路的数据存取这样将来换地图 SDK 时只改仓储实现其他层完全不动。我从这个 Demo 里学到的最大一条教训是导航功能不要从“能不能走通”开始验证要从“状态切换是否闭环”开始验证。从那以后每次接手类似项目我都先画一遍状态图再跑一遍最小可运行链路确认IDLE - ROUTING - NAVIGATING - ARRIVED每一步都有日志可查才敢往上叠业务逻辑。这套笨办法帮我少踩了好多坑希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表