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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容