本系列的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é)自取就行