Google今年推出了Flutter 2。一套Dart代码可以覆盖桌面,web和移动开发,大大降低跨平台应用开发成本。然而平台相关的逻辑还是需要用相应的本地代码去实现。这篇文章分享如何从0开始,搭建发布Flutter的条形码二维码扫描插件,以及如何使用该插件来实现一个Android的扫码应用。
Flutter安装
参考Flutter中国区安装指南:https://flutter.dev/community/china
Linux, mac配置环境变量:
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
Windows在系统设置里添加PUB_HOSTED_URL
和FLUTTER_STORAGE_BASE_URL
。
Flutter条形码,二维码SDK插件搭建及发布
创建对应平台的插件模板。这里使用安卓:
flutter create --org com.dynamsoft --template=plugin --platforms=android -a java flutter_barcode_sdk
后面要添加iOS可以在当前工程目录里运行:
flutter create --template=plugin --platforms=ios .
开发Flutter插件
Dart代码
打开自动生成的lib/flutter_barcode_sdk.dart
文件。这里面定义插件的API。
创建一个BarcodeResult
类,用于反序列化Android Java返回的结果数据:
class BarcodeResult {
final String format;
final String text;
final int x1;
final int y1;
final int x2;
final int y2;
final int x3;
final int y3;
final int x4;
final int y4;
BarcodeResult(this.format, this.text, this.x1, this.y1, this.x2, this.y2,
this.x3, this.y3, this.x4, this.y4);
BarcodeResult.fromJson(Map<dynamic, dynamic> json)
: format = json['format'],
text = json['text'],
x1 = json['x1'],
y1 = json['y1'],
x2 = json['x2'],
y2 = json['y2'],
x3 = json['x3'],
y3 = json['y3'],
x4 = json['x4'],
y4 = json['y4'];
Map<String, dynamic> toJson() => {
'format': format,
'text': text,
'x1': x1,
'y1': y1,
'x2': x2,
'y2': y2,
'x3': x3,
'y3': y3,
'x4': x4,
'y4': y4,
};
}
创建两个接口。decodeFile()
用于文件解码,decodeImageBuffer()
用于图像数据解码。
List<BarcodeResult> _convertResults(List<Map<dynamic, dynamic>> ret) {
return ret.map((data) => BarcodeResult.fromJson(data)).toList();
}
Future<List<BarcodeResult>> decodeFile(String filename) async {
List<Map<dynamic, dynamic>> ret = List<Map<dynamic, dynamic>>.from(
await _channel.invokeMethod('decodeFile', {'filename': filename}));
return _convertResults(ret);
}
Future<List<BarcodeResult>> decodeImageBuffer(
Uint8List bytes, int width, int height, int stride, int format) async {
List<Map<dynamic, dynamic>> ret = List<Map<dynamic, dynamic>>.from(
await _channel.invokeMethod('decodeImageBuffer', {
'bytes': bytes,
'width': width,
'height': height,
'stride': stride,
'format': format
}));
return _convertResults(ret);
}
这里要注意数据类型转换的问题。在Java层定义的List<Map<String, Object>>
类型是不能直接返回给Dart的List<Map<dynamic, dynamic>>
类型的。需要使用List<Map<dynamic, dynamic>>.from()
来转换。另外,使用map
把List<Map<dynamic, dynamic>>
转换成List<BarcodeResult>
。
Android Java代码
这里使用Dynamsoft Barcode Reader SDK。
首先打开android
目录下的build.gradle
文件添加依赖:
rootProject.allprojects {
repositories {
maven {
url "http://download2.dynamsoft.com/maven/dbr/aar"
}
google()
jcenter()
}
}
dependencies {
implementation 'com.dynamsoft:dynamsoftbarcodereader:8.2.0@aar'
}
然后打开android/src/main/java/com/dynamsoft/flutter_barcode_sdk/FlutterBarcodeSdkPlugin.java
添加Java代码。
onMethodCall
是Dart通向Java的入口:
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "getPlatformVersion":
result.success("Android " + android.os.Build.VERSION.RELEASE);
break;
case "decodeFile": {
final String filename = call.argument("filename");
List<Map<String, Object>> results = mBarcodeManager.decodeFile(filename);
result.success(results);
}
break;
case "decodeFileBytes": {
final byte[] bytes = call.argument("bytes");
List<Map<String, Object>> results = mBarcodeManager.decodeFileBytes(bytes);
result.success(results);
}
break;
case "decodeImageBuffer": {
final byte[] bytes = call.argument("bytes");
final int width = call.argument("width");
final int height = call.argument("height");
final int stride = call.argument("stride");
final int format = call.argument("format");
final Result r = result;
mExecutor.execute(new Runnable() {
@Override
public void run() {
final List<Map<String, Object>> results = mBarcodeManager.decodeImageBuffer(bytes, width, height, stride, format);
mHandler.post(new Runnable() {
@Override
public void run() {
r.success(results);
}
});
}
});
}
break;
default:
result.notImplemented();
}
}
每一个case
对应一个接口名。这里的调用在主线程。在视频模式下会持续调用decodeImageBuffer
,为了不影响性能,把解码放到线程中执行。
新建BarcodeManager.java
来管理条形码解码相关代码。创建BarcodeReader
对象以及解码调用方法:
public BarcodeManager() {
try {
mBarcodeReader = new BarcodeReader();
DMLTSConnectionParameters parameters = new DMLTSConnectionParameters();
parameters.organizationID = "200001";
mBarcodeReader.initLicenseFromLTS(parameters, new DBRLTSLicenseVerificationListener() {
@Override
public void LTSLicenseVerificationCallback(boolean isSuccess, Exception error) {
if (!isSuccess) {
error.printStackTrace();
}
}
});
mBarcodeReader.initRuntimeSettingsWithString("{\"ImageParameter\":{\"Name\":\"Balance\",\"DeblurLevel\":5,\"ExpectedBarcodesCount\":512,\"LocalizationModes\":[{\"Mode\":\"LM_CONNECTED_BLOCKS\"},{\"Mode\":\"LM_SCAN_DIRECTLY\"}]}}", EnumConflictMode.CM_OVERWRITE);
PublicRuntimeSettings settings = mBarcodeReader.getRuntimeSettings();
settings.intermediateResultTypes = EnumIntermediateResultType.IRT_TYPED_BARCODE_ZONE;
settings.barcodeFormatIds = EnumBarcodeFormat.BF_ONED | EnumBarcodeFormat.BF_DATAMATRIX | EnumBarcodeFormat.BF_QR_CODE | EnumBarcodeFormat.BF_PDF417;
settings.barcodeFormatIds_2 = 0;
mBarcodeReader.updateRuntimeSettings(settings);
} catch (Exception e) {
e.printStackTrace();
}
}
public List<Map<String, Object>> decodeFile(String filename) {
List<Map<String, Object>> ret = new ArrayList<Map<String, Object>>();
try {
TextResult[] results = mBarcodeReader.decodeFile(filename, "");
wrapResults(results, ret);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
public List<Map<String, Object>> decodeImageBuffer(byte[] bytes, int width, int height, int stride, int format) {
List<Map<String, Object>> ret = new ArrayList<Map<String, Object>>();
int pixelFormat = EnumImagePixelFormat.IPF_BGR_888;
switch(format) {
case 0:
pixelFormat = EnumImagePixelFormat.IPF_GRAYSCALED;
break;
case 1:
pixelFormat = EnumImagePixelFormat.IPF_ARGB_8888;
break;
}
try {
TextResult[] results = mBarcodeReader.decodeBuffer(bytes, width, height, stride, pixelFormat, "");
wrapResults(results, ret);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
发布Flutter插件
发布前先运行下面的命令分析工程:
flutter pub publish --dry-run
如果没有问题,就可以发布了:
flutter pub publish
这里是我发布之后的页面:
https://pub.dev/packages/flutter_barcode_sdk
开发Flutter扫码应用
有了插件,就可以用Dart快速实现扫码应用了。
创建一个新的Flutter工程
flutter create myapp
添加依赖:
dependencies:
camera:
flutter_barcode_sdk:
打开main.dart
,初始化camera和barcode SDK:
CameraController _controller;
Future<void> _initializeControllerFuture;
FlutterBarcodeSdk _barcodeReader;
bool _isScanAvailable = true;
bool _isScanRunning = false;
String _barcodeResults = '';
String _buttonText = 'Start Video Scan';
@override
void initState() {
super.initState();
_controller = CameraController(
widget.camera,
ResolutionPreset.medium,
);
_initializeControllerFuture = _controller.initialize();
_initializeControllerFuture.then((_) {
setState(() {});
});
_barcodeReader = FlutterBarcodeSdk();
}
界面布局。创建视频预览窗口,文字显示控件和两个按钮:
@override
Widget build(BuildContext context) {
return Column(children: [
Expanded(child: getCameraWidget()),
Container(
height: 100,
child: Row(children: <Widget>[
Text(
_barcodeResults,
style: TextStyle(fontSize: 14, color: Colors.white),
)
]),
),
Container(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
MaterialButton(
child: Text(_buttonText),
textColor: Colors.white,
color: Colors.blue,
onPressed: () async {
try {
// Ensure that the camera is initialized.
await _initializeControllerFuture;
videoScan();
// pictureScan();
} catch (e) {
// If an error occurs, log the error to the console.
print(e);
}
}),
MaterialButton(
child: Text("Picture Scan"),
textColor: Colors.white,
color: Colors.blue,
onPressed: () async {
pictureScan();
})
]),
),
]);
}
一个按钮触发视频流扫码,一个按钮触发拍照解码:
void videoScan() async {
if (!_isScanRunning) {
setState(() {
_buttonText = 'Stop Video Scan';
});
_isScanRunning = true;
await _controller.startImageStream((CameraImage availableImage) async {
assert(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
int format = FlutterBarcodeSdk.IF_UNKNOWN;
switch (availableImage.format.group) {
case ImageFormatGroup.yuv420:
format = FlutterBarcodeSdk.IF_YUV420;
break;
case ImageFormatGroup.bgra8888:
format = FlutterBarcodeSdk.IF_BRGA8888;
break;
default:
format = FlutterBarcodeSdk.IF_UNKNOWN;
}
if (!_isScanAvailable) {
return;
}
_isScanAvailable = false;
_barcodeReader
.decodeImageBuffer(
availableImage.planes[0].bytes,
availableImage.width,
availableImage.height,
availableImage.planes[0].bytesPerRow,
format)
.then((results) {
if (_isScanRunning) {
setState(() {
_barcodeResults = getBarcodeResults(results);
});
}
_isScanAvailable = true;
}).catchError((error) {
_isScanAvailable = false;
});
});
} else {
setState(() {
_buttonText = 'Start Video Scan';
_barcodeResults = '';
});
_isScanRunning = false;
await _controller.stopImageStream();
}
}
void pictureScan() async {
final image = await _controller.takePicture();
List<BarcodeResult> results = await _barcodeReader.decodeFile(image?.path);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DisplayPictureScreen(
imagePath: image?.path,
barcodeResults: getBarcodeResults(results)),
),
);
}
拍照显示部分可以参考官方文档。
现在可以运行测试程序了:
flutter run
视频模式
拍照模式
视频
https://www.bilibili.com/video/BV1uB4y1c7Ys