Flutter for OpenHarmony 二维码扫描App实战 - 批量扫描实现
批量扫描功能实现摘要 本文介绍了批量扫描二维码功能的实现方案,主要解决连续扫描多个二维码的效率问题。设计采用StatefulWidget管理扫描状态,核心功能包括:连续扫描保持状态、结果累积显示、单项删除和批量操作。页面分为扫描区域和结果列表区域,扫描区域模拟相机预览,结果列表使用ListView.builder动态渲染。当扫描记录为空时显示友好提示,有记录时以卡片形式展示序号、内容和类型,并提供
在某些场景下,用户需要连续扫描多个二维码,比如仓库盘点、活动签到等。如果每次扫描都要返回再重新进入,效率会很低。批量扫描功能让用户可以连续扫描多个二维码,扫描结果会累积显示在列表中,最后可以一次性保存或导出。
批量扫描的设计思路
批量扫描页面需要解决几个核心问题:
连续扫描:扫描成功后不跳转到结果页面,而是继续保持扫描状态,让用户可以立即扫描下一个。
结果累积:每次扫描的结果都添加到列表中,用户可以看到已经扫描了多少个,以及每个的内容。
单项管理:用户可以删除某一条扫描结果,比如扫描错误的时候。
批量操作:提供清空全部、保存全部、导出全部等批量操作功能。
BatchScanView 的整体结构
批量扫描页面使用 StatefulWidget,因为需要管理扫描结果列表的状态:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../data/models/qr_record.dart';
class BatchScanView extends StatefulWidget {
const BatchScanView({super.key});
State<BatchScanView> createState() => _BatchScanViewState();
}
class _BatchScanViewState extends State<BatchScanView> {
final List<QrRecord> scannedItems = [];
这里使用 StatefulWidget 而不是 GetX 的响应式状态管理,是因为批量扫描的状态只在这个页面内使用,不需要跨页面共享。使用 StatefulWidget 更简单直接。
scannedItems 是一个 QrRecord 列表,存储所有扫描到的记录。每次扫描成功后,会往这个列表添加一条记录。
模拟扫描方法
为了测试,添加一个模拟扫描的方法:
void _addDemoItem() {
setState(() {
scannedItems.add(QrRecord(
content: 'https://example.com/item${scannedItems.length + 1}',
type: QrType.url,
));
});
}
_addDemoItem 方法模拟一次扫描,创建一个 QrRecord 对象并添加到列表。内容使用序号区分,比如 “item1”、“item2” 等。
setState 是 StatefulWidget 更新状态的标准方法。调用 setState 后,Flutter 会重新调用 build 方法,UI 会更新显示新添加的记录。
在实际项目中,这个方法会被相机扫描的回调替代。当相机识别到二维码时,调用类似的逻辑添加记录。
AppBar 配置
批量扫描页面的应用栏:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('批量扫描'),
actions: [
if (scannedItems.isNotEmpty)
TextButton(
onPressed: () {
Get.snackbar('成功', '已保存${scannedItems.length}条记录',
snackPosition: SnackPosition.BOTTOM);
},
child: const Text('保存全部'),
),
],
),
actions 中有一个"保存全部"按钮,只有当列表不为空时才显示。使用 if (scannedItems.isNotEmpty) 进行条件渲染。
TextButton 比 IconButton 更适合这种文字操作,用户一眼就能知道这个按钮的功能。点击后会把所有记录保存到历史记录服务中。
Get.snackbar 显示操作结果,告诉用户保存了多少条记录。${scannedItems.length} 是字符串插值,会显示实际的数量。
扫描区域
页面上半部分是扫描区域:
body: Column(
children: [
Container(
height: 200.h,
color: Colors.black87,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.qr_code_scanner, size: 60.sp, color: Colors.white54),
SizedBox(height: 12.h),
Text(
'批量扫描模式',
style: TextStyle(color: Colors.white70, fontSize: 14.sp),
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: _addDemoItem,
child: const Text('模拟扫描一个'),
),
],
),
),
),
扫描区域使用 Container 包裹,高度固定为 200.h。背景色 Colors.black87 是深灰色,模拟相机预览的效果。内部使用 Column 垂直排列图标、文字和按钮。
Icon 使用 qr_code_scanner 图标,大小 60.sp,颜色是半透明白色。下方的文字"批量扫描模式"提示用户当前处于批量扫描状态。
ElevatedButton 是模拟扫描的按钮,点击调用 _addDemoItem 方法。在实际项目中,这个区域会被相机预览组件替代,扫描成功后自动调用添加记录的方法。
已扫描列表区域
扫描区域下方是已扫描的记录列表:
Expanded(
child: scannedItems.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox, size: 60.sp, color: Colors.grey),
SizedBox(height: 12.h),
Text(
'暂无扫描记录',
style: TextStyle(color: Colors.grey, fontSize: 14.sp),
),
],
),
)
Expanded 让列表区域占据剩余的所有空间。使用三元表达式判断列表是否为空,为空时显示空状态提示,不为空时显示列表。
空状态使用 Center 居中显示,包含一个收件箱图标和"暂无扫描记录"的文字。这种空状态设计比直接显示空白更友好,让用户知道这里会显示什么内容。
扫描记录列表
当有扫描记录时,使用 ListView.builder 显示:
: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: scannedItems.length,
itemBuilder: (context, index) {
final item = scannedItems[index];
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text('${index + 1}'),
),
title: Text(
item.content,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(item.typeLabel),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () {
setState(() => scannedItems.removeAt(index));
},
),
),
);
},
),
),
],
),
ListView.builder 是懒加载列表,只渲染可见的项目,适合长列表。padding 设置四周 16.w 的内边距。
每个列表项使用 Card 包裹,提供卡片效果。ListTile 是 Material Design 的标准列表项组件,包含 leading、title、subtitle 和 trailing 四个位置。
leading 使用 CircleAvatar 显示序号,让用户知道这是第几个扫描的。title 显示二维码内容,限制一行并用省略号截断。subtitle 显示二维码类型。trailing 是删除按钮,点击后从列表中移除该项。
底部操作栏
页面底部是清空和导出两个按钮:
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => setState(() => scannedItems.clear()),
child: const Text('清空'),
),
),
SizedBox(width: 16.w),
Expanded(
child: ElevatedButton(
onPressed: scannedItems.isEmpty ? null : () {
Get.snackbar('成功', '已导出${scannedItems.length}条记录',
snackPosition: SnackPosition.BOTTOM);
},
child: const Text('导出'),
),
),
],
),
),
),
);
}
}
bottomNavigationBar 是 Scaffold 的底部导航栏位置,这里用来放置操作按钮。SafeArea 确保按钮不会被系统导航栏遮挡。
Row 让两个按钮水平排列,Expanded 让它们平分宽度。清空按钮使用 OutlinedButton 描边样式,表示这是次要操作。导出按钮使用 ElevatedButton 凸起样式,表示这是主要操作。
导出按钮的 onPressed 使用三元表达式,列表为空时设为 null 禁用按钮。这是一个很好的用户体验细节,避免用户点击无效的按钮。
批量扫描的实际应用
在实际项目中,批量扫描功能需要集成相机扫描库。以 mobile_scanner 为例:
import 'package:mobile_scanner/mobile_scanner.dart';
class _BatchScanViewState extends State<BatchScanView> {
final MobileScannerController _scannerController = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
final List<QrRecord> scannedItems = [];
final Set<String> _scannedContents = {};
void _onDetect(BarcodeCapture capture) {
for (final barcode in capture.barcodes) {
final content = barcode.rawValue;
if (content != null && !_scannedContents.contains(content)) {
_scannedContents.add(content);
setState(() {
scannedItems.add(QrRecord(
content: content,
type: _detectType(content),
));
});
_playBeep();
}
}
}
}
MobileScannerController 控制相机的行为,detectionSpeed 设置检测速度,facing 设置使用后置摄像头。
_scannedContents 是一个 Set,用于存储已扫描的内容,避免重复添加相同的二维码。这在批量扫描时很重要,因为相机可能会多次识别同一个二维码。
扫描区域的相机预览
将模拟的扫描区域替换为真实的相机预览:
Container(
height: 200.h,
child: ClipRRect(
borderRadius: BorderRadius.circular(0),
child: MobileScanner(
controller: _scannerController,
onDetect: _onDetect,
overlay: Center(
child: Container(
width: 150.w,
height: 150.w,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12.r),
),
),
),
),
),
),
MobileScanner 是相机预览组件,controller 传入控制器,onDetect 是识别回调。overlay 可以在预览上叠加自定义 UI,这里添加了一个扫描框。
扫描提示音
扫描成功后播放提示音,给用户反馈:
import 'package:audioplayers/audioplayers.dart';
class _BatchScanViewState extends State<BatchScanView> {
final AudioPlayer _audioPlayer = AudioPlayer();
void _playBeep() async {
await _audioPlayer.play(AssetSource('sounds/beep.mp3'));
}
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
}
使用 audioplayers 插件播放音频。AssetSource 从 assets 目录加载音频文件。记得在 dispose 中释放资源。
震动反馈
除了声音,还可以添加震动反馈:
import 'package:vibration/vibration.dart';
void _onScanSuccess() async {
if (await Vibration.hasVibrator() ?? false) {
Vibration.vibrate(duration: 100);
}
}
Vibration 插件提供震动功能。先检查设备是否支持震动,然后调用 vibrate 方法,duration 设置震动时长(毫秒)。
导出功能的实现
导出功能可以将扫描结果保存为文件或分享:
import 'package:share_plus/share_plus.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<void> _exportRecords() async {
if (scannedItems.isEmpty) return;
final buffer = StringBuffer();
buffer.writeln('批量扫描结果');
buffer.writeln('扫描时间: ${DateTime.now()}');
buffer.writeln('总数: ${scannedItems.length}');
buffer.writeln('---');
for (var i = 0; i < scannedItems.length; i++) {
final item = scannedItems[i];
buffer.writeln('${i + 1}. [${item.typeLabel}] ${item.content}');
}
final directory = await getTemporaryDirectory();
final file = File('${directory.path}/scan_result.txt');
await file.writeAsString(buffer.toString());
await Share.shareXFiles([XFile(file.path)], text: '批量扫描结果');
}
StringBuffer 用于高效拼接字符串。getTemporaryDirectory 获取临时目录路径。Share.shareXFiles 调用系统分享功能。
保存到历史记录
将批量扫描的结果保存到历史记录服务:
void _saveAllRecords() {
final historyService = Get.find<QrHistoryService>();
for (final item in scannedItems) {
historyService.addScanRecord(item.copyWith(
scanTime: DateTime.now(),
source: 'batch',
));
}
Get.snackbar(
'保存成功',
'已保存 ${scannedItems.length} 条记录到历史',
snackPosition: SnackPosition.BOTTOM,
);
setState(() {
scannedItems.clear();
_scannedContents.clear();
});
}
遍历所有记录,调用 historyService.addScanRecord 保存。保存后清空当前列表,准备下一轮扫描。
扫描统计信息
在页面顶部显示扫描统计:
Widget _buildStats() {
final typeCount = <QrType, int>{};
for (final item in scannedItems) {
typeCount[item.type] = (typeCount[item.type] ?? 0) + 1;
}
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
color: Theme.of(context).primaryColor.withOpacity(0.1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_StatItem('总数', '${scannedItems.length}'),
...typeCount.entries.take(3).map((e) =>
_StatItem(e.key.label, '${e.value}'),
),
],
),
);
}
统计各类型的数量,显示在页面顶部。用户可以一眼看到扫描了多少个,各类型分别有多少。
列表项的滑动删除
给列表项添加滑动删除功能:
Dismissible(
key: Key(item.content + index.toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 16.w),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
setState(() {
scannedItems.removeAt(index);
_scannedContents.remove(item.content);
});
},
child: Card(
child: ListTile(
// 列表项内容
),
),
),
Dismissible 组件提供滑动删除功能。direction 设置只能从右向左滑动。background 是滑动时显示的背景。onDismissed 是删除后的回调。
暂停和继续扫描
添加暂停扫描的功能,方便用户查看已扫描的内容:
bool _isPaused = false;
void _togglePause() {
setState(() {
_isPaused = !_isPaused;
if (_isPaused) {
_scannerController.stop();
} else {
_scannerController.start();
}
});
}
暂停时停止相机扫描,继续时重新开始。按钮图标根据状态切换。
二维码类型自动识别
根据内容自动识别二维码类型:
QrType _detectType(String content) {
if (content.startsWith('http://') || content.startsWith('https://')) {
return QrType.url;
} else if (content.startsWith('WIFI:')) {
return QrType.wifi;
} else if (content.startsWith('tel:')) {
return QrType.phone;
} else if (content.startsWith('mailto:')) {
return QrType.email;
} else if (content.startsWith('smsto:')) {
return QrType.sms;
} else if (content.startsWith('BEGIN:VCARD')) {
return QrType.contact;
} else if (content.startsWith('BEGIN:VEVENT')) {
return QrType.event;
} else if (content.startsWith('geo:')) {
return QrType.location;
}
return QrType.text;
}
根据内容的前缀判断类型。网址以 http 或 https 开头,WiFi 以 WIFI: 开头,电话以 tel: 开头,等等。
批量扫描的应用场景
批量扫描功能在很多场景下都很实用:
仓库盘点:连续扫描商品条码,记录库存数量。
活动签到:扫描参会者的二维码门票,快速完成签到。
资产管理:扫描设备标签,建立资产清单。
物流分拣:扫描包裹条码,记录分拣信息。
性能优化建议
批量扫描可能会产生大量数据,需要注意性能:
列表优化:使用 ListView.builder 而不是 ListView,避免一次性创建所有列表项。
去重处理:使用 Set 存储已扫描的内容,O(1) 时间复杂度判断是否重复。
内存管理:如果扫描数量很大,考虑分批保存到数据库,而不是全部保存在内存中。
UI 更新:避免频繁调用 setState,可以使用节流或防抖。
小结
批量扫描功能让用户可以连续扫描多个二维码,提高效率。实现时需要考虑去重、提示反馈、列表管理、导出保存等多个方面。
页面布局分为扫描区域和列表区域两部分,底部是操作按钮。使用 StatefulWidget 管理扫描结果列表的状态。实际项目中需要集成相机扫描库,添加声音和震动反馈,提供导出和保存功能。
批量扫描在仓库盘点、活动签到、资产管理等场景下非常实用,是二维码应用的重要功能之一。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)