欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

本文基于flutter3.27.5开发

在这里插入图片描述

一、qr_code_scanner 库概述

二维码扫描是移动应用的常见功能,用于支付、登录、信息获取、商品溯源等场景。在 Flutter for OpenHarmony 应用开发中,qr_code_scanner 是一个功能完善的二维码扫描插件,提供了跨平台的实时扫描能力。

qr_code_scanner 库特点

qr_code_scanner 库基于 Flutter 平台视图实现,提供了以下核心特性:

实时扫描:通过相机预览实时检测二维码和条形码,支持多种码制格式。

原生嵌入:使用 Platform View 将原生相机视图嵌入 Flutter 界面,体验流畅自然。

相机控制:支持闪光灯开关、前后摄像头切换、相机暂停恢复等操作。

多格式支持:支持 QR Code、Code 128、Code 39、EAN-13、EAN-8、Aztec、Data Matrix 等多种条码格式。

扫描区域:支持自定义扫描区域,提高扫描效率和用户体验。

权限处理:自动处理相机权限请求,简化开发流程。

功能支持对比

功能 Android iOS OpenHarmony
实时扫描
QR Code
条形码
闪光灯控制
前后摄像头
扫描区域
相机暂停恢复
原始字节数据

使用场景:移动支付、扫码登录、商品溯源、票务验证、信息获取、社交分享等。

⚠️ 注意:qr_code_scanner 原库已进入维护模式,建议新项目考虑使用 mobile_scanner 库。但对于已有项目迁移,qr_code_scanner_ohos 仍然是可行的选择。


二、安装与配置

2.1 添加依赖

在项目的 pubspec.yaml 文件中添加 qr_code_scanner_ohos 依赖:

dependencies:


  qr_code_scanner_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_qr_code_scanner.git
      path: ohos

然后执行以下命令获取依赖:

flutter pub get

2.2 权限配置

在 OpenHarmony 项目中需要配置相机权限。打开 ohos/entry/src/main/module.json5 文件,在 requestPermissions 数组中添加:

"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:camera_reason",
    "usedScene": {
      "abilities": [
        "EntryAbility"
      ],
      "when": "inuse"
    }
  }
]

然后在 ohos/entry/src/main/resources/base/element/string.json 中添加权限说明:

{
  "string": [
    {
      "name": "camera_reason",
      "value": "需要相机权限来扫描二维码"
    }
  ]
}

三、核心 API 详解

3.1 QRView 组件

QRView 是二维码扫描的核心组件,用于显示相机预览并实时检测二维码。

class QRView extends StatefulWidget {
  const QRView({
    required Key key,              // 必需,用于获取控制器
    required this.onQRViewCreated, // 创建完成回调
    this.overlay,                  // 扫描框叠加层
    this.overlayMargin = EdgeInsets.zero,  // 叠加层边距
    this.cameraFacing = CameraFacing.back, // 相机方向
    this.onPermissionSet,          // 权限设置回调
    this.formatsAllowed = const <BarcodeFormat>[], // 允许的码制
  });
}

参数说明

参数 类型 说明
key GlobalKey 必需,用于获取 QRViewController
onQRViewCreated QRViewCreatedCallback QRView 创建完成回调
overlay QrScannerOverlayShape? 扫描框叠加层组件
overlayMargin EdgeInsetsGeometry 叠加层边距
cameraFacing CameraFacing 相机方向(前/后)
onPermissionSet PermissionSetCallback? 权限设置回调
formatsAllowed List 允许扫描的码制格式

使用示例

final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');

QRView(
  key: qrKey,
  onQRViewCreated: (controller) {
    controller.scannedDataStream.listen((scanData) {
      print('扫描结果: ${scanData.code}');
    });
  },
  overlay: QrScannerOverlayShape(
    borderColor: Colors.red,
    borderRadius: 10,
    borderLength: 30,
    borderWidth: 10,
    cutOutSize: 250,
  ),
)

3.2 QRViewController 控制器

QRViewController 提供相机控制和扫描结果获取功能。

class QRViewController {
  Stream<Barcode> get scannedDataStream;  // 扫描结果流
  bool get hasPermissions;                // 是否有权限
  
  Future<CameraFacing> getCameraInfo();   // 获取摄像头信息
  Future<CameraFacing> flipCamera();      // 切换摄像头
  Future<bool?> getFlashStatus();         // 获取闪光灯状态
  Future<void> toggleFlash();             // 切换闪光灯
  Future<void> pauseCamera();             // 暂停相机
  Future<void> resumeCamera();            // 恢复相机
  Future<void> stopCamera();              // 停止相机
  Future<SystemFeatures> getSystemFeatures(); // 获取设备功能
  void dispose();                         // 释放资源
}

3.3 scannedDataStream 属性

scannedDataStream 是扫描结果的数据流,每次扫描到条码都会触发。

Stream<Barcode> get scannedDataStream

使用示例

controller.scannedDataStream.listen((barcode) {
  print('内容: ${barcode.code}');
  print('格式: ${barcode.format}');
});

3.4 getCameraInfo 方法

getCameraInfo 方法用于获取当前摄像头信息。

Future<CameraFacing> getCameraInfo()

返回值:返回当前摄像头方向(front 或 back)。

使用示例

final cameraInfo = await controller.getCameraInfo();
if (cameraInfo == CameraFacing.front) {
  print('当前是前置摄像头');
} else {
  print('当前是后置摄像头');
}

3.5 flipCamera 方法

flipCamera 方法用于切换前后摄像头。

Future<CameraFacing> flipCamera()

返回值:返回切换后的摄像头方向。

使用示例

await controller.flipCamera();

3.6 getFlashStatus 方法

getFlashStatus 方法用于获取闪光灯状态。

Future<bool?> getFlashStatus()

返回值:返回闪光灯是否开启,不支持时返回 null。

使用示例

final isFlashOn = await controller.getFlashStatus();
print('闪光灯状态: $isFlashOn');

3.7 toggleFlash 方法

toggleFlash 方法用于切换闪光灯开关状态。

Future<void> toggleFlash()

使用示例

await controller.toggleFlash();

3.8 pauseCamera 方法

pauseCamera 方法用于暂停相机预览和扫描。

Future<void> pauseCamera()

使用示例

await controller.pauseCamera();

3.9 resumeCamera 方法

resumeCamera 方法用于恢复相机预览和扫描。

Future<void> resumeCamera()

使用示例

await controller.resumeCamera();

3.10 stopCamera 方法

stopCamera 方法用于停止相机。

Future<void> stopCamera()

使用示例

await controller.stopCamera();

3.11 getSystemFeatures 方法

getSystemFeatures 方法用于获取设备支持的相机功能。

Future<SystemFeatures> getSystemFeatures()

返回值:返回 SystemFeatures 对象,包含设备功能信息。

使用示例

final features = await controller.getSystemFeatures();
print('有前置摄像头: ${features.hasFrontCamera}');
print('有后置摄像头: ${features.hasBackCamera}');
print('有闪光灯: ${features.hasFlash}');

3.12 dispose 方法

dispose 方法用于释放控制器资源。

void dispose()

使用示例


void dispose() {
  controller?.dispose();
  super.dispose();
}

四、数据类型详解

4.1 Barcode 类

Barcode 类封装了扫描结果信息。

class Barcode {
  Barcode(this.code, this.format, this.rawBytes);

  final String? code;           // 扫描内容
  final BarcodeFormat format;   // 码制格式
  final List<int>? rawBytes;    // 原始字节数据
}

属性说明

属性 类型 说明
code String? 扫描到的文本内容
format BarcodeFormat 条码格式类型
rawBytes List? 原始字节数据

4.2 BarcodeFormat 枚举

BarcodeFormat 定义了支持的条码格式。

enum BarcodeFormat {
  aztec,         // Aztec 2D 条码
  codabar,       // CODABAR 1D(iOS不支持)
  code39,        // Code 39 1D
  code93,        // Code 93 1D
  code128,       // Code 128 1D
  dataMatrix,    // Data Matrix 2D
  ean8,          // EAN-8 1D
  ean13,         // EAN-13 1D
  itf,           // ITF 1D
  maxicode,      // MaxiCode 2D(iOS不支持)
  pdf417,        // PDF417
  qrcode,        // QR Code 二维码
  rss14,         // RSS 14(iOS不支持)
  rssExpanded,   // RSS EXPANDED(iOS不支持)
  upcA,          // UPC-A 1D
  upcE,          // UPC-E 1D
  upcEanExtension, // UPC/EAN 扩展
  unknown,       // 未知格式
}

4.3 CameraFacing 枚举

CameraFacing 定义了摄像头方向。

enum CameraFacing {
  back,     // 后置摄像头
  front,    // 前置摄像头
  unknown,  // 未知
}

4.4 SystemFeatures 类

SystemFeatures 类包含设备相机功能信息。

class SystemFeatures {
  final bool hasBackCamera;    // 是否有后置摄像头
  final bool hasFrontCamera;   // 是否有前置摄像头
  final bool hasFlash;         // 是否有闪光灯
  final String? activeCamera;  // 当前激活的摄像头ID
}

五、Widget 组件详解

5.1 QrScannerOverlayShape 扫描框

QrScannerOverlayShape 是内置的扫描框样式组件,继承自 ShapeBorder。

class QrScannerOverlayShape extends ShapeBorder {
  QrScannerOverlayShape({
    this.borderColor = Colors.red,        // 边框颜色
    this.borderWidth = 3.0,               // 边框宽度
    this.overlayColor = const Color.fromRGBO(0, 0, 0, 80), // 遮罩颜色
    this.borderRadius = 0,                // 圆角半径
    this.borderLength = 40,               // 边角长度
    double? cutOutSize,                   // 扫描区域大小(正方形)
    double? cutOutWidth,                  // 扫描区域宽度
    double? cutOutHeight,                 // 扫描区域高度
    this.cutOutBottomOffset = 0,          // 裁剪区域底部偏移
  });
}

参数说明

参数 类型 默认值 说明
borderColor Color Colors.red 边框颜色
borderWidth double 3.0 边框宽度
overlayColor Color Color.fromRGBO(0,0,0,80) 遮罩颜色
borderRadius double 0 边角圆角半径
borderLength double 40 边角线条长度
cutOutSize double? 250 扫描区域大小
cutOutWidth double? null 扫描区域宽度
cutOutHeight double? null 扫描区域高度
cutOutBottomOffset double 0 扫描区域底部偏移

使用示例

QRView(
  key: qrKey,
  onQRViewCreated: _onQRViewCreated,
  overlay: QrScannerOverlayShape(
    borderColor: Colors.green,
    borderRadius: 10,
    borderLength: 30,
    borderWidth: 10,
    cutOutSize: 250,
  ),
)

5.2 自定义矩形扫描框

QrScannerOverlayShape(
  borderColor: Colors.blue,
  borderWidth: 4,
  borderRadius: 8,
  borderLength: 25,
  cutOutWidth: 300,
  cutOutHeight: 150,
  overlayColor: Colors.black.withOpacity(0.7),
)

六、OpenHarmony 平台实现原理

6.1 原生 API 映射

qr_code_scanner 在 OpenHarmony 平台上使用以下原生模块:

Flutter API OpenHarmony API
QRView PlatformView + XComponent
flipCamera camera.CameraManager + CameraInput
toggleFlash camera.FlashMode / TorchMode
pauseCamera PhotoSession.stop()
resumeCamera PhotoSession.start()
扫码解码 @kit.ScanKit detectBarcode.decodeImage

6.2 相机初始化实现

OpenHarmony 使用 CameraKit 初始化相机:

async initCamera(XComponentSurfaceId: string, front: boolean = false): Promise<void> {
  const cameraManager = camera.getCameraManager(this.context);
  this.camerasDevices = cameraManager.getSupportedCameras();
  
  // 选择前置或后置摄像头
  this.curcamerasDevice = front ? this.camerasDevices[1] : this.camerasDevices[0];
  
  // 创建预览输出
  let previewProfilesObj = this.getPreviewProfilesObj();
  let previewOutput = cameraManager.createPreviewOutput(
    previewProfilesObj, 
    XComponentSurfaceId
  );
  
  // 创建相机输入
  this.cameraInput = cameraManager.createCameraInput(this.curcamerasDevice);
  await this.cameraInput.open();
  
  // 创建拍照会话
  this.photoSession = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO);
  this.photoSession.beginConfig();
  this.photoSession.addInput(this.cameraInput);
  this.photoSession.addOutput(previewOutput);
  await this.photoSession.commitConfig();
  await this.photoSession.start();
}

6.3 扫码解码实现

OpenHarmony 使用 ScanKit 的 detectBarcode 进行解码:

decodeImageBuffer(imgComponent: image.Component, width: number, height: number, 
    channel: MethodChannel, barcodeFormats: Array<number>) {
  let byteImg: detectBarcode.ByteImage = {
    byteBuffer: imgComponent.byteBuffer,
    width: width,
    height: height,
    format: detectBarcode.ImageFormat.NV21
  };
  
  let options: scanBarcode.ScanOptions = {
    scanTypes: [scanCore.ScanType.ALL],
    enableMultiMode: false,
    enableAlbum: false
  };
  
  detectBarcode.decodeImage(byteImg, options).then((result) => {
    if (result.scanResults.length) {
      const res = result.scanResults[0];
      const codes = new Map<string, string | object>();
      codes.set('code', res.originalValue);
      codes.set('type', ConstCodeFormat.BarcodeFormatToString(res.scanType));
      channel.invokeMethod('onRecognizeQR', codes);
    }
  });
}

6.4 闪光灯控制实现

setFlashMode(isOpen: boolean): void {
  if (!this.hasFlash()) {
    return;
  }
  const newMode = isOpen 
    ? camera.FlashMode.FLASH_MODE_ALWAYS_OPEN 
    : camera.FlashMode.FLASH_MODE_CLOSE;
  
  if (!this.isFlashModeSupported(newMode)) {
    return;
  }
  this.photoSession.setFlashMode(newMode);
}

6.5 PlatformView 实现

OhosView 使用 XComponent 显示相机预览:

@Entry
@Component
export struct OhosView {
  @Prop params: Params;
  @Prop channel: MethodChannel | null = null;
  @State surfaceId: string = '';
  private mXComponentController: XComponentController = new XComponentController();
  
  build() {
    Stack() {
      if (this.userGrant) {
        Column() {
          XComponent({
            id: 'componentId',
            type: XComponentType.SURFACE,
            controller: this.mXComponentController
          })
            .onLoad(() => {
              this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
              this.scan();
            })
            .width(this.cameraWidth)
            .height(this.cameraHeight)
        }
      }
    }
  }
}

七、MethodChannel 通信协议

7.1 方法列表

方法 参数 返回值 说明
startScan List barcodeFormats null 开始扫描
getCameraInfo - int 获取摄像头信息
flipCamera - int 切换摄像头
getFlashInfo - bool 获取闪光灯状态
toggleFlash - bool? 切换闪光灯
pauseCamera - null 暂停相机
resumeCamera - bool 恢复相机
stopCamera - null 停止相机
getSystemFeatures - Map 获取设备功能
changeScanArea scanAreaWidth, scanAreaHeight null 设置扫描区域

7.2 扫描结果回调

{
  "code": "https://example.com",
  "type": "QR_CODE",
  "rawBytes": [...]
}

7.3 设备功能返回格式

{
  "hasFrontCamera": true,
  "hasBackCamera": true,
  "hasFlash": true,
  "activeCamera": "0"
}

八、实战案例

8.1 完整扫码应用示例

在这里插入图片描述

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:qr_code_scanner_ohos/qr_code_scanner_ohos.dart';

void main() {
  runApp(const MaterialApp(home: MyHome()));
}

class MyHome extends StatelessWidget {
  const MyHome({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('二维码扫描示例')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => const QRScannerPage()),
            );
          },
          child: const Text('打开扫描器'),
        ),
      ),
    );
  }
}

class QRScannerPage extends StatefulWidget {
  const QRScannerPage({super.key});

  
  State<StatefulWidget> createState() => _QRScannerPageState();
}

class _QRScannerPageState extends State<QRScannerPage> with WidgetsBindingObserver {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  Barcode? result;
  QRViewController? controller;
  bool isFlashOn = false;
  CameraFacing cameraFacing = CameraFacing.back;
  
  Barcode? lastResult;
  DateTime? lastScanTime;

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  
  void reassemble() {
    super.reassemble();
    if (Platform.isAndroid || defaultTargetPlatform == TargetPlatform.ohos) {
      controller?.pauseCamera();
    } else if (Platform.isIOS) {
      controller?.resumeCamera();
    }
  }

  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.paused:
        controller?.pauseCamera();
        break;
      case AppLifecycleState.resumed:
        controller?.resumeCamera();
        break;
      default:
        break;
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(flex: 4, child: _buildQrView(context)),
          Expanded(
            flex: 1,
            child: Container(
              color: Colors.grey[200],
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  if (result != null)
                    Padding(
                      padding: const EdgeInsets.all(16),
                      child: Text(
                        '类型: ${describeEnum(result!.format)}\n数据: ${result!.code}',
                        textAlign: TextAlign.center,
                        style: const TextStyle(fontSize: 16),
                      ),
                    )
                  else
                    const Text('请扫描二维码'),
                  _buildControlButtons(),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildQrView(BuildContext context) {
    var scanArea = (MediaQuery.of(context).size.width < 400 ||
            MediaQuery.of(context).size.height < 400)
        ? 150.0
        : 300.0;

    return QRView(
      key: qrKey,
      onQRViewCreated: _onQRViewCreated,
      overlay: QrScannerOverlayShape(
        borderColor: Colors.red,
        borderRadius: 10,
        borderLength: 30,
        borderWidth: 10,
        cutOutSize: scanArea,
      ),
      onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
      formatsAllowed: const [
        BarcodeFormat.qrcode,
        BarcodeFormat.ean13,
        BarcodeFormat.code128,
      ],
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) {
      final now = DateTime.now();
      if (lastResult?.code != scanData.code ||
          lastScanTime == null ||
          now.difference(lastScanTime!).inSeconds > 2) {
        setState(() {
          result = scanData;
          lastResult = scanData;
          lastScanTime = now;
        });
      }
    });
  }

  void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
    if (!p) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('相机权限未授予')),
      );
    }
  }

  Widget _buildControlButtons() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Container(
          margin: const EdgeInsets.all(8),
          child: ElevatedButton.icon(
            onPressed: () async {
              await controller?.toggleFlash();
              final flashStatus = await controller?.getFlashStatus();
              setState(() {
                isFlashOn = flashStatus ?? false;
              });
            },
            icon: Icon(isFlashOn ? Icons.flash_on : Icons.flash_off),
            label: Text(isFlashOn ? '关闭闪光灯' : '打开闪光灯'),
          ),
        ),
        Container(
          margin: const EdgeInsets.all(8),
          child: ElevatedButton.icon(
            onPressed: () async {
              await controller?.flipCamera();
              final cameraInfo = await controller?.getCameraInfo();
              setState(() {
                cameraFacing = cameraInfo ?? CameraFacing.back;
              });
            },
            icon: const Icon(Icons.flip_camera_android),
            label: Text(cameraFacing == CameraFacing.back ? '前置摄像头' : '后置摄像头'),
          ),
        ),
      ],
    );
  }

  
  void dispose() {
    controller?.dispose();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}

代码要点说明

功能模块 实现方式
实时扫描 通过 scannedDataStream 监听扫描结果
闪光灯控制 toggleFlash() 切换,getFlashStatus() 获取状态
摄像头切换 flipCamera() 切换前后摄像头
权限处理 onPermissionSet 回调处理权限结果
生命周期管理 WidgetsBindingObserver 监听应用状态变化
扫描格式限制 formatsAllowed 参数限制支持的码制
防重复扫描 时间间隔判断,2秒内相同结果不重复触发
热重载处理 reassemble() 方法处理热重载时的相机状态

九、常见问题与解决方案

9.1 相机权限问题

问题:应用启动后无法显示相机预览。

解决方案

  1. 确保 module.json5 中已正确配置相机权限
  2. 检查权限说明字符串是否已添加
  3. 使用 onPermissionSet 回调处理权限结果:
QRView(
  key: qrKey,
  onQRViewCreated: _onQRViewCreated,
  onPermissionSet: (controller, hasPermission) {
    if (!hasPermission) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请授予相机权限')),
      );
    }
  },
)

9.2 热重载问题

问题:热重载后相机无法正常工作。

解决方案


void reassemble() {
  super.reassemble();
  if (Platform.isAndroid || defaultTargetPlatform == TargetPlatform.ohos) {
    controller?.pauseCamera();
  } else if (Platform.isIOS) {
    controller?.resumeCamera();
  }
}

9.3 扫描速度慢

问题:扫描识别速度较慢。

解决方案

  1. 限制扫描的码制格式,减少解码计算量:
QRView(
  key: qrKey,
  onQRViewCreated: _onQRViewCreated,
  formatsAllowed: const [BarcodeFormat.qrcode],  // 仅扫描 QR Code
)
  1. 调整扫描区域大小,聚焦关键区域

9.4 弱光环境下扫描困难

问题:在光线不足的环境下难以扫描。

解决方案

Future<void> _enableFlashForLowLight() async {
  final flashStatus = await controller?.getFlashStatus();
  if (flashStatus == false) {
    await controller?.toggleFlash();
    setState(() {
      isFlashOn = true;
    });
  }
}

9.5 扫描结果重复触发

问题:扫描成功后结果多次触发。

解决方案

Barcode? lastResult;
DateTime? lastScanTime;

void _onQRViewCreated(QRViewController controller) {
  controller.scannedDataStream.listen((scanData) {
    final now = DateTime.now();
    if (lastResult?.code != scanData.code || 
        lastScanTime == null ||
        now.difference(lastScanTime!).inSeconds > 2) {
      setState(() {
        result = scanData;
        lastResult = scanData;
        lastScanTime = now;
      });
    }
  });
}

十、与 mobile_scanner 对比

特性 qr_code_scanner mobile_scanner
维护状态 维护模式 活跃开发
底层框架 zxing / MTBBarcode ML Kit / Vision
性能 良好 优秀
API 复杂度 简单 中等
自定义能力 中等
OpenHarmony 支持
从图片扫描
多码同时识别

建议

  • 新项目推荐使用 mobile_scanner
  • 已有项目迁移可继续使用 qr_code_scanner
  • 需要从图片扫描二维码的场景选择 mobile_scanner

十一、总结

qr_code_scanner_ohos 为 Flutter for OpenHarmony 提供了完整的二维码扫描能力,支持实时扫描、闪光灯控制、摄像头切换等功能。虽然原库已进入维护模式,但对于需要迁移的现有项目仍然是一个可靠的选择。

核心要点

  1. 使用 Platform View 架构嵌入原生相机视图
  2. 通过 QRViewController 控制扫描行为
  3. 通过 scannedDataStream 监听扫描结果
  4. 正确处理权限和生命周期
  5. 可根据需求选择限制码制格式

最佳实践

  • 始终检查并请求相机权限
  • 正确管理控制器生命周期
  • 提供友好的用户引导和错误提示
  • 考虑弱光环境下的用户体验
  • 新项目建议评估 mobile_scanner 作为替代方案
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐