Flutter-車牌號識別插件(Android實現)

由于最近在做一個Flutter項目框仔,需要使用到車牌識別的功能洼裤,并且要求識別功能使用本地的識別SDK殖蚕。于是在網上找到一個原生的車牌識別的庫冒滩。

原文地址:
http://www.reibang.com/p/94784c3bf2c1

原項目Demo地址:
https://github.com/AleynP/LPR
感謝LPR作者提供SDK及Demo支持微驶!

按照以上內容部署好原生項目代碼,然后下面我們來提供控件的移植开睡。主要分為以下幾個步驟:
①.包裝ScannerView;
②.編寫ScannerView的FlutterPlugin(kotlin實現);
③.編寫ScannerView的Dart部分實現因苹;
④.測試用例-Demo.

  1. 移植第一步,使用xml包裝ScannerView篇恒,如果不這樣包裝扶檐,可能會在Flutter中使用時報錯“l(fā)ayout_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)

    // 設置識別結構的監(jiān)聽事件
    fun setOnScannerOCRListener(onResult: (String)->Unit) = scannerView?.setOnScannerOCRListener { onResult(it) }

    fun start() = scannerView?.start()

    fun release() = scannerView?.release()

}
  1. 創(chuàng)建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));
  }

  /// 視圖創(chuàng)建成功事件
  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;

  /// 車牌識別狀態(tài)View
  PlateConginitionViewState get _plateCongnitionState =>
      _plateCongnitionKey.currentState;

  /// 更改閃光燈狀態(tài)
  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閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件于宙,死亡現場離奇詭異浮驳,居然都是意外死亡,警方通過查閱死者的電腦和手機捞魁,發(fā)現死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門抹恳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人署驻,你說我怎么就攤上這事奋献。” “怎么了旺上?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵瓶蚂,是天一觀的道長。 經常有香客問我宣吱,道長窃这,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任征候,我火速辦了婚禮杭攻,結果婚禮上,老公的妹妹穿的比我還像新娘疤坝。我一直安慰自己兆解,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布跑揉。 她就那樣靜靜地躺著锅睛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪历谍。 梳的紋絲不亂的頭發(fā)上现拒,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音望侈,去河邊找鬼印蔬。 笑死,一個胖子當著我的面吹牛脱衙,可吹牛的內容都是我干的侥猬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼岂丘,長吁一口氣:“原來是場噩夢啊……” “哼陵究!你這毒婦竟也來了眠饮?” 一聲冷哼從身側響起奥帘,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仪召,沒想到半個月后寨蹋,有當地人在樹林里發(fā)現了一具尸體松蒜,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年已旧,在試婚紗的時候發(fā)現自己被綠了秸苗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡运褪,死狀恐怖惊楼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情秸讹,我是刑警寧澤檀咙,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站璃诀,受9級特大地震影響弧可,放射性物質發(fā)生泄漏。R本人自食惡果不足惜劣欢,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一棕诵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凿将,春花似錦校套、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至灭忠,卻和暖如春膳算,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弛作。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工涕蜂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人映琳。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓机隙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親萨西。 傳聞我的和親對象是個殘疾皇子有鹿,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344