Flutter原生相機(jī)實現(xiàn)拍照、錄制視頻摊册、掃描二維碼和條碼系列3

本系列的Flutter文章分為三篇肤京,這個是第三篇
本篇是基于第一篇已經(jīng)引入Flutter官方的Camera庫的基礎(chǔ)之上開發(fā)的
本篇主要是講解使用Google官方的MLKit庫來實現(xiàn)掃描二維碼和條形碼功能,這樣的就可以完全拋棄其他的第三方掃碼庫茅特,包括之前毀譽(yù)參半的zxing庫
Android原生版的相機(jī)掃碼功能傳送門:點我點我

第一步要實現(xiàn)掃碼功能首先要引入Google官方的MLKit庫中的掃碼功能

google_mlkit_barcode_scanning: ^0.8.0

小提示
Google的MLKit庫一開始專門為Android相機(jī)使用的蟆沫,現(xiàn)在也有flutter的官方庫了,可以放心使用
而且MLKit庫中還包含面部識別温治,文字識別等功能饭庞,可以按需導(dǎo)入MLKit官網(wǎng)地址

Flutter掃描二維碼的整體思路就是:
CameraController開啟預(yù)覽把視頻流回傳給MLKit庫的解析組件,解析組件再返回掃描出的數(shù)據(jù)

第二步開啟視頻預(yù)覽熬荆,將預(yù)覽邏輯封裝在了一個獨立的widget中

class ScanQRCodeViewState extends State<ScanQRCodeView> {
  final List<CameraDescription> _cameras = [];//可用的攝像頭集合
  final BarcodeScanner _barcodeScanner = BarcodeScanner();//掃碼庫
  CameraController? _controller;
  int _currentCameraIndex = -1; //當(dāng)前所選的攝像頭
  bool _isChangingCameraLens = false;//正在切換攝像頭標(biāo)記

  double _currentZoomLevel = 1.0;//當(dāng)前放大級別 雙指放大預(yù)覽畫面使用
  double _minAvailableZoom = 1.0;//最小放大級別 
  double _maxAvailableZoom = 1.0;//最大放大級別
  
 //畫面旋轉(zhuǎn)方向 主要是Android需要
  final _orientations = {
    DeviceOrientation.portraitUp: 0,
    DeviceOrientation.landscapeLeft: 90,
    DeviceOrientation.portraitDown: 180,
    DeviceOrientation.landscapeRight: 270,
  };

初始化相機(jī)

  @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) {
        //默認(rèn)選擇后置攝像頭
        _currentCameraIndex = i;
        break;
      }
    }

    if (_currentCameraIndex != -1) {
      startLiveFeed();
    }
}

在接下來就要初始化CameraController的參數(shù)

///開始接收畫面
Future<void> startLiveFeed() async {
    final camera = _cameras[_currentCameraIndex];//獲取到當(dāng)前攝像頭
    _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);
      //設(shè)置閃光燈類型為自動
      _controller?.setFlashMode(FlashMode.auto);
      _isProcessImage = false;
      if (!mounted) {
        return;
      }
      setState(() {});
    });
  }

第三步處理解析視頻流邏輯

//跟CameraController綁定的回調(diào)
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獲取旋轉(zhuǎn)方向的方式是不一樣的
    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庫中對于圖片信息封裝的數(shù)據(jù)類

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數(shù)據(jù)

///分析圖片
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的回調(diào)進(jìn)行處理
    pausePreview();
}

最后的一些細(xì)節(jié)
停止視頻預(yù)覽和解析的方法

///停止接收畫面
Future<void> stopLiveFeed() async {
    if (_isControllerDispose) {
      return;
    }
    _isControllerDispose = true;
    await _controller?.setFlashMode(FlashMode.off);
    await _controller?.stopImageStream();
    await _controller?.dispose();
    _controller = null;
}

切換前后攝像頭的方法累盗,F(xiàn)lutter切換攝像頭的時候要先停止視頻流 再重新開啟

  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;
}

最后頁面關(guān)閉的時候要釋放資源

@override
  void dispose() {
    super.dispose();
    stopLiveFeed();
    _barcodeScanner.close();
  }

本篇到此結(jié)束,希望可以幫助有需要的中小廠的朋友突琳,歡迎各位交流~
GitHub項目地址若债,有需要的同學(xué)自取就行

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拆融,隨后出現(xiàn)的幾起案子蠢琳,更是在濱河造成了極大的恐慌啊终,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件傲须,死亡現(xiàn)場離奇詭異蓝牲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)泰讽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門例衍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人已卸,你說我怎么就攤上這事佛玄。” “怎么了累澡?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵翎嫡,是天一觀的道長。 經(jīng)常有香客問我永乌,道長惑申,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任翅雏,我火速辦了婚禮圈驼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘望几。我一直安慰自己绩脆,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布橄抹。 她就那樣靜靜地躺著靴迫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪楼誓。 梳的紋絲不亂的頭發(fā)上玉锌,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音疟羹,去河邊找鬼主守。 笑死,一個胖子當(dāng)著我的面吹牛榄融,可吹牛的內(nèi)容都是我干的参淫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼愧杯,長吁一口氣:“原來是場噩夢啊……” “哼涎才!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起力九,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤耍铜,失蹤者是張志新(化名)和其女友劉穎邑闺,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體业扒,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡检吆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年舒萎,在試婚紗的時候發(fā)現(xiàn)自己被綠了程储。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡臂寝,死狀恐怖章鲤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咆贬,我是刑警寧澤败徊,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站掏缎,受9級特大地震影響皱蹦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜眷蜈,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一沪哺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酌儒,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至榴啸,卻和暖如春孽惰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸥印。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工灰瞻, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人辅甥。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓酝润,卻偏偏與公主長得像,于是被迫代替她去往敵國和親璃弄。 傳聞我的和親對象是個殘疾皇子要销,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容