HarmonyOS开发:手机-车机联动——分布式能力让两块屏幕变成一个系统
HarmonyOS开发:手机-车机联动——分布式能力让两块屏幕变成一个系统
📌 核心要点:车机互联的核心不是"投屏",而是分布式能力让应用在手机和车机之间无缝流转,通知同步、跨设备输入、数据共享三驾马车缺一不可。
背景与动机
你有没有用过那种"手机投屏到车机"的功能?手机上打开导航,画面"投"到车机屏幕上。听起来不错,但实际体验呢?
你在手机上操作,车机屏幕只是个显示器。你想在车机上缩放地图?不行,得在手机上操作。手机来电话了,导航画面被电话界面覆盖,车机屏幕也跟着黑了。手机锁屏了,车机也黑了。
这不叫联动,这叫"手机当遥控器,车机当显示器"。
真正的车机互联是什么?是你在手机上设好导航目的地,上了车,导航自动"流转"到车机上继续运行——手机可以锁屏放口袋里,车机上的导航照样跑。是手机收到微信消息,车机上弹出通知,你用旋钮就能看到摘要。是你在车机上输入地址不方便,掏出手机打字,文字直接出现在车机的搜索框里。
HarmonyOS的分布式能力就是干这个的。它不是投屏,而是让手机和车机变成一个"超级终端"——应用可以跨设备运行,数据可以跨设备同步,输入可以跨设备传递。
核心原理
分布式能力架构
HarmonyOS的分布式能力基于"超级终端"概念,底层是分布式软总线,上层是分布式任务管理、分布式数据同步、分布式输入。
graph TD
A[超级终端] --> B[分布式软总线]
B --> C[设备发现与认证]
B --> D[安全通信通道]
A --> E[分布式任务管理]
E --> E1[应用流转]
E --> E2[任务迁移]
E --> E3[任务回迁]
A --> F[分布式数据同步]
F --> F1[分布式数据对象]
F --> F2[分布式文件]
F --> F3[偏好设置同步]
A --> G[分布式输入]
G --> G1[跨设备键盘]
G --> G2[跨设备触控]
G --> G3[跨设备语音]
classDef root fill:#E91E63,stroke:#880E4F,color:#fff
classDef bus fill:#2196F3,stroke:#1565C0,color:#fff
classDef task fill:#4CAF50,stroke:#2E7D32,color:#fff
classDef data fill:#FF9800,stroke:#E65100,color:#fff
classDef input fill:#9C27B0,stroke:#6A1B9A,color:#fff
class A root
class B,C,D bus
class E,E1,E2,E3 task
class F,F1,F2,F3 data
class G,G1,G2,G3 input
应用流转流程
应用流转是车机互联最核心的能力。它的工作流程是这样的:
- 手机上正在运行应用(比如导航)
- 用户上车,车机与手机自动连接
- 用户点击"流转到车机"(或系统自动触发)
- 应用的UI和状态迁移到车机上继续运行
- 手机端应用进入"挂起"状态,释放资源
- 用户下车时,应用可以"回迁"到手机继续运行
关键点:流转的不是"画面",而是"应用实例"。车机上运行的是应用的完整实例,有自己的渲染、自己的状态、自己的生命周期。手机端只是暂停了,不是在后台偷偷运行。
通知与消息同步
手机收到通知,车机上也要能看到。但不是所有通知都适合在车机上展示——微信聊天记录可以在车机上显示摘要,但银行验证码就不该出现在车机屏幕上(别人坐副驾可能看到)。
通知同步规则:
- 社交消息(微信、钉钉):同步,显示摘要,语音可播报
- 系统通知(更新、提醒):同步,静默展示
- 敏感通知(银行、支付):不同步,或只显示"收到一条消息"
- 来电:同步,提供接听/拒接选项
代码实战
基础用法:应用流转
先实现最核心的功能——应用从手机流转到车机。
// AppMigration.ets - 应用流转管理
import { distributedMissionManager } from '@kit.DistributedMission';
import { deviceManager } from '@kit.DistributedHardware';
import { AbilityConstant } from '@kit.AbilityKit';
// 流转状态
export enum MigrationState {
IDLE = 'idle', // 空闲
DISCOVERING = 'discovering', // 发现设备中
MIGRATING = 'migrating', // 流转中
REMOTE_RUNNING = 'remote', // 远端运行中
RECOVERING = 'recovering', // 回迁中
}
export class AppMigration {
private migrationState: MigrationState = MigrationState.IDLE;
private deviceManagerInstance: deviceManager.DeviceManager | null = null;
private remoteDeviceId: string = '';
private onStateChange?: (state: MigrationState) => void;
// 初始化设备管理器
async init(): Promise<void> {
try {
// 创建设备管理器
this.deviceManagerInstance = deviceManager.createDeviceManager('com.example.carnavi');
// 监听设备状态变化
this.deviceManagerInstance.on('deviceStateChange', (data: deviceManager.DeviceStateChangeResponse) => {
if (data.action === deviceManager.DeviceStateChangeAction.AVAILABLE) {
console.info(`[Migration] 发现设备: ${data.device.name}`);
// 如果是车机设备,自动记录
if (data.device.deviceType === deviceManager.DeviceType.CAR) {
this.remoteDeviceId = data.device.deviceId;
}
} else if (data.action === deviceManager.DeviceStateChangeAction.UNAVAILABLE) {
console.warn(`[Migration] 设备离线: ${data.device.name}`);
if (data.device.deviceId === this.remoteDeviceId) {
this.remoteDeviceId = '';
}
}
});
console.info('[Migration] 设备管理器初始化完成');
} catch (err) {
console.error(`[Migration] 初始化失败: ${(err as Error).message}`);
}
}
// 发现附近的车机设备
async discoverCarDevices(): Promise<deviceManager.DeviceInfo[]> {
if (!this.deviceManagerInstance) {
return [];
}
this.updateState(MigrationState.DISCOVERING);
try {
// 获取可信设备列表
const devices = this.deviceManagerInstance.getTrustedDeviceListSync();
const carDevices = devices.filter(d => d.deviceType === deviceManager.DeviceType.CAR);
console.info(`[Migration] 发现 ${carDevices.length} 台车机`);
this.updateState(MigrationState.IDLE);
return carDevices;
} catch (err) {
console.error(`[Migration] 设备发现失败: ${(err as Error).message}`);
this.updateState(MigrationState.IDLE);
return [];
}
}
// 流转到车机
async migrateToCar(deviceId: string, params: Record<string, string> = {}): Promise<boolean> {
if (!this.deviceManagerInstance) {
return false;
}
this.updateState(MigrationState.MIGRATING);
try {
// 发起流转
await distributedMissionManager.continueMission({
srcDeviceId: '', // 空字符串表示本机
dstDeviceId: deviceId,
missionId: '', // 空字符串表示当前任务
params: params,
});
this.remoteDeviceId = deviceId;
this.updateState(MigrationState.REMOTE_RUNNING);
console.info('[Migration] 应用已流转到车机');
return true;
} catch (err) {
console.error(`[Migration] 流转失败: ${(err as Error).message}`);
this.updateState(MigrationState.IDLE);
return false;
}
}
// 从车机回迁到手机
async recoverFromCar(): Promise<boolean> {
if (!this.remoteDeviceId) {
console.warn('[Migration] 没有远端运行的应用');
return false;
}
this.updateState(MigrationState.RECOVERING);
try {
await distributedMissionManager.continueMission({
srcDeviceId: this.remoteDeviceId,
dstDeviceId: '', // 回迁到本机
missionId: '',
params: {},
});
this.remoteDeviceId = '';
this.updateState(MigrationState.IDLE);
console.info('[Migration] 应用已回迁到手机');
return true;
} catch (err) {
console.error(`[Migration] 回迁失败: ${(err as Error).message}`);
this.updateState(MigrationState.IDLE);
return false;
}
}
// 设置状态回调
setOnStateChange(callback: (state: MigrationState) => void): void {
this.onStateChange = callback;
}
// 更新状态
private updateState(state: MigrationState): void {
this.migrationState = state;
this.onStateChange?.(state);
}
// 获取当前状态
getState(): MigrationState {
return this.migrationState;
}
}
进阶用法:通知同步与跨设备输入
应用流转解决了"应用在哪跑"的问题,但日常使用中更频繁的场景是通知同步和跨设备输入。
// CarPhoneSync.ets - 通知同步与跨设备输入
import { notificationManager } from '@kit.NotificationKit';
import { distributedDataObject } from '@kit.ArkData';
import { inputMethodClient } from '@kit.IMEKit';
// 通知同步管理
export class NotificationSync {
private syncedNotifications: notificationManager.NotificationRequest[] = [];
// 监听手机端通知
startListening(): void {
// 订阅所有通知
notificationManager.on('notificationEvent', (event: notificationManager.NotificationEvent) => {
this.handleNotification(event);
});
console.info('[Sync] 通知监听已启动');
}
// 处理通知
private handleNotification(event: notificationManager.NotificationEvent): void {
const notification = event.request;
const bundleName = notification.notificationId?.toString() || '';
// 敏感通知过滤
if (this.isSensitiveNotification(bundleName)) {
console.info(`[Sync] 过滤敏感通知: ${bundleName}`);
return;
}
// 驾驶模式过滤:行车中只保留重要通知
const isDriving = AppStorage.get<boolean>('isDrivingMode') || false;
if (isDriving && !this.isImportantNotification(event)) {
return;
}
// 在车机上展示通知
this.showCarNotification(notification);
this.syncedNotifications.push(notification);
}
// 判断是否为敏感通知
private isSensitiveNotification(bundleName: string): boolean {
const sensitiveApps = ['com.bank', 'com.alipay', 'com.wechat.pay'];
return sensitiveApps.some(app => bundleName.includes(app));
}
// 判断是否为重要通知
private isImportantNotification(event: notificationManager.NotificationEvent): boolean {
// 来电、导航、紧急消息为重要通知
const contentType = event.request.content?.notificationContentType;
return contentType === notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT;
}
// 在车机上展示通知
private showCarNotification(notification: notificationManager.NotificationRequest): void {
const title = notification.content?.title || '新消息';
const text = notification.content?.text || '';
// 使用车机通知样式(大字体、简洁)
const carNotification: notificationManager.NotificationRequest = {
id: notification.id,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: title,
text: text.length > 30 ? text.substring(0, 30) + '...' : text,
},
},
deliveryTime: Date.now(),
};
notificationManager.publish(carNotification).catch((err: Error) => {
console.error(`[Sync] 车机通知发布失败: ${err.message}`);
});
}
// 停止监听
stopListening(): void {
notificationManager.off('notificationEvent');
this.syncedNotifications = [];
}
}
// 跨设备输入管理
export class CrossDeviceInput {
private distributedObject: distributedDataObject.DataObject | null = null;
// 初始化分布式数据对象(用于跨设备输入同步)
async init(): Promise<void> {
// 创建分布式数据对象,用于同步输入内容
this.distributedObject = distributedDataObject.create({
sessionId: 'cross_input_' + Date.now(),
inputText: '',
inputTarget: '', // 目标输入框ID
timestamp: 0,
});
// 监听远端输入变化
this.distributedObject.on('change', (sessionId: string, fields: string[]) => {
if (fields.includes('inputText')) {
const text = this.distributedObject!['inputText'] as string;
const target = this.distributedObject!['inputTarget'] as string;
this.handleRemoteInput(text, target);
}
});
console.info('[Input] 跨设备输入初始化完成');
}
// 手机端发送输入到车机
sendInputToCar(text: string, targetField: string): void {
if (!this.distributedObject) return;
this.distributedObject['inputText'] = text;
this.distributedObject['inputTarget'] = targetField;
this.distributedObject['timestamp'] = Date.now();
// 保存变更,触发同步
this.distributedObject.save('default');
console.info(`[Input] 已发送输入到车机: ${text.substring(0, 20)}...`);
}
// 处理远端输入
private handleRemoteInput(text: string, target: string): void {
console.info(`[Input] 收到远端输入: target=${target}, text=${text}`);
// 通知UI层更新对应的输入框
AppStorage.setOrCreate('remoteInputText', text);
AppStorage.setOrCreate('remoteInputTarget', target);
}
// 销毁
destroy(): void {
if (this.distributedObject) {
this.distributedObject.off('change');
this.distributedObject = null;
}
}
}
完整示例:车机互联主页面
把应用流转、通知同步、跨设备输入整合到一个页面里。
// CarPhoneLinkPage.ets - 车机互联主页面
import { deviceManager } from '@kit.DistributedHardware';
@Entry
@Component
struct CarPhoneLinkPage {
@State connectedPhone: string = '未连接';
@State migrationState: string = '空闲';
@State notificationCount: number = 0;
@State recentNotifications: string[] = [];
@State remoteInputText: string = '';
@State searchQuery: string = '';
build() {
Column({ space: 16 }) {
// 标题与连接状态
Row() {
Text('车机互联')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Row({ space: 8 }) {
Circle({ width: 10, height: 10 })
.fill(this.connectedPhone !== '未连接' ? '#4CAF50' : '#F44336')
Text(this.connectedPhone)
.fontSize(14)
.fontColor(this.connectedPhone !== '未连接' ? '#4CAF50' : '#888888')
}
.margin({ left: 16 })
}
.width('100%')
.padding({ left: 30, top: 20 })
Scroll() {
Column({ space: 16 }) {
// 应用流转卡片
this.MigrationCard()
// 通知同步卡片
this.NotificationCard()
// 跨设备输入卡片
this.CrossInputCard()
}
.padding({ left: 30, right: 30, bottom: 30 })
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#0D1117')
}
// 应用流转卡片
@Builder
MigrationCard() {
Column({ space: 16 }) {
Text('应用流转')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor(Color.White)
Text(`当前状态: ${this.migrationState}`)
.fontSize(14)
.fontColor('#AAAAAA')
Row({ space: 12 }) {
Button('流转到车机')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#4CAF50')
.borderRadius(12)
.width(140)
.height(48)
.enabled(this.migrationState === '空闲')
.onClick(() => {
this.migrationState = '流转中';
setTimeout(() => { this.migrationState = '远端运行'; }, 2000);
})
Button('回迁到手机')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#FF9800')
.borderRadius(12)
.width(140)
.height(48)
.enabled(this.migrationState === '远端运行')
.onClick(() => {
this.migrationState = '回迁中';
setTimeout(() => { this.migrationState = '空闲'; }, 2000);
})
}
}
.width('100%')
.padding(20)
.backgroundColor('#161B22')
.borderRadius(16)
.alignItems(HorizontalAlign.Start)
}
// 通知同步卡片
@Builder
NotificationCard() {
Column({ space: 12 }) {
Row() {
Text('通知同步')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor(Color.White)
Text(`${this.notificationCount}条`)
.fontSize(14)
.fontColor('#4FC3F7')
.margin({ left: 12 })
}
if (this.recentNotifications.length === 0) {
Text('暂无新通知')
.fontSize(14)
.fontColor('#888888')
} else {
Column({ space: 8 }) {
ForEach(this.recentNotifications.slice(0, 5), (msg: string) => {
Row() {
Text('📱')
.fontSize(16)
Text(msg)
.fontSize(14)
.fontColor('#CCCCCC')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor('#1A1A2E')
.borderRadius(8)
})
}
}
}
.width('100%')
.padding(20)
.backgroundColor('#161B22')
.borderRadius(16)
.alignItems(HorizontalAlign.Start)
}
// 跨设备输入卡片
@Builder
CrossInputCard() {
Column({ space: 12 }) {
Text('跨设备输入')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor(Color.White)
Text('在手机上输入,文字自动同步到车机搜索框')
.fontSize(13)
.fontColor('#888888')
// 搜索框(可接收手机端输入)
Row({ space: 8 }) {
Text('🔍')
.fontSize(18)
TextInput({ text: this.searchQuery, placeholder: '搜索目的地...' })
.fontSize(16)
.fontColor(Color.White)
.backgroundColor('#1A1A2E')
.borderRadius(8)
.layoutWeight(1)
.height(48)
.onChange((value: string) => {
this.searchQuery = value;
})
}
.width('100%')
.padding({ left: 12, right: 12 })
.backgroundColor('#1A1A2E')
.borderRadius(12)
if (this.remoteInputText) {
Text(`手机输入: ${this.remoteInputText}`)
.fontSize(13)
.fontColor('#4FC3F7')
}
}
.width('100%')
.padding(20)
.backgroundColor('#161B22')
.borderRadius(16)
.alignItems(HorizontalAlign.Start)
}
}
踩坑与注意事项
坑1:流转后状态丢失
应用流转最怕的就是状态丢失——你在手机上设好了导航路线,流转到车机后路线没了,还得重新设。
原因:流转时只迁移了UI,没有迁移数据。HarmonyOS的流转机制支持数据迁移,但需要你主动实现onContinue和onRestoreData回调。
解决方案:
- 在
onContinue中把关键状态序列化到wantParam中 - 在车机端的
onCreate中从wantParam恢复状态 - 大数据(如轨迹点列表)使用分布式数据对象同步,不要塞在
wantParam里
坑2:设备发现延迟
分布式设备发现不是实时的。从手机靠近车机到系统发现设备,可能需要5-15秒。如果应用一启动就去查设备列表,大概率查不到。
解决方案:
- 提前注册
deviceStateChange监听,设备上线时自动通知 - 不要轮询设备列表,太耗电
- 提供手动刷新按钮,让用户主动触发发现
坑3:通知同步的权限问题
读取手机通知需要ohos.permission.NOTIFICATION_CONTROLLER权限,这个权限是系统权限,普通应用申请不到。
车机端能收到通知的前提是:手机端的应用主动把通知"推"到车机,而不是车机去"拉"手机的通知。这意味着你需要在手机端的应用里集成通知转发逻辑。
坑4:跨设备输入的延迟
跨设备输入通过分布式数据对象同步,延迟通常在100-500ms。对于打字来说这个延迟勉强可接受,但对于实时操作(比如游戏、绘画)就太慢了。
跨设备输入适用场景:
- ✅ 搜索框输入、地址输入、消息回复
- ❌ 实时操作、精细控制
坑5:两台设备时间不同步
手机和车机的系统时间可能有几秒甚至几分钟的差异。如果你的业务逻辑依赖时间戳(比如判断通知的先后顺序、计算行程时长),必须考虑时间差。
解决方案:
- 使用NTP协议同步两台设备的时间
- 或者在通信时带上"相对时间"而非"绝对时间"
HarmonyOS 6适配说明
HarmonyOS 6在车机互联方面做了几项重要更新:
-
自动流转触发:新增基于场景的自动流转。用户上车后(检测到蓝牙连接车机),系统自动提示"是否将导航流转到车机"。之前需要用户手动操作。
-
流转状态可视化:新增流转状态指示器,在状态栏显示当前应用的运行位置(手机/车机)。用户一眼就能知道应用在哪跑。
-
通知智能过滤:新增AI驱动的通知过滤——系统根据通知内容和驾驶状态自动决定是否在车机展示。紧急通知(如来电)必展示,广告通知必过滤。
-
跨设备剪贴板:新增跨设备剪贴板同步。手机上复制的地址,车机上直接粘贴。之前需要通过分布式数据对象手动同步。
适配代码:
// HarmonyOS 6 自动流转触发
import { distributedMissionManager } from '@kit.DistributedMission';
// 在UIAbility中实现自动流转
export default class EntryAbility extends UIAbility {
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
// 序列化应用状态
wantParam['navDestination'] = AppStorage.get<string>('navDestination') || '';
wantParam['navRouteData'] = AppStorage.get<string>('navRouteData') || '';
wantParam['musicTrackId'] = AppStorage.get<string>('musicTrackId') || '';
console.info('[Migration] onContinue: 状态已序列化');
return AbilityConstant.OnContinueResult.AGREE;
}
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 从流转参数中恢复状态
if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
const destination = want.parameters?.['navDestination'] as string || '';
const routeData = want.parameters?.['navRouteData'] as string || '';
const trackId = want.parameters?.['musicTrackId'] as string || '';
// 恢复应用状态
AppStorage.setOrCreate('navDestination', destination);
AppStorage.setOrCreate('navRouteData', routeData);
AppStorage.setOrCreate('musicTrackId', trackId);
console.info('[Migration] 状态已从手机恢复');
}
}
}
总结
车机互联的本质不是"投屏",而是"融合"。手机和车机不再是两个独立的设备,而是一个超级终端的两个界面。应用在哪跑、数据在哪存、输入从哪来——这些对用户来说应该是透明的。他只需要知道:上车了,东西自动到车机上;下车了,东西自动回到手机上。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 分布式能力概念多,调试复杂 |
| 使用频率 | ⭐⭐⭐⭐⭐ 车机互联是智慧出行的核心卖点 |
| 重要程度 | ⭐⭐⭐⭐⭐ 直接决定用户体验的连贯性 |
一句话:车机互联做得好不好,不是看投屏清不清楚,而是看用户能不能"无感"地在手机和车机之间切换——他甚至不应该意识到自己在切换设备。
- 点赞
- 收藏
- 关注作者
评论(0)