本系列的Flutter文章分为三篇,这个是第三篇
本篇是基于第一篇已经引入Flutter官方的Camera库的基础之上开发的
本篇主要是讲解使用Google官方的MLKit库来实现扫描二维码和条形码功能,这样的就可以完全抛弃其他的第三方扫码库,包括之前毁誉参半的zxing库
Android原生版的相机扫码功能传送门:点我点我
第一步要实现扫码功能首先要引入Google官方的MLKit库中的扫码功能
google_mlkit_barcode_scanning: ^0.8.0
小提示
Google的MLKit库一开始专门为Android相机使用的,现在也有flutter的官方库了,可以放心使用
而且MLKit库中还包含面部识别,文字识别等功能,可以按需导入MLKit官网地址
Flutter扫描二维码的整体思路就是:
CameraController开启预览把视频流回传给MLKit库的解析组件,解析组件再返回扫描出的数据
第二步开启视频预览,将预览逻辑封装在了一个独立的widget中
class ScanQRCodeViewState extends State<ScanQRCodeView> {
final List<CameraDescription> _cameras = [];//可用的摄像头集合
final BarcodeScanner _barcodeScanner = BarcodeScanner();//扫码库
CameraController? _controller;
int _currentCameraIndex = -1; //当前所选的摄像头
bool _isChangingCameraLens = false;//正在切换摄像头标记
double _currentZoomLevel = 1.0;//当前放大级别 双指放大预览画面使用
double _minAvailableZoom = 1.0;//最小放大级别
double _maxAvailableZoom = 1.0;//最大放大级别
//画面旋转方向 主要是Android需要
final _orientations = {
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeLeft: 90,
DeviceOrientation.portraitDown: 180,
DeviceOrientation.landscapeRight: 270,
};
初始化相机
@override
void initState() {
super.initState();
_initCamera();
}
///初始化摄像头
void _initCamera() async {
if (_cameras.isEmpty) {
final list = await availableCameras();
_cameras.addAll(list);
}
for (var i = 0; i < _cameras.length; i++) {
if (_cameras[i].lensDirection == CameraLensDirection.back) {
//默认选择后置摄像头
_currentCameraIndex = i;
break;
}
}
if (_currentCameraIndex != -1) {
startLiveFeed();
}
}
在接下来就要初始化CameraController的参数
///开始接收画面
Future<void> startLiveFeed() async {
final camera = _cameras[_currentCameraIndex];//获取到当前摄像头
_controller = CameraController(
camera,
ResolutionPreset.high, //代表是720p的画面 还可以更高
enableAudio: false,//不需要音频
imageFormatGroup: Platform.isAndroid ? ImageFormatGroup.nv21 : ImageFormatGroup.bgra8888,//输出图片的格式
);
_initControllerParams();
}
最后初始化CameraController,并且要绑定解析逻辑。第一篇文章之前提到过,初始化的过程是个异步的过程
///初始化controller
Future<void> _initControllerParams() async {
_controller?.initialize().then((value) async {
//获取画面的缩放级别
double minZoomLevel = await _controller!.getMinZoomLevel();
_currentZoomLevel = minZoomLevel;
_minAvailableZoom = minZoomLevel;
double maxZoomLevel = await _controller!.getMaxZoomLevel();
_maxAvailableZoom = maxZoomLevel;
//这里就是处理视频流的逻辑了
_controller?.startImageStream(_processCameraImage);
//设置闪光灯类型为自动
_controller?.setFlashMode(FlashMode.auto);
_isProcessImage = false;
if (!mounted) {
return;
}
setState(() {});
});
}
第三步处理解析视频流逻辑
//跟CameraController绑定的回调
void _processCameraImage(CameraImage image) {
if (_isSelectingPhoto) {
//正在从相册选照片就不处理视频流
return;
}
final inputImage = _inputImageFromCameraImage(image);
if (inputImage == null) return;
_analysisImage(inputImage);
}
获取inputImage对象
InputImage? _inputImageFromCameraImage(CameraImage image) {
if (_controller == null) return null;
final camera = _cameras[_currentCameraIndex];
final sensorOrientation = camera.sensorOrientation;
InputImageRotation? rotation;//Android和iOS获取旋转方向的方式是不一样的
if (Platform.isIOS) {
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
} else if (Platform.isAndroid) {
var rotationCompensation = _orientations[_controller!.value.deviceOrientation];
if (rotationCompensation == null) return null;
if (camera.lensDirection == CameraLensDirection.front) {
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
} else {
rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360;
}
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
}
if (rotation == null) return null;
final format = InputImageFormatValue.fromRawValue(image.format.raw);
if (format == null ||
(Platform.isAndroid && format != InputImageFormat.nv21) ||
(Platform.isIOS && format != InputImageFormat.bgra8888)) return null;
if (image.planes.length != 1) return null;
final plane = image.planes.first;
return InputImage.fromBytes(
bytes: plane.bytes,
metadata: InputImageMetadata(
size: Size(image.width.toDouble(), image.height.toDouble()),
rotation: rotation, // 只有Android才会用到
format: format, // 只有iOS才会用到
bytesPerRow: plane.bytesPerRow, // 只有iOS才会用到
),
);
}
InputImage对象是MLKit库中对于图片信息封装的数据类
class InputImage {
/// The file path to the image.
final String? filePath;
/// The bytes of the image.
final Uint8List? bytes;
/// The type of image.
final InputImageType type;
/// The image data when creating an image of type = [InputImageType.bytes].
final InputImageMetadata? metadata;
InputImage._({this.filePath, this.bytes, required this.type, this.metadata});
/// Creates an instance of [InputImage] from path of image stored in device.
factory InputImage.fromFilePath(String path) {
return InputImage._(filePath: path, type: InputImageType.file);
}
/// Creates an instance of [InputImage] by passing a file.
factory InputImage.fromFile(File file) {
return InputImage._(filePath: file.path, type: InputImageType.file);
}
/// Creates an instance of [InputImage] using bytes.
factory InputImage.fromBytes(
{required Uint8List bytes, required InputImageMetadata metadata}) {
return InputImage._(
bytes: bytes, type: InputImageType.bytes, metadata: metadata);
}
第四步用BarcodeScanner来解析InputImage数据
///分析图片
void _analysisImage(InputImage inputImage) async {
//解析出的二维码或者条形码可能是多个
final barcodes = await _barcodeScanner.processImage(inputImage);
if (barcodes.isEmpty) {
return;
}
if (_isProcessImage) {
return;
}
_isProcessImage = true;
List<String> list = barcodes.map((barcode) => barcode.displayValue ?? '').toList();
widget.onCodeList(list);//给widget的回调进行处理
pausePreview();
}
最后的一些细节
停止视频预览和解析的方法
///停止接收画面
Future<void> stopLiveFeed() async {
if (_isControllerDispose) {
return;
}
_isControllerDispose = true;
await _controller?.setFlashMode(FlashMode.off);
await _controller?.stopImageStream();
await _controller?.dispose();
_controller = null;
}
切换前后摄像头的方法,Flutter切换摄像头的时候要先停止视频流 再重新开启
Future _switchLiveCamera() async {
setState(() => _isChangingCameraLens = true);
_currentCameraIndex = (_currentCameraIndex + 1) % _cameras.length;
await stopLiveFeed();
await startLiveFeed();
setState(() => _isChangingCameraLens = false);
}
切换闪光灯的方法也是异步的
Future _switchFlashMode() async {
_currentFlashIndex++;
if (_currentFlashIndex == flashModeArray.length) {
_currentFlashIndex = 0;
}
await _controller?.setFlashMode(_getFlashMode());
setState(() {});
}
FlashMode _getFlashMode() {
if (_currentFlashIndex == 1) {
return FlashMode.torch;
} else if (_currentFlashIndex == 2) {
return FlashMode.off;
}
return FlashMode.auto;
}
最后页面关闭的时候要释放资源
@override
void dispose() {
super.dispose();
stopLiveFeed();
_barcodeScanner.close();
}
本篇到此结束,希望可以帮助有需要的中小厂的朋友,欢迎各位交流~
GitHub项目地址,有需要的同学自取就行