在HarmonyOS 6上开发自定义扫码界面(使用customScan+XComponent)时,你是否遇到过这种“玄学”Bug:扫码界面加载后,预览区域一片漆黑,但相机权限已授权、代码逻辑看似正常,甚至扫码识别逻辑还能偶尔触发?你反复检查了ohos.permission.CAMERA权限,确认XComponent组件已渲染,但预览画面始终“死黑”。

这并非相机硬件故障,而是HarmonyOS 6扫码服务(Scan Kit)对“初始化时序”与“XComponent宽高”的严格校验。本文将彻底解析这一“黑屏”现象,并提供一套基于init时序控制与ViewControl参数校准的完整修复方案。

一、现象:权限已给,预览却“黑屏”

1. 问题现场:看得见的组件,看不见的画面

场景复现:用户进入自定义扫码页,系统弹窗授权相机权限(用户点击“允许”),XComponent组件正常渲染(有背景色或占位图),但相机预览画面始终为黑色。控制台可能无任何报错,或仅出现AdjustRenderFit failed等模糊日志。

预期效果

实际效果

技术假象

授权相机 → 显示预览画面

❌ 授权相机 → 预览区域黑屏

相机流未绑定或初始化失败

错误代码示例(导致“黑屏”的元凶)

// ❌ 错误示例:异步时序混乱(init未完成就start)
import customScan from '@ohos.customScan';

@Entry
@Component
struct CustomScanPage {
  @State message: string = '准备扫码...';

  onPageShow() {
    // 1. 初始化(异步)
    customScan.init().then(() => {
      console.log('✅ init完成');
    });

    // 2. 立即启动扫码(错误!init可能未完成)
    this.startScan(); // 导致黑屏:ScanOption is null
  }

  startScan() {
    let scanOption = { ... }; // 扫码配置
    customScan.start(scanOption, (err, result) => {
      if (err) {
        console.error('❌ 扫码失败:', err.code, err.message);
        return;
      }
      this.message = result.scanResult;
    });
  }

  build() {
    Column() {
      XComponent({ ... }) // 扫码预览区域
      Text(this.message)
    }
  }
}

2. 根因揭秘:时序与参数的“双重陷阱”

核心机制:HarmonyOS 6 的customScan服务要求严格的初始化时序精确的XComponent参数。任何一步错误,都会导致相机流无法绑定到Surface,进而显示黑屏。

根因类型

典型日志

导致结果

时序问题

ScanOption is null, please call customScan.init first

init未完成就调用start,扫码服务未就绪

权限问题

Permission denied.

调用init时未获取相机权限

参数问题

ViewControl's width/height range error

XComponent宽高为0或比例异常

资源未释放

Camera restart camera session failed

返回页面时未释放相机,重启失败

失败本质:在HarmonyOS 6上,相机预览的建立是“脆弱”的init的异步性、XComponent的渲染时机、ViewControl的宽高比例,任一环节出错,都会导致黑屏。

二、解决方案:时序控制 + 参数校准

1. 修复原理:等待init完成,校准ViewControl

核心思路:确保customScan.init()完全完成后(包括权限获取)再调用start,并确保XComponent已渲染且宽高合理,再将正确的surfaceId传入ViewControl

修复代码

import customScan from '@ohos.customScan';
import window from '@ohos.window';

@Entry
@Component
struct CustomScanPage {
  @State message: string = '准备扫码...';
  @State isInitComplete: boolean = false; // 初始化状态
  private surfaceId: string = ''; // XComponent的surfaceId

  async onPageShow() {
    await this.initScanService(); // 等待初始化完成
    if (this.isInitComplete && this.surfaceId) {
      this.startScan();
    }
  }

  // 初始化扫码服务(含权限申请)
  async initScanService(): Promise<void> {
    try {
      // 1. 申请相机权限(必须在init前)
      let context = getContext(this) as common.UIAbilityContext;
      let permissions = ['ohos.permission.CAMERA'];
      let atManager = abilityAccessCtrl.createAtManager();
      let grantStatus = await atManager.requestPermissionsFromUser(context, permissions);
      if (grantStatus.authResults[permissions[0]] !== 0) {
        this.message = '相机权限被拒绝';
        return;
      }

      // 2. 初始化扫码服务
      await customScan.init();
      this.isInitComplete = true;
      console.log('✅ 扫码服务初始化完成');
    } catch (err) {
      console.error('❌ 初始化失败:', err.message);
    }
  }

  // 启动扫码(必须在init完成后)
  startScan() {
    if (!this.isInitComplete) {
      console.error('❌ 未初始化完成,禁止start');
      return;
    }

    let scanOption: customScan.ScanOption = {
      scanMode: customScan.ScanMode.CAMERA_SCAN,
      scanTypes: [customScan.ScanType.QR_CODE],
      viewControl: {
        surfaceId: this.surfaceId, // 必须使用XComponent的surfaceId
        position: { x: 0, y: 0 },
        size: { width: 720, height: 1280 } // 建议使用合理宽高(非0)
      }
    };

    customScan.start(scanOption, (err, result) => {
      if (err) {
        console.error('❌ 扫码失败:', err.code, err.message);
        return;
      }
      this.message = result.scanResult;
    });
  }

  // XComponent加载完成回调
  onXComponentLoad(ctx: XComponentContext) {
    this.surfaceId = ctx.getXComponentSurfaceId();
    console.log('✅ surfaceId:', this.surfaceId);
  }

  onPageHide() {
    // 页面隐藏时释放资源
    customScan.stop();
    customScan.release();
    this.isInitComplete = false;
  }

  build() {
    Column() {
      // XComponent必须设置合理宽高(建议16:9或4:3)
      XComponent({
        id: 'scan_preview',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .width('100%')
        .aspectRatio(16/9) // 关键:固定宽高比,避免拉伸或黑屏
        .onLoad((ctx: XComponentContext) => this.onXComponentLoad(ctx))

      Text(this.message)
        .margin(10)
    }
    .width('100%')
    .height('100%')
  }
}

2. 效果对比:从“黑屏”到“正常预览”

修复前(错误场景)

修复后(正确姿势)

关键改进

init未完成就start

✅ 等待init Promise完成

解决ScanOption is null

XComponent宽高为0

✅ 设置固定宽高比(16:9)

解决ViewControl's width/height range error

未获取surfaceId

✅ onLoad回调获取surfaceId

解决surfaceid range error

三、进阶:黑屏问题的“防呆”策略

1. 时序控制的“三必须”

规则

原因

违反后果

必须先申请权限

init内部会校验权限

Permission denied

必须等待init完成

init是异步操作

ScanOption is null

必须获取surfaceId

相机流需要绑定Surface

黑屏无画面

2. XComponent的“黄金比例”

常见问题XComponent宽高为0或比例极端(如1000:1),导致相机预览无法渲染。

// 推荐设置(避免黑屏)
XComponent({ ... })
  .width('100%')
  .aspectRatio(16/9) // 或 4:3,符合相机预览比例

// 错误设置(导致黑屏)
XComponent({ ... })
  .width(0) // 宽度为0
  .height('100%')

3. 资源释放的“生命周期”

返回页面黑屏:离开页面时未释放相机,重新进入时相机重启失败。

onPageHide() {
  customScan.stop();  // 停止扫码
  customScan.release(); // 释放相机资源
}

四、总结:自定义扫码的“避黑”法则

  1. 时序是生命initonXComponentLoadstart缺一不可,顺序不可乱

  2. 参数是基础XComponent必须有合理的宽高比(16:9/4:3),且surfaceId必须正确传入ViewControl

  3. 权限是前提:调用init前必须确保ohos.permission.CAMERA已授权。

通过这套“时序控制 + 参数校准”的组合拳,你的自定义扫码界面将彻底告别“黑屏”,实现真正的所见即扫

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐