Flutter原生相机实现拍照、录制视频、扫描二维码和条码系列3

2023-12-18  本文已影响0人  没有小叮当的大雄

本系列的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项目地址,有需要的同学自取就行

上一篇下一篇

猜你喜欢

热点阅读