8、导航应用开发:CarNavigationManager、导航指令与路况、地图集成(Google Maps)、位置权限处理

导航,可以说是车载系统里最核心的应用之一。你想想看,用户开车时最离不开的就是导航。今天我们就来聊聊在 Android Automotive OS 上怎么把导航做好。

说实话,车载导航和手机导航差别挺大的。手机导航你随便装个 App 就能用,但在车上,你得考虑驾驶安全、屏幕交互、还有和车辆硬件的配合。我个人习惯把车载导航开发分成四个模块:地图集成、导航管理、路况处理、权限控制。咱们一个一个来。

核心要点:Android Automotive OS 的导航开发,本质上是把手机端的导航能力,安全地迁移到车载环境中。CarNavigationManager 就是连接你的导航 App 和车载系统的桥梁。

8.1 CarNavigationManager:导航的“总指挥”

CarNavigationManager 是 Android Automotive OS 提供的一个系统服务。它的作用,说白了就是让你的导航 App 能和车载系统“对话”。

我在项目中遇到过一个问题:导航 App 启动后,仪表盘上不显示导航信息。后来发现,就是没有正确注册 CarNavigationManager。嗯,这里要注意,这个注册过程是必须的。

// 获取 CarNavigationManager 实例
Car car = Car.createCar(context);
CarNavigationManager navManager = (CarNavigationManager) car.getCarManager(
    android.car.Car.CAR_NAVIGATION_SERVICE
);

// 注册导航状态监听
navManager.registerNavigationStatusListener(new CarNavigationManager.CarNavigationStatusCallback() {
    @Override
    public void onNavigationStateChanged(int state) {
        // state: 0=未导航, 1=导航中, 2=重新规划路线
        if (state == 1) {
            // 导航已开始
        }
    }

    @Override
    public void onInstrumentClusterNavigationInfoChanged(
            CarNavigationInstrumentCluster clusterInfo) {
        // 仪表盘需要显示导航信息时触发
        // 这里可以发送导航指令到仪表盘
    }
});

这段代码看起来简单,但有个坑。我曾经在项目里忘记在 onDestroy 里注销监听,结果导致内存泄漏。所以记得在生命周期结束时调用 navManager.unregisterNavigationStatusListener()

个人经验:CarNavigationManager 的注册最好放在 Service 里,而不是 Activity。因为导航服务需要长时间运行,Activity 可能会被销毁重建。我习惯用一个前台 Service 来管理导航生命周期。

8.2 导航指令与路况:让用户知道怎么走

导航指令,就是告诉用户“前方300米右转”这样的信息。在 Automotive OS 上,这些指令不仅要显示在屏幕上,还要能发送到仪表盘和 HUD(抬头显示)。

我记得有一次,测试反馈说导航语音和仪表盘显示不一致。查了半天,原来是导航指令的发送时机不对。来看正确的做法:

// 创建导航指令
CarNavigationInstrumentCluster clusterInfo = 
    navManager.getInstrumentClusterInfo();

if (clusterInfo != null && clusterInfo.isSupported()) {
    // 构建导航事件
    NavigationState navState = new NavigationState.Builder()
        .setDestinationName("北京南站")
        .setRemainingDistance(5000)  // 剩余距离,单位米
        .setRemainingTime(300)       // 剩余时间,单位秒
        .setTurnEvent(new TurnEvent.Builder()
            .setTurnAngle(90)        // 转弯角度
            .setTurnSide(TurnEvent.TURN_SIDE_RIGHT)
            .setRoadName("建国路")
            .build())
        .build();
    
    // 发送到仪表盘
    navManager.sendNavigationState(navState);
}

路况信息怎么处理?我建议用 Google Maps 的路况数据,实时更新。但要注意,路况更新不要太频繁,否则会消耗大量流量和电量。我个人习惯每 30 秒更新一次。

导航指令类型 说明 更新频率建议
转弯提示 即将转弯的方向和距离 每次路线变化时
路况更新 前方道路拥堵情况 每 30 秒
到达信息 剩余距离和时间 每 5 秒
重新规划 路线变更通知 即时

避坑指南:我曾经在路况更新时直接在主线程解析 JSON,结果导致界面卡顿。路况数据解析一定要放在子线程,或者用 Kotlin 协程处理。另外,路况数据要缓存,网络不好时用缓存数据。

8.3 地图集成:Google Maps 的正确打开方式

Google Maps 在 Automotive OS 上的集成,和手机端不太一样。你不能直接用手机版的 Maps SDK,得用 Automotive 专用的版本。

为什么?因为车载屏幕的交互方式不同。手机你可以用手指缩放,但车上你得考虑驾驶安全,缩放操作要简化。

// 在 build.gradle 中添加依赖
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'com.google.android.libraries.maps:maps-android-utils:3.4.0'

// 布局文件中使用 MapFragment
<fragment
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

// 初始化地图
SupportMapFragment mapFragment = (SupportMapFragment) 
    getSupportFragmentManager().findFragmentById(R.id.map);
mapFragment.getMapAsync(new OnMapReadyCallback() {
    @Override
    public void onMapReady(GoogleMap googleMap) {
        // 设置地图样式,适合驾驶场景
        googleMap.setMapStyle(MapStyleOptions.loadRawResource(
            getContext(), R.raw.driving_style));
        
        // 禁用一些不适合驾驶时的操作
        googleMap.getUiSettings().setZoomGesturesEnabled(false);
        googleMap.getUiSettings().setScrollGesturesEnabled(false);
        
        // 开启交通图层
        googleMap.setTrafficEnabled(true);
    }
});

这里有个细节:我建议把地图的缩放和滚动手势关掉。为什么?因为开车时用户不应该去操作地图。导航路线是自动跟随的,用户只需要看就行。如果非要操作,可以用语音或者方向盘上的按键。

关键点:车载地图的核心是“看”,不是“玩”。所有交互设计都要围绕驾驶安全来设计。我见过一些 App 把手机上的地图交互直接搬过来,结果被用户投诉说太危险。

8.4 位置权限处理:别让用户在路上抓狂

位置权限,是导航 App 的命门。没有位置权限,导航就是个摆设。但 Android 的权限机制越来越严格,你得处理好各种情况。

我记得有一次,用户反馈说导航 App 启动后一直显示“正在定位”。查了日志才发现,是权限请求被拒绝了。而且用户点了“不再询问”,这就麻烦了。

// 检查位置权限
if (ContextCompat.checkSelfPermission(this, 
        Manifest.permission.ACCESS_FINE_LOCATION) 
        != PackageManager.PERMISSION_GRANTED) {
    
    // 判断是否需要显示解释说明
    if (ActivityCompat.shouldShowRequestPermissionRationale(
            this, Manifest.permission.ACCESS_FINE_LOCATION)) {
        // 用户之前拒绝过,显示解释说明
        showPermissionExplanationDialog();
    } else {
        // 第一次请求权限
        ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
            LOCATION_PERMISSION_REQUEST_CODE);
    }
} else {
    // 权限已授予,开始定位
    startLocationUpdates();
}

但这里有个坑:在 Android 12 以上,还有 ACCESS_BACKGROUND_LOCATION 权限。导航 App 需要在后台持续定位,所以这个权限也得申请。

我的做法:在 App 第一次启动时,先申请前台位置权限。等用户开始导航时,再申请后台位置权限。这样用户体验更好,不会一上来就被权限请求吓到。

权限被永久拒绝怎么办?我建议在界面上显示一个引导提示,告诉用户去设置里手动开启。但不要频繁弹窗,否则用户会烦。

// 引导用户去设置页面
private void showSettingsGuide() {
    new AlertDialog.Builder(this)
        .setTitle("需要位置权限")
        .setMessage("导航功能需要位置权限,请在设置中开启")
        .setPositiveButton("去设置", (dialog, which) -> {
            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            intent.setData(Uri.parse("package:" + getPackageName()));
            startActivity(intent);
        })
        .setNegativeButton("取消", null)
        .show();
}

避坑指南:我曾经在权限被拒绝后,直接退出了 App。结果用户投诉说“一打开就闪退”。正确的做法是:没有位置权限时,显示一个友好的提示页面,让用户知道为什么需要权限,而不是直接崩溃。

8.5 知识体系总览

说了这么多,我们来梳理一下导航应用开发的核心脉络。下面这张图可以帮你快速理解各个模块之间的关系:

导航应用开发核心架构 用户交互层(屏幕/仪表盘/HUD) CarNavigationManager(导航状态管理) 导航指令处理 转弯提示/路线规划 地图集成 Google Maps/路况 位置权限 前台/后台定位 Android Automotive OS 系统服务 车辆传感器 / 仪表盘通信 / 语音交互 导航 App 通过 CarNavigationManager 与系统交互,实现安全驾驶体验

这张图展示了导航应用的完整架构。从用户交互层到底层系统服务,每一层都有明确的职责。你在开发时,可以按照这个结构来组织代码。

好了,导航应用开发的核心内容就这些。记住一点:车载导航和手机导航最大的区别,就是安全第一。所有功能设计都要围绕“让用户更安全地驾驶”这个目标。代码写得好不好是其次,用户能不能安全到达目的地,才是最重要的。

公众号:蓝海资料掘金营,微信deep3321