如何实现支持摄像头二维码扫描的Flutter自定义Widget

如何实现支持摄像头二维码扫描的Flutter自定义Widget,第1张

如何实现支持摄像头二维码扫描的Flutter自定义Widget

前些日子写了一个flutter_barcode_sdk,配合已经存在的Flutter camera插件可以实现摄像头扫码 - 从Java层获取摄像头数据,通过Dart传递,再到Java去做解码。这个过程可以优化。这篇文章的目的是把摄像头控制和二维码扫描合并到Android代码里,然后封装出一整个Flutter插件。

基于Android TextView的Flutter自定义Widget

根据官方文档,我们先做一个最简单的自定义widget。

  1. 创建插件工程:

    flutter create --org com.dynamsoft --template=plugin --platforms=android -a java flutter_qrcode_scanner
    
  2. 打开lib/flutter_qrcode_scanner.dart文件,把自动生成的代码替换成:

     class ScannerView extends StatefulWidget {
       const ScannerView({Key? key}) : super(key: key);
       @override
       State createState() => _ScannerViewState();
     }
        
     class _ScannerViewState extends State {
       @override
       void initState() {
         super.initState();
       }
        
       @override
       Widget build(BuildContext context) {
         const String viewType = 'com.dynamsoft.flutter_qrcode_scanner/nativeview';
         final Map creationParams = {};
        
         return AndroidView(
           viewType: viewType,
           creationParams: creationParams,
           creationParamsCodec: const StandardMessageCodec(),
         );
       }
     }    
    
  3. 在Android目录下创建一个NativeView.java文件。创建一个TextView,并在getView()中返回:

    import android.content.Context;
     import android.graphics.Color;
     import android.view.View;
     import android.widget.TextView;
     import androidx.annotation.NonNull;
     import androidx.annotation.Nullable;
     import io.flutter.plugin.platform.PlatformView;
     import java.util.Map;
        
     class NativeView implements PlatformView {
         @NonNull private final TextView textView;
         
          NativeView(@NonNull Context context, int id, @Nullable Map creationParams) {
              textView = new TextView(context);
              textView.setTextSize(72);
              textView.setBackgroundColor(Color.rgb(255, 255, 255));
              textView.setText("Rendered on a native Android view (id: " + id + ")");
          }
         
          @NonNull
          @Override
          public View getView() {
              return textView;
          }
         
          @Override
          public void dispose() {}
     }
    

    接下来创建NativeViewFactory.java,用于初始化NativeView:

     import android.content.Context;
     import android.view.View;
     import androidx.annotation.Nullable;
     import androidx.annotation.NonNull;
     import io.flutter.plugin.common.BinaryMessenger;
     import io.flutter.plugin.common.StandardMessageCodec;
     import io.flutter.plugin.platform.PlatformView;
     import io.flutter.plugin.platform.PlatformViewFactory;
     import java.util.Map;
    
     class NativeViewFactory extends PlatformViewFactory {
       @NonNull private final BinaryMessenger messenger;
    
       NativeViewFactory(@NonNull BinaryMessenger messenger) {
         super(StandardMessageCodec.INSTANCE);
         this.messenger = messenger;
       }
    
       @NonNull
       @Override
       public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
         final Map creationParams = (Map) args;
         return new NativeView(context, id, creationParams);
       }
     }
    

    在自动生成的FlutterQrcodeScannerPlugin.java中,注册NativeViewFactory:

    @Override
     public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
       channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_qrcode_scanner");
       channel.setMethodCallHandler(this);
       flutterPluginBinding.getPlatformViewRegistry().registerViewFactory(
           "com.dynamsoft.flutter_qrcode_scanner/nativeview",
           new NativeViewFactory(flutterPluginBinding.getBinaryMessenger()));
     }
    

这样一个最简单的自定义Flutter Text Widget就完成了。我们可以修改example/lib/main.dart,然后运行看下效果:

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_qrcode_scanner/flutter_qrcode_scanner.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State createState() => _MyAppState();
}

class _MyAppState extends State {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: ScannerView(),
        ),
      ),
    );
  }
}
flutter run

把Flutter Text Widget改成Flutter Camera Widget

如果以上Widget没有问题,我们开始改造。

首先在android/build.gradle中添加:

 rootProject.allprojects {
     repositories {
         google()
         mavenCentral()
         maven {
             url "https://download.dynamsoft.com/maven/dce/aar"
         }
     }
 }
    
 android {
     compileSdkVersion 30

     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }

     defaultConfig {
         minSdkVersion 21
     }
 }

 dependencies {
     implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.0.0@aar'
 }

这里用到现成封装好的Camera SDK,你也可以用别的,比如Google官方的CameraX。

在NativeView.java中导入:

import com.dynamsoft.dce.*;

用DCECameraView替换TextView。同时,使用CameraEnhancer来加载DCECameraView:

private final DCECameraView cameraView;
 private CameraEnhancer cameraEnhancer;

 NativeView(BinaryMessenger messenger, @NonNull Activity context, int id,
         @Nullable Map creationParams) {
     this.context = context;

     cameraEnhancer = new CameraEnhancer(context);
     cameraView = new DCECameraView(context);
     cameraEnhancer.setCameraView(cameraView);
     try {
         cameraEnhancer.open();
     } catch (Exception e) {
         e.printStackTrace();
     }
 }

为了切换摄像头的状态,我们需要监控应用的生命周期:

class NativeView implements PlatformView, Application.ActivityLifecycleCallbacks {
     @Override
     public void onActivityResumed(Activity activity) {
         try {
             cameraEnhancer.open();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }

     @Override
     public void onActivityPaused(Activity activity) {
         try {
             cameraEnhancer.close();
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 }

在NativeViewFactory.java中,我们把传入的Context换成Activity:

class NativeViewFactory extends PlatformViewFactory {
   @NonNull private final BinaryMessenger messenger;
   @NonNull private Activity activity;

   NativeViewFactory(@NonNull BinaryMessenger messenger, Activity activity) {
     super(StandardMessageCodec.INSTANCE);
     this.messenger = messenger;
     this.activity = activity;
   }

   @NonNull
   @Override
   public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
     final Map creationParams = (Map) args;
     return new NativeView(messenger, activity, id, creationParams);
   }

最后修改FlutterQrcodeScannerPlugin:

public class FlutterQrcodeScannerPlugin implements FlutterPlugin, ActivityAware {
     private Activity activity;
     private FlutterPluginBinding flutterPluginBinding;

     @Override
     public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
       this.flutterPluginBinding = flutterPluginBinding;
     }

     @Override
     public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
       this.flutterPluginBinding = null;
     }

     @Override
     public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
       bind(activityPluginBinding);
     }

     @Override
     public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
       bind(activityPluginBinding);
     }

     @Override
     public void onDetachedFromActivityForConfigChanges() {
       activity = null;
     }

     @Override
     public void onDetachedFromActivity() {
       activity = null;
     }

     private void bind(ActivityPluginBinding activityPluginBinding) {
       activity = activityPluginBinding.getActivity();
       flutterPluginBinding.getPlatformViewRegistry().registerViewFactory(
           "com.dynamsoft.flutter_qrcode_scanner/nativeview",
           new NativeViewFactory(flutterPluginBinding.getBinaryMessenger(), activity));
     }
 }

现在重新运行示例代码,之前的Text View应该变成了Camera View。

集成barcode SDK,实现支持摄像头二维码扫描的Flutter插件

摄像头Widget做好了,接下来就是增加二维码识别功能。这里要进一步对Java和Dart的代码进行改造。

第一步还是修改build.gradle文件,添加barcode SDK:

rootProject.allprojects {
     repositories {
         google()
         mavenCentral()
         maven {
             url "https://download.dynamsoft.com/maven/dce/aar"
         }
         maven {
             url "https://download.dynamsoft.com/maven/dbr/aar"
         }
     }
 }
    
 android {
     compileSdkVersion 30

     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }

     defaultConfig {
         minSdkVersion 21
     }
 }

创建QRCideScanner.java文件来实现摄像头和二维码识别的逻辑:

public class QRCodeScanner {
     private CameraEnhancer mCameraEnhancer;
     private BarcodeReader reader;
     private Activity context;
     private DCECameraView cameraView;
     private DetectionHandler handler;

     public interface DetectionHandler {
         public void onDetected(List> data);
     }

     public QRCodeScanner(Activity context, DCECameraView cameraView) {
         this.context = context;
         this.cameraView = cameraView;
         mCameraEnhancer = new CameraEnhancer(context);
         mCameraEnhancer.setCameraView(cameraView);
         cameraView.setOverlayVisible(true);

         DCESettingParameters dceSettingParameters = new DCESettingParameters();
         dceSettingParameters.cameraInstance = mCameraEnhancer;
         dceSettingParameters.textResultCallback = mTextResultCallback;

         try {
             // mCameraEnhancer.open();
             reader = new BarcodeReader();
             reader.SetCameraEnhancerParam(dceSettingParameters);
             PublicRuntimeSettings settings = reader.getRuntimeSettings();
             settings.barcodeFormatIds = EnumBarcodeFormat.BF_QR_CODE;
             reader.updateRuntimeSettings(settings);
         } catch (Exception e) {
             // TODO: handle exception
         }
     }

     TextResultCallback mTextResultCallback = new TextResultCallback() {
         @Override
         public void textResultCallback(int i, TextResult[] results, Object userData) {
             if (results != null) {
                 final List> ret = new ArrayList>();
                 for (TextResult result: results) {
                     final Map data = new HashMap<>();
                     data.put("format", result.barcodeFormatString);
                     data.put("text", result.barcodeText);
                     data.put("x1", result.localizationResult.resultPoints[0].x);
                     data.put("y1", result.localizationResult.resultPoints[0].y);
                     data.put("x2", result.localizationResult.resultPoints[1].x);
                     data.put("y2", result.localizationResult.resultPoints[1].y);
                     data.put("x3", result.localizationResult.resultPoints[2].x);
                     data.put("y3", result.localizationResult.resultPoints[2].y);
                     data.put("x4", result.localizationResult.resultPoints[3].x);
                     data.put("y4", result.localizationResult.resultPoints[3].y);
                     data.put("angle", result.localizationResult.angle);
                     ret.add(data);
                 }

                 if (handler != null) {
                     handler.onDetected(ret);
                 }
             }
         }
     };

     public void setDetectionHandler(DetectionHandler handler) {
         this.handler = handler;
     }

     public void startScan() {
         try {
             // mCameraEnhancer.open();
             cameraView.setOverlayVisible(true);
             reader.StartCameraEnhancer();
         } catch (Exception e) {
             // TODO: handle exception
         }
     }

     public void stopScan() {
         try {
             // mCameraEnhancer.close();
             cameraView.setOverlayVisible(false);
             reader.StopCameraEnhancer();
         } catch (Exception e) {
             // TODO: handle exception
         }
     }

     public void setLicense(String license) {
         try {
             reader.initLicense(license);
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 }

在NativeView中,我们增加MethodChannel来触发接口调用:

class NativeView implements PlatformView, MethodCallHandler, Application.ActivityLifecycleCallbacks, QRCodeScanner.DetectionHandler {
     @Override
     public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
         switch (call.method) {
         case "startScanning":
             qrCodeScanner.startScan();
             result.success(null);
             break;
         case "stopScanning":
             qrCodeScanner.stopScan();
             result.success(null);
             break;
         case "setLicense":
             final String license = call.argument("license");
             qrCodeScanner.setLicense(license);
             result.success(null);
             break;
         case "setBarcodeFormats":
             final int formats = call.argument("formats");
             qrCodeScanner.setBarcodeFormats(formats);
             result.success(null);
             break;
         default:
             result.notImplemented();
         }
     }
 }

二维码识别回调函数是在工作线程中执行的,而结果回传需要在主线程中做,所以使用runOnUiThread:

@Override
 public void onDetected(List> data) {
     context.runOnUiThread(new Runnable() {
         @Override
         public void run() {
             methodChannel.invokeMethod("onDetected", data);
         }
     });
 }

Dart这边增加一个ScannerViewController来接收返回结果:

 class _ScannerViewState extends State {
   ScannerViewController? _controller;

   @override
   void initState() {
     super.initState();
   }

   @override
   Widget build(BuildContext context) {
     const String viewType = 'com.dynamsoft.flutter_qrcode_scanner/nativeview';
     final Map creationParams = {};

     return AndroidView(
       viewType: viewType,
       onPlatformViewCreated: _onPlatformViewCreated,
       creationParams: creationParams,
       creationParamsCodec: const StandardMessageCodec(),
     );
   }

   void _onPlatformViewCreated(int id) {
     _controller = ScannerViewController(id);
     widget.onScannerViewCreated(_controller!);
   }
 }

 class ScannerViewController {
   late MethodChannel _channel;
   final StreamController> _streamController =
       StreamController>();
   Stream> get scannedDataStream => _streamController.stream;

   ScannerViewController(int id) {
     _channel =
         MethodChannel('com.dynamsoft.flutter_qrcode_scanner/nativeview_$id');
     _channel.setMethodCallHandler((call) async {
       switch (call.method) {
         case 'onDetected':
           if (call.arguments != null) {
             List data = fromPlatformData(call.arguments as List);
             _streamController.sink.add(data);
           }
           break;
       }
     });
   }
 }

此外,增加Java层的调用函数:

Future startScanning() async {
   await _channel.invokeMethod('startScanning');
 }

 Future stopScanning() async {
   await _channel.invokeMethod('stopScanning');
 }

 /// Apply for a 30-day FREE trial license: https://www.dynamsoft.com/customer/license/trialLicense
 Future setLicense(String license) async {
   await _channel.invokeMethod('setLicense', {'license': license});
 }

到此,整个插件完成了。我们修改示例代码,并在build.gradle中把最小Android版本设置成21:

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_camera_qrcode_scanner/flutter_qrcode_scanner.dart';
import 'package:flutter_camera_qrcode_scanner/dynamsoft_barcode.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State createState() => _MyAppState();
}

class _MyAppState extends State {
  ScannerViewController? controller;
  String _barcodeResults = '';

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('QR Code Scanner'),
        ),
        body: Stack(children: [
          ScannerView(onScannerViewCreated: onScannerViewCreated),
          Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Container(
                height: 100,
                child: SingleChildScrollView(
                  child: Text(
                    _barcodeResults,
                    style: TextStyle(fontSize: 14, color: Colors.white),
                  ),
                ),
              ),
              Container(
                height: 100,
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      MaterialButton(
                          child: Text('Start Scan'),
                          textColor: Colors.white,
                          color: Colors.blue,
                          onPressed: () async {
                            controller!.startScanning();
                          }),
                      MaterialButton(
                          child: Text("Stop Scan"),
                          textColor: Colors.white,
                          color: Colors.blue,
                          onPressed: () async {
                            controller!.stopScanning();
                          })
                    ]),
              ),
            ],
          )
        ]),
      ),
    );
  }

  void onScannerViewCreated(ScannerViewController controller) {
    setState(() {
      this.controller = controller;
    });
    controller.setLicense('LICENSE-KEY');
    controller.startScanning(); // auto start scanning
    controller.scannedDataStream.listen((results) {
      setState(() {
        _barcodeResults = getBarcodeResults(results);
      });
    });
  }

  String getBarcodeResults(List results) {
    StringBuffer sb = new StringBuffer();
    for (BarcodeResult result in results) {
      sb.write(result.format);
      sb.write("n");
      sb.write(result.text);
      sb.write("nn");
    }
    if (results.isEmpty) sb.write("No QR Code Detected");
    return sb.toString();
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}


在使用这个插件的时候,Dart层要做的事情就是接收识别结果,并通过UI绘制出来。

pub.dev下载插件

flutter_camera_qrcode_scanner

源码

https://github.com/yushulx/flutter_qrcode_scanner

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5082421.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-16
下一篇 2022-11-16

发表评论

登录后才能评论

评论列表(0条)

保存