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

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

作者: 没有小叮当的大雄 | 来源:发表于2023-12-18 11:11 被阅读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项目地址,有需要的同学自取就行

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/njkagdtx.html