系列番外·智慧出行篇。上篇聊完空间计算,评论区有车友问:“鸿蒙车机这么火,能不能把咱们的电商Demo适配到车机上?等红灯时逛个街?” 这需求太真实了!HarmonyOS智能座舱(HarmonySpace)本身就是分布式能力的集大成者。今天我们将电商Demo“上车”,打造一个专为驾驶场景优化的车载商城。重点解决三大车机开发痛点:驾驶安全约束(DO NOT DISTURB)语音交互替代触控屏幕自适应(横屏+折叠屏)。全程基于API23,含官方文档未提及的车机专属UI规范。


一、前言:为什么车机是电商的新蓝海?

在手机端,电商App已是一片红海。但在车机端,这是一个全新的增量市场:

  • 场景独特:用户在通勤、自驾游途中,可能产生特定的购物需求(如订购咖啡、预订酒店、购买周边文创)。

  • 系统级整合:鸿蒙车机不是简单的Android车机,它能与手机、手表无缝流转。手机上选好的商品,上车自动续接;车机上下的单,手机手表同步通知。

  • 交互变革:驾驶时不能低头玩手机,因此语音交互简化UI成为核心。这恰好是鸿蒙的强项。

本文将基于之前的电商Demo,改造出一个符合车规级安全标准的车机应用。我们将重点放在“听单”和“语音购”上,而非传统的滑动浏览。


二、核心概念辨析(手机App vs 车机App)

很多开发者直接把手机App搬到车机上,结果审核不通过。必须先理解两者的本质差异:

维度

手机电商App

车机电商App(合规版)

核心交互

手指触控(精细操作)

语音交互(主要)+ 旋钮/触摸(辅助)

UI设计

纵向滚动,信息密集

横向布局,卡片化,字体巨大,信息极简

安全约束

无特殊限制

驾驶模式:时速>5km/h时,禁止视频播放、复杂表单输入

硬件能力

触屏、摄像头

方向盘按键、旋钮、车速信号、GPS

适用场景

深度购物、比价

即时性消费:订餐、买景区门票、充值时

💡 核心认知:车机App的首要原则是驾驶安全。任何可能导致驾驶员分心的设计都是违规的。我们的目标不是做一个“能用的车机App”,而是做一个“安全的车机App”。


三、代码实现:从手机到车机的“适应性进化”

3.1 环境准备与配置

  1. 新建模块:在DevEco Studio中,右键项目 -> New -> Module -> Car Module。

  2. 权限配置:车机对权限管控极严。修改module.json5

    
      
    
      
    {
      "module": {
        "deviceTypes": ["car"], // 声明为车机应用
        "requestPermissions": [
          { "name": "ohos.permission.DRIVE_MODE", "reason": "检测驾驶状态以调整UI" },
          { "name": "ohos.permission.MICROPHONE", "reason": "语音搜索商品" },
          { "name": "ohos.permission.LOCATION", "reason": "推荐沿途服务区商品" }
        ]
      }
    }
  3. 屏幕适配:车机多为横屏(1280x720或1920x1080)。在resources目录下新建layout-car目录存放车机专用布局。

3.2 构建车机专属UI(横向卡片流)

创建entry_car/src/main/ets/pages/Index.ets。抛弃手机端的List纵向滚动,改用Swiper横向切换。



import { router } from '@kit.ArkUI'
import { abilityAccessCtrl } from '@kit.AbilityKit'

@Entry
@Component
struct CarIndex {
  private swiperController: SwiperController = new SwiperController()
  @State currentIndex: number = 0
  @State isDriving: boolean = false // 是否处于驾驶模式
  private goodsList: GoodsBean[] = [] // 复用之前的商品数据模型

  aboutToAppear(): void {
    this.loadGoodsForDrive()
    this.monitorDrivingStatus()
  }

  /**
   * 加载适合驾驶场景的商品(如咖啡、零食、景区票)
   */
  loadGoodsForDrive(): void {
    // 模拟数据:只加载即时性消费品
    this.goodsList = [
      { id: 101, name: '沿途星巴克咖啡', price: 35, image: 'coffee.jpg' },
      { id: 102, name: '服务区特色盒饭', price: 45, image: 'rice.jpg' },
      { id: 103, name: '张家界景区门票', price: 228, image: 'ticket.jpg' }
    ]
  }

  /**
   * 监听驾驶状态(模拟车速信号)
   */
  monitorDrivingStatus(): void {
    // 实际开发中,需通过Car Kit获取真实车速
    // 此处模拟:5秒后进入驾驶模式
    setTimeout(() => {
      this.isDriving = true
      promptAction.showToast({ message: '已进入驾驶模式,部分功能受限' })
    }, 5000)
  }

  build() {
    Stack() {
      // 背景图:模拟驾驶场景
      Image($r('app.media.car_bg'))
        .width('100%')
        .height('100%')
        .blur(this.isDriving ? 0 : 10) // 非驾驶模式模糊背景,减少干扰

      Column() {
        // 顶部状态栏
        Row() {
          Text('智慧商城')
            .fontSize(28)
            .fontColor(Color.White)
            .fontWeight(FontWeight.Bold)
          Blank()
          // 驾驶模式指示器
          if (this.isDriving) {
            Text('🚗 行驶中')
              .fontSize(18)
              .fontColor('#FFD700')
              .backgroundColor('#333')
              .padding(8)
              .borderRadius(16)
          }
        }
        .width('90%')
        .margin({ top: 30 })

        Blank()

        // 核心:横向商品卡片流
        Swiper({
          controller: this.swiperController,
          indicator: false, // 隐藏指示器,减少视觉干扰
          loop: false
        }) {
          ForEach(this.goodsList, (item: GoodsBean) => {
            this.GoodsCard(item)
          }, (item: GoodsBean) => item.id.toString())
        }
        .width('100%')
        .height(400)
        .onChange((index: number) => {
          this.currentIndex = index
        })

        Blank()

        // 底部操作提示(语音优先)
        Row() {
          Text('🎤 说“小艺小艺,购买这个”')
            .fontSize(22)
            .fontColor(Color.White)
          Blank()
          // 辅助触控:仅在停车时可用
          Button('确认购买')
            .enabled(!this.isDriving) // 驾驶时禁用按钮
            .backgroundColor(this.isDriving ? '#666' : '#0A59F7')
            .onClick(() => this.buyCurrentItem())
        }
        .width('90%')
        .margin({ bottom: 30 })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 商品卡片组件(车机专用:大图、大字、少文字)
   */
  @Builder
  GoodsCard(item: GoodsBean) {
    Column() {
      Image(item.image)
        .width(500)
        .height(280)
        .borderRadius(24)
        .objectFit(ImageFit.Cover)
      Text(item.name)
        .fontSize(32)
        .fontColor(Color.White)
        .margin({ top: 15 })
        .maxLines(1)
      Text(`¥${item.price}`)
        .fontSize(36)
        .fontColor('#FFD700')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 10 })
    }
    .padding(20)
    .backgroundColor('#1A1A1A')
    .borderRadius(32)
    .shadow({ radius: 20, color: '#000000' })
  }

  /**
   * 购买当前商品(语音调用此方法)
   */
  buyCurrentItem(): void {
    const item = this.goodsList[this.currentIndex]
    // 模拟下单
    promptAction.showToast({ message: `已下单:${item.name},请停车后支付` })
    // 实际开发中:调用云函数创建订单,并同步到手机端
  }
}

3.3 集成语音交互(核心差异点)

车机不能让司机低头打字,必须集成语音。创建entry_car/src/main/ets/common/VoiceControl.ets



import { voiceInteraction } from '@kit.VoiceInteractionKit'
import { BusinessError } from '@kit.BasicServicesKit'

/**
 * 车机语音控制类
 */
export class VoiceControl {
  private context: Context = null!
  private voiceEngine: voiceInteraction.VoiceEngine | null = null

  constructor(context: Context) {
    this.context = context
  }

  /**
   * 初始化语音引擎
   */
  async init(): Promise<void> {
    try {
      // 创建语音引擎,指定车机场景
      this.voiceEngine = await voiceInteraction.createVoiceEngine(this.context, {
        scenario: voiceInteraction.Scenario.IN_CAR, // 关键:车机场景优化
        wakeupWords: ['小艺小艺'], // 唤醒词
        asrConfig: {
          vadEnable: true, // 自动端点检测(自动判断说话结束)
          maxSpeechTime: 10 // 最长录音10秒
        }
      })

      // 注册语义理解回调
      this.voiceEngine.on('intent', (intent: voiceInteraction.Intent) => {
        this.handleIntent(intent)
      })

      console.log('车机语音引擎初始化成功')
    } catch (err) {
      const e = err as BusinessError
      console.error('语音引擎初始化失败:', e.message)
    }
  }

  /**
   * 处理语音意图
   */
  private handleIntent(intent: voiceInteraction.Intent): void {
    console.log('识别到意图:', intent.action)
    switch (intent.action) {
      case 'buy_current_item':
        // 用户说“购买这个”
        // 调用UI层的购买逻辑
        break
      case 'next_item':
        // 用户说“下一个”
        // 调用Swiper的next()
        break
      case 'previous_item':
        // 用户说“上一个”
        break
      default:
        console.log('未识别的指令')
    }
  }

  /**
   * 开始录音识别
   */
  startListening(): void {
    if (this.voiceEngine) {
      this.voiceEngine.startListening()
    }
  }

  /**
   * 释放资源
   */
  release(): void {
    if (this.voiceEngine) {
      this.voiceEngine.release()
    }
  }
}

注意:在CarIndex.etsaboutToAppear中初始化VoiceControl,并在onDestroy中释放。


四、踩坑记录(官方文档没写的6个车机坑)

  1. 驾驶模式检测延迟Car Kit获取车速有1-2秒的延迟。不要直接用实时车速判断是否禁用UI,而要监听DRIVING_STATUS_CHANGED广播,否则会出现界面闪烁。

  2. 语音ASR(识别)不准:车机环境嘈杂(风噪、路噪)。必须在asrConfig中开启降噪算法noiseSuppression: true),否则识别率极低。

  3. UI字体大小:车机UI的最小字体不得小于24fp。官方规范是“扫视可见”,驾驶员只有几百毫秒的视觉停留时间。

  4. 横屏适配陷阱:手机布局是Column纵向,matchParent在横屏下会变宽。车机布局应多用Row和固定尺寸,防止拉伸变形。

  5. 方向盘按键映射:车机应用必须支持方向盘上的“上一首/下一首”按键。这需要监听KeyEvent,并将其映射为Swiper的滑动事件。

  6. 审核红线:时速超过5km/h时,绝对不能弹出输入法(Keyboard)。如果必须输入,必须引导用户说“请停车后操作”或使用语音输入。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐