Flutter-车牌号识别插件(Android实现)

由于最近在做一个Flutter项目,需要使用到车牌识别的功能,并且要求识别功能使用本地的识别SDK。于是在网上找到一个原生的车牌识别的库。

原文地址:
https://www.jianshu.com/p/94784c3bf2c1

原项目Demo地址:
https://github.com/AleynP/LPR
感谢LPR作者提供SDK及Demo支持!

按照以上内容部署好原生项目代码,然后下面我们来提供控件的移植。主要分为以下几个步骤:
①.包装ScannerView;
②.编写ScannerView的FlutterPlugin(kotlin实现);
③.编写ScannerView的Dart部分实现;
④.测试用例-Demo.

  1. 移植第一步,使用xml包装ScannerView,如果不这样包装,可能会在Flutter中使用时报错“layout_height”相关的问题。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.boyou.materialmanager.core.widget.scanner.ScannerView
        android:id="@+id/scanner_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

对xml进行view的kotlin实现:

class PlateRecognitionView : ConstraintLayout {

    private var scannerView: ScannerView? = null

    constructor(context: Context) : super(context){
        init(context, null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs){
        init(context, attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){
        init(context, attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes){
        init(context, attrs)
    }

    private fun init(ctx: Context, attrs: AttributeSet?){
        View.inflate(ctx, R.layout.view_for_plate_recognition, this)
        scannerView = findViewById(R.id.scanner_view)
    }
  
    // 设置控制相机的生命周期的LifecycleOwner
    fun setLifeRecycle(lifecycleOwner: LifecycleOwner) = scannerView?.setLifeRecycle(lifecycleOwner)

    fun setScannerOptions(options: ScannerOptions, flashMode: Int) = scannerView?.setScannerOptions(options, flashMode)

    // 设置闪光灯效果
    fun setFlashMode(flashMode: Int) = scannerView?.setFlashMode(flashMode)

    // 设置识别结构的监听事件
    fun setOnScannerOCRListener(onResult: (String)->Unit) = scannerView?.setOnScannerOCRListener { onResult(it) }

    fun start() = scannerView?.start()

    fun release() = scannerView?.release()

}
  1. 创建Kotlin <---> Flutter 双向通讯的组件
    2.1. 实现PlatformView接口,编写相关的通讯方法等逻辑,如下:
class PlateRecognitionPlatformViewFactory(private val binaryMessenger: BinaryMessenger)
    : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context?, viewId: Int, args: Any?): PlatformView =
            PlateRecognitionPlatformView(context!!, viewId, binaryMessenger)

}

class PlateRecognitionPlatformView(private val ctx: Context, viewId: Int, binaryMessenger: BinaryMessenger)
    : PlatformView, MethodChannel.MethodCallHandler, EventChannel.StreamHandler, LifecycleOwner {

    private var lifecycle: LifecycleRegistry

    private var scannerView: PlateRecognitionView? = null

    private var methodChannel: MethodChannel? = null

    private var eventChannel: EventChannel? = null

    private var eventSink: EventChannel.EventSink? = null

    init {
        methodChannel = MethodChannel(binaryMessenger, "PlateRecognitionView$viewId-CN").apply {
            setMethodCallHandler(this@PlateRecognitionPlatformView)
        }
        eventChannel = EventChannel(binaryMessenger, "PlateRecognitionView$viewId-ET").apply {
            setStreamHandler(this@PlateRecognitionPlatformView)
        }
        lifecycle = LifecycleRegistry(this)
    }

    override fun getView(): View {
        if (scannerView == null)
            scannerView = PlateRecognitionView(ctx).apply {
                layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT)
                setLifeRecycle(this@PlateRecognitionPlatformView)
            }
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
        return scannerView!!
    }

    override fun onFlutterViewAttached(flutterView: View) {
        super.onFlutterViewAttached(flutterView)
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    override fun onFlutterViewDetached() {
        super.onFlutterViewDetached()
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
    }

    override fun dispose() {
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        if (eventChannel != null) {
            eventChannel?.setStreamHandler(null)
            eventChannel = null
        }
        if (methodChannel != null) {
            methodChannel?.setMethodCallHandler(null)
            methodChannel = null
        }
        if (scannerView != null)
            scannerView?.release()
        scannerView = null
    }

    override fun getLifecycle(): Lifecycle = lifecycle

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        this.eventSink = events
    }

    override fun onCancel(arguments: Any?) {
        this.eventSink = null
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "initSpotCamera" -> {
                initSpotCamera(call.argument<Int>("flashMode") ?: ImageCapture.FLASH_MODE_AUTO)
                result.success(null)
            }
            "setFlashMode" -> {
                setFlashMode(call.argument<Int>("flashMode") ?: ImageCapture.FLASH_MODE_OFF)
                result.success(null)
            }
            "restart" -> {
                scannerView?.start()
                result.success(null)
            }
            "resume" -> {
                resume()
                result.success(null)
            }
            "pause" -> {
                pause()
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }

    /** 初始化识别相机 **/
    private fun initSpotCamera(flashMode: Int) {
        scannerView?.apply {
            setScannerOptions(ScannerOptions.Builder()
                    .setTipText("请将识别车牌放入框内")
                    .setFrameCornerColor(-0xd93101)
                    .setLaserLineColor(-0xd93101)
                    .build(), flashMode)
            setOnScannerOCRListener { cardNum -> eventSink?.success(cardNum) }
        }
    }

    private fun resume(){
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    }

    private fun pause() {
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
    }

    private fun setFlashMode(flashMode: Int) = scannerView?.setFlashMode(flashMode)

}

2.2. 编写Flutter插件,实现FlutterPlugin接口

class PlateRecognitionViewPlugin : FlutterPlugin, ActivityAware {

    private var appContext: Context? = null

    private var activity: Activity? = null

    private lateinit var flutterBinding: FlutterPlugin.FlutterPluginBinding

    private var methodChannel: MethodChannel? = null

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        appContext = binding.applicationContext
        flutterBinding = binding
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        if (methodChannel != null) {
            methodChannel?.setMethodCallHandler(null)
            methodChannel = null
        }
    }

    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
        activity = binding.activity
        flutterBinding.platformViewRegistry.registerViewFactory("PlateRecognitionView-Plugin",
                PlateRecognitionPlatformViewFactory(flutterBinding.binaryMessenger))
    }

    override fun onDetachedFromActivityForConfigChanges() {
    }

    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    }

    override fun onDetachedFromActivity() {}

}
  1. 编写Flutter组件,实现与Native组件通讯

typedef void OnPlateCongnitionViewCreated();

/// 车牌识别控件视图
class PlateCongnitionView extends StatefulWidget {
  PlateCongnitionView({
    Key key,
    @required this.width,
    @required this.height,
    this.background,
    @required this.onViewCreated,
  }) : super(key: key);

  final double width;

  final double height;

  final Color background;

  final OnPlateCongnitionViewCreated onViewCreated;

  @override
  State<StatefulWidget> createState() => PlateConginitionViewState();
}

class PlateConginitionViewState extends State<PlateCongnitionView> {
  MethodChannel _methodChannel;

  EventChannel _eventChannel;

  bool _flashLightEnabled = false;

  /// 初始化操作
  /// [flashMode]-闪光灯模式
  void initSpotCamera(int flashMode) {
    _flashLightEnabled = flashMode == FLASH_MODE_ON;
    _methodChannel?.invokeMethod('initSpotCamera', {'flashMode': flashMode});
  }

  /// 获取闪光灯是否已打开
  bool get flashLightEnabled => _flashLightEnabled;

  /// 设置闪光灯是否已打开
  set flashLightEnabled(bool value) {
    _flashLightEnabled = value;
    _methodChannel?.invokeMethod(
        'setFlashMode', {'flashMode': value ? FLASH_MODE_ON : FLASH_MODE_OFF});
  }

  /// 重新识别
  void restart() => _methodChannel?.invokeMethod('restart');

  /// 唤醒
  void resume() {
    _methodChannel?.invokeMethod('resume');
    restart(); //重新调用识别功能
  }

  /// 暂停
  void pause() => _methodChannel?.invokeMethod('pause');

  /// 接收识别结果事件
  void onReceiveResult(void listen(String cardNum)) {
    _eventChannel
        ?.receiveBroadcastStream()
        ?.listen((data) => listen(data as String));
  }

  /// 视图创建成功事件
  void _onViewCreated(id) {
    _methodChannel = MethodChannel('PlateRecognitionView$id-CN');
    _eventChannel = EventChannel('PlateRecognitionView$id-ET');
    widget.onViewCreated();
  }

  @override
  Widget build(BuildContext context) => Container(
      width: widget.width,
      height: widget.height,
      color: widget.background,
      child: AndroidView(
          viewType: 'PlateRecognitionView-Plugin',
          creationParamsCodec: StandardMessageCodec(),
          onPlatformViewCreated: _onViewCreated));

  @override
  void dispose() {
    if (_methodChannel != null) _methodChannel = null;
    if (_eventChannel != null) _eventChannel = null;
    super.dispose();
  }
}

4.测试用例,注意控住相机生命周期


/// 闪光灯自动模式
const int FLASH_MODE_AUTO = 0;

/// 闪光灯打开
const int FLASH_MODE_ON = 1;

/// 闪光灯关闭
const int FLASH_MODE_OFF = 2;

///车牌识别页面
class PlateCongnitionPage extends StatefulWidget {
  PlateCongnitionPage({Key key}) : super(key: key);

  @override
  _PlateCongnitionPageState createState() => _PlateCongnitionPageState();
}

class _PlateCongnitionPageState extends State<PlateCongnitionPage>
    with WidgetsBindingObserver {
  /// 车牌视图View的Key
  final GlobalKey<PlateConginitionViewState> _plateCongnitionKey = GlobalKey();

  /// 闪光灯是否打开
  bool _isFlashLightEnabled = false;

  /// 是否显示了识别结果对话框
  bool _isShownPlateCongnitionResultDialog = false;

  /// 车牌识别状态View
  PlateConginitionViewState get _plateCongnitionState =>
      _plateCongnitionKey.currentState;

  /// 更改闪光灯状态
  void _onChangeFlashLightState(v) {
    setState(() => _isFlashLightEnabled = v);
    _plateCongnitionState.flashLightEnabled = v;
    AppConfigUtil().setIsFlashLightOn(v);
  }

  /// 车牌识别视图建立事件
  void _onPlateCongnitionViewCreated() async {
    bool isFlashLightEnabled = await AppConfigUtil().isFlashLightOn;
    _plateCongnitionState
      ..initSpotCamera(isFlashLightEnabled ? FLASH_MODE_ON : FLASH_MODE_OFF)
      ..onReceiveResult(_showPlateCongnitionResultDialog);
    setState(() => _isFlashLightEnabled = isFlashLightEnabled);
  }

  /// 显示车牌识别结果对话框
  ///
  /// [plateNum]-车牌号
  void _showPlateCongnitionResultDialog(String plateNum) async {
    if (!_isShownPlateCongnitionResultDialog) {
      _isShownPlateCongnitionResultDialog = true;
      _plateCongnitionState.pause();
      var result = await showDialog(
          context: context,
          useSafeArea: false,
          barrierDismissible: false,
          builder: (_) => PlateCongnitionResultPage(plateNum: plateNum));
      if (result != null && result is VehicleInfoEntity) {
        Navigator.pop(context, result);
        _isShownPlateCongnitionResultDialog = false;
        return;
      }
      _plateCongnitionState.resume();
      _isShownPlateCongnitionResultDialog = false;
    }
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  Widget build(BuildContext context) => WillPopScope(
      onWillPop: () async => Future.value(true),
      child: Scaffold(
          appBar: AppBar(
              title: Text('车牌识别'),
              backgroundColor: Colors.lightBlue,
              centerTitle: true),
          body: _buildContentView()));

  /// 构建主体视图控件
  Widget _buildContentView() => Stack(children: [
        PlateCongnitionView(
            key: _plateCongnitionKey,
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height -
                kToolbarHeight -
                MediaQuery.of(context).padding.top,
            onViewCreated: _onPlateCongnitionViewCreated),
        Positioned(
            left: 0,
            right: 0,
            bottom: 20,
            child: Container(
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                  Text('闪光灯',
                      style: TextStyle(fontSize: 12, color: Colors.white)),
                  CupertinoSwitch(
                      activeColor: Colors.lightBlue,
                      trackColor: Colors.grey,
                      value: _isFlashLightEnabled,
                      onChanged: _onChangeFlashLightState)
                ])))
      ]);

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      if (!_isShownPlateCongnitionResultDialog) {
        bool isFlashLightEnabled = await AppConfigUtil().isFlashLightOn;
        setState(() => _isFlashLightEnabled = isFlashLightEnabled);
        _plateCongnitionState?.resume();
        if (isFlashLightEnabled)
          _plateCongnitionState?.flashLightEnabled = true;
      }
    } else if (state == AppLifecycleState.inactive) {
      _plateCongnitionState?.pause();
    }
  }

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