HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-扫一扫场景
HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-扫一扫场景
1.1 场景概述
在移动应用开发中,不同设备的屏幕形态各异,例如刘海屏、全面屏、折叠屏等,系统状态栏、导航栏、软键盘等元素亦会占据屏幕空间。扫码类页面通常需要在全屏相机预览上叠加关闭按钮、重扫与图库入口,并在识别后基于码位信息绘制可点击标记;若扫码开关、结果去重与坐标归一化处理不完整,容易出现重复识别、标记错位或重扫无效等问题。本文将详细介绍本示例中实时扫码 + 图库解码、结果去重与重扫重建相机、码位箭头叠层导航详情页的适配方法,以及实现原理与具体场景案例。本文对应工程内扫一扫示例页。
1.1.1 使用场景
用户在扫码页面中需要快速完成实时识别与从图库选图识别两类操作;识别到码值后,可通过预览层上的箭头入口进入详情页查看类型、码值与位置信息。扫一扫适配验证(预览层控件与扫码结果协同自适应),指的是页面可在不同窗口尺寸下保持相机预览铺满、底部操作区可用,并在识别结果出现时正确叠加点击入口与结果列表状态。下列表格展示不同横向断点下的逻辑与布局。
| 横向断点 | sm | md | lg | xl |
|---|---|---|---|---|
| 展示逻辑 | 全屏相机预览;左上角关闭按钮与底部操作栏;支持实时扫码和图库识别;识别到结果后显示箭头入口 | 同左;closeBtnStyle 在 md 档增大至约 32×32 并上移到 top/left 20 | 同左;closeBtnStyle 在 lg 档约 36×36,位置 top/left 24 | 同 lg;PC 下可验证“图库选图”和“扫码成功后进入详情”链路 |
| 展示布局 | ![]() |
![]() |
![]() |
![]() |
1.1.2 常见问题
扫码类界面若未统筹,容易出现以下问题:
- 相机连续回调导致同一码值重复入列,界面出现重复结果
- 重扫仅清空文本未重建相机会话,扫描状态无法恢复
- 从图库解码未覆盖 JPEG / PNG 分支,导致部分图片无法识别
- 未做归一化坐标映射时,预览层箭头与实际码位不一致
1.1.3 多设备适配
- 适配点 1:实时扫码使用 Camera 的 scanBarcode 与 onReadCode,读取到 codeStringValue 后去重入列并关闭扫描开关,避免短时间重复触发。
图 1-0:手机竖屏下扫码主界面。

图 1-1:md 档扫码界面。

图 1-1a:手机横屏下扫码主界面。

图 1-2:lg 档扫码界面。

- 适配点 2:图库识别路径优先 launchImageLibrary + QRreader,若无结果再走 jsQR(JPEG 通过 jpeg-js、PNG 通过 pngjs 解码),并按图片宽高生成 positionNormalized。
图 2-0:手机竖屏下从图库选择图片。

图 2-0a:手机横屏下从图库选择图片。

图 2-1:PC 下从图库选择图片后的效果。

图 2-2:PC 下扫码成功后的效果。

图 2-3:手机竖屏下扫码成功后的效果。

图 2-4:手机横屏下扫码成功后的效果。

- 适配点 3:结果层使用 StyleSheet.absoluteFillObject 覆盖预览区;优先使用归一化中心点定位箭头,缺失位置时回退到右下角纵向堆叠;点击后导航 codeDetail 查看详情。
图 3-1:PC / 特大宽下整体布局。

1.2 开发指导
1.2.1 Flutter开发
1.2.1.1 关键能力
三方库mobile_scanner插件
在yaml里面增加camera依赖(image_picker插件可用实现选择图片扫描)
image_picker:
git:
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
path: "packages/image_picker/image_picker"
mobile_scanner:
git:
url: https://gitcode.com/openharmony-sig/fluttertpc_mobile_scanner.git
1.2.1.2 指导案例
- 引入核心库
import 'package:flutter/foundation.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
- 使用以下命令导入软件包package:mobile_scanner/mobile_scanner.dart。唯一必需的参数是onDetect,它会返回扫描的条形码或二维码。
MobileScanner(
onDetect: (result) {
print(result.barcodes.first.rawValue);
},
),
- 如果您想对扫描仪进行更多控制,则需要创建一个新的MobileScannerController控制器。该控制器包含多个参数,用于调整扫描仪。
final MobileScannerController controller = MobileScannerController(
cameraResolution: size,
detectionSpeed: detectionSpeed,
detectionTimeoutMs: detectionTimeout,
formats: selectedFormats,
returnImage: returnImage,
torchEnabled: true,
invertImage: invertImage,
autoZoom: autoZoom,
);
....
MobileScanner(
controller: controller,
onDetect: (result) {
print(result.barcodes.first.rawValue);
},
);
- 生命周期变化
如果要在应用程序不活动时暂停扫描仪,则需要使用WidgetsBindingObserver。
首先,StreamSubscription为条形码事件提供一个接口。另外,请确保创建一个设置为 false 的MobileScannerController接口autoStart,因为我们将自行处理生命周期。
final MobileScannerController controller = MobileScannerController(
autoStart: false,
);
StreamSubscription<Object?>? _subscription;
然后,确保你的State类混入了 mixedWidgetsBindingObserver来处理生命周期变更,并将所需的逻辑添加到didChangeAppLifecycleState函数中:
class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
// ...
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// If the controller is not ready, do not try to start or stop it.
// Permission dialogs can trigger lifecycle changes before the controller is ready.
if (!controller.value.hasCameraPermission) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
// Restart the scanner when the app is resumed.
// Don't forget to resume listening to the barcode events.
_subscription = controller.barcodes.listen(_handleBarcode);
unawaited(controller.start());
case AppLifecycleState.inactive:
// Stop the scanner when the app is paused.
// Also stop the barcode events subscription.
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
// ...
}
然后,启动扫描仪void initState():
@override
void initState() {
super.initState();
// Start listening to lifecycle changes.
WidgetsBinding.instance.addObserver(this);
// Start listening to the barcode events.
_subscription = controller.barcodes.listen(_handleBarcode);
// Finally, start the scanner itself.
unawaited(controller.start());
}
MobileScannerController最后,用完后请将其丢弃。
@override
Future<void> dispose() async {
// Stop listening to lifecycle changes.
WidgetsBinding.instance.removeObserver(this);
// Stop listening to the barcode events.
unawaited(_subscription?.cancel());
_subscription = null;
// Dispose the widget itself.
super.dispose();
// Finally, dispose of the controller.
await controller.dispose();
}
1.2.1.3 示例代码
相机的Sample示例代码地址:example ,开发者可以通过该地址查看完整的示例代码,并根据自己的需求进行修改和扩展。
更多关于HarmonyOS鸿蒙Next中Flutter框架多设备开发指导-扫一扫场景的实战教程也可以访问 https://www.itying.com/category-92-b0.html
在HarmonyOS Next中,使用Flutter框架实现扫一扫多设备场景,需通过Platform Channel调用鸿蒙原生的Scan Kit接口。Flutter侧用dart:io或MethodChannel触发扫描,设备间通信利用分布式数据管理或分布式软总线传递扫码结果。注意在EntryForm中声明ohos.permission.CAMERA权限,并配置元数据支持多设备协同。
该文档提供了HarmonyOS Next上基于Flutter实现扫一扫场景的多设备适配完整方案。总体思路是使用 mobile_scanner 插件驱动相机实时扫码,结合 image_picker 实现图库选图解码。适配核心在于UI控件随断点(sm/md/lg/xl)动态调整尺寸与位置,并保证预览层始终全屏。
关键适配点:
- 实时扫码通过
onDetect回调获取码值,立即关闭扫描开关进行去重,避免重复入列。 - 图库识别优先走原生解码器,失败时使用
jpeg-js/pngjs解析,并以图片宽高生成归一化坐标。 - 结果层用
StyleSheet.absoluteFillObject覆盖预览区,箭头按归一化中心点定位,缺失时回退至右下角堆叠;点击导航至详情页。 - 重扫时需重建相机会话并重置状态,而非仅清空文本。
常见问题如去重失败、重扫无效、箭头错位等均通过上述机制解决。完整示例可参考官方样例库 multidevice_adaption_sample_flutter 下的 scanner 目录。





