Из бесплатных доступных библиотек для работы с qr кодами в Android самой лучшей (на мой личный взгляд) является zxing-android-embedded. Часто, UI который предоставляет эта библиотека не достаточно или нужен какой-то иной. В этой статье пойдет речь о том, как «кастомизировать» UI библиотеки zxing-android-embedded для распознавания QR кодов при использовать её Flutter проекте.
Представленная статья и код вместе с ней, всего лишь минимальный достаточный пример для демонстрации возможностей «кастомизации» zxing-android для работы с ней во flutter. Статья затрагивает только Android реализацию не касаясь IOS.
Мы будем использовать три основных компонента для взаимодействия с этой библиотекой из flutter окружения. Для этого нам потребуется:
-
PlatformViewLink
-
MethodChannel
-
EventChannel
PlatformViewLink:
Даёт возможность «прокинуть» нативный android экран(View) в ваше fluttter приложение. Это удобно в тех случаях когда есть готовое, проверенное решение под нативную платформу, а времени переделывать под flutter не хватает и легче показать android activity напрямую в вашем flutter приложении. По такому принципу работают google maps во flutter приложениях. В нашем случае через PlatformViewLink мы будем показывать нативный экран со стримом фотокамеры.
MethodChannel:
Даёт возможность вызывать нативные методы платформы(android или ios и т.д.) из flutter среды в и получать результат . Надо заметить что все вызовы методов асинхронны. В данном проекте MethodChannel будет использоваться чтобы включать и выключать подсветку фотокамеры.
EventChannel:
Почти то же самое что и MethodChannel, с тем лишь отличием, что мы можем подписаться на поток событий, генерируемых в нативной среде. Самый часты кейс это например «слушать» gps координаты от нативной платформы. В данном примере EventChannel будет использоваться для отправки распознанного QR кода из android окружения в наше flutter приложение. Конечно, для получения результата мы могли бы использовать MethodChannel, например самостоятельно запрашивая данные скажем каждые 10 секунд. Но такой подход выглядит не очень правильным в условиях, когда у нас есть возможность получить результат именно тогда когда он готов.
Создадим пустой flutter проект. В консоли терминала вышей любимой ОС выполним команду:
![](https://habrastorage.org/getpro/habr/upload_files/1cd/04a/3e0/1cd04a3e052bbbbda1d086e154aa8c74.png)
Откроем main.dart. Добавим в качестве home элемента MaterialApp виджета, QrCodePage — виджет, который обернёт основной экран в Scaffold и добавит AppBar для него:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: QrCodePage(), ); } } class QrCodePage extends StatelessWidget { QrCodePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("QR code App"), ), body: PlatformView(), ); } }
PlatformView — основной StatefulWidge виджет. Он будет обёрткой над PlatformViewLink и будет непосредственно транслировать видео с камеры:
class _PlatformViewState extends State<PlatformView> { final MethodChannel platformMethodChannel = MethodChannel('flashlight'); bool isFlashOn = false; bool permissionIsGranted = false; String result = ''; void _handleQRcodeResult() { const EventChannel _stream = EventChannel('qrcodeResultStream'); _stream.receiveBroadcastStream().listen((onData) { print('EventChannel onData = $onData'); result = onData; setState(() {}); }); } Future<void> _onFlash() async { try { dynamic result = await platformMethodChannel.invokeMethod('onFlash'); setState(() { isFlashOn = true; }); } on PlatformException catch (e) { debugPrint('PlatformException ${e.message}'); } } Future<void> _offFlash() async { try { dynamic result = await platformMethodChannel.invokeMethod('offFlash'); setState(() { isFlashOn = false; }); } on PlatformException catch (e) { debugPrint('PlatformException ${e.message}'); } } @override void initState() { super.initState(); _handleQRcodeResult(); _checkPermissions(); } _requestAppPermissions() { showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: const Text('Permission required'), content: const Text('Allow camera permissions'), actions: <Widget>[ TextButton( onPressed: () { _checkPermissions(); Navigator.pop(context, 'OK'); }, child: const Text('OK'), ), ], )); } _checkPermissions() async { var status = await Permission.camera.status; if (!status.isGranted) { final PermissionStatus permissionStatus = await Permission.camera.request(); if (!permissionStatus.isGranted) { _requestAppPermissions(); } } } @override Widget build(BuildContext context) { final String viewType = '<platform-view-type>'; final Map<String, dynamic> creationParams = <String, dynamic>{}; return result.isEmpty ? Stack( alignment: Alignment.center, children: [ PlatformViewLink( viewType: viewType, surfaceFactory: (BuildContext context, PlatformViewController controller) { return Container( child: AndroidViewSurface( controller: controller as AndroidViewController, gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{}, hitTestBehavior: PlatformViewHitTestBehavior.opaque, ), ); }, onCreatePlatformView: (PlatformViewCreationParams params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: StandardMessageCodec(), ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); }, ), Align( alignment: Alignment.topCenter, child: ElevatedButton( onPressed: () { if (!isFlashOn) { _onFlash(); } else { _offFlash(); } }, child: isFlashOn ? Text('off flashlight') : Text('on flashlight'))), Align( alignment: Alignment.center, child: Container( height: 200, width: 200, decoration: BoxDecoration( color: Colors.transparent, border: Border.all( color: Colors.blueAccent, width: 5, )), ), ) ], ) : Container( child: Center(child: Text('QR code result:\n$result')), ); } }
Пару комментариев к коду ваше:
-
Запрашиваем разрешения на работу с камерой:
-
В поле класса, создаём platformMethodChannel — через этот экземпляр будем вызывать нативные методы (которые мы создадим чуть позже) в android окружении. Аргумент в конструкторе ‘flashlight’ это своего рода уникальный ID, который должен быть идентичный во flutter и нативной среде:
-
Метод _handleQRcodeResult() — будет получать результат отсканированного qr кода:
-
Методы _onFlash() и _offFlash() вызывают соответствующий метод на стороне Android фреймворка.
-
В некоторых случаях необходимо передать параметры в нативную среду. Для этого удобно использовать creationParams. Но в нашем примере параметров для передачи у нас не будет:
![](https://habrastorage.org/getpro/habr/upload_files/fda/94d/0cb/fda94d0cbc4532f7ee649e3ca64d3e2e.png)
-
В качестве ViewGroup используем Stack для того чтоб расположить дополнительные UI элементы. В моём примере это рамка в центр экрана(Container с прозрачным фоном и BoxDecoration) и ElevatedButton над ней для включения подсветки.
Взглянем на Android реализацию:
В build.gradle модуля app (android/app/build.gradle) подключим библиотеку. В раздел dependencies добавим:
![](https://habrastorage.org/getpro/habr/upload_files/98b/404/5dd/98b4045dd894f86a481f1896e2f64189.png)
В MainActivity, в методе configureFlutterEngine, EventChannel
class MainActivity : FlutterFragmentActivity(), LifecycleOwner, ResultCallback { var myEvents: EventChannel.EventSink? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) EventChannel(flutterEngine.dartExecutor.binaryMessenger, "qrcodeResultStream") .setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { myEvents = events } override fun onCancel(arguments: Any?) { myEvents = null } }) flutterEngine .platformViewsController .registry .registerViewFactory("<platform-view-type>", NativeViewFactory()) } override fun result(result: String) { myEvents?.success(result) } override fun getMyFlutterEngine(): FlutterEngine? = flutterEngine }
EventChannel.StreamHandler возвращает нам объект EventChannel.EventSink вызывая на котором .success(result) — мы передаём событие во flutter фреймворк. В нашем случае это будет строка с QR кодом.
![](https://habrastorage.org/getpro/habr/upload_files/e4c/846/2ad/e4c8462ad363a8c3e6e34ca8e9e67d5a.png)
В методе выше мы регистрируем фабрику которая может возвращать разные View в зависимости от переданных аргументов, но мы не будем усложнять пример и возвращаем наш единственное NativeView:
![](https://habrastorage.org/getpro/habr/upload_files/ef3/17b/b27/ef317bb276bc169341fbf3ba08f6b3fb.png)
Взглянем на интерфейс ResultCallback, который имплементирует MainActivity:
![](https://habrastorage.org/getpro/habr/upload_files/492/4c3/306/4924c3306ccb7a705633e5c7585ce430.png)
Метод result(result: String) нужен для передачи результата (распознанного qr кода) в MainActivity.
метод getMyFlutterEngine() — вернёт нам FlutterEngine в нашем NativeView.
Основной код будет в NativeView:
class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView { private val textView: TextView private val CHANNEL = "flashlight" private val rootView: View private var barcodeView: DecoratedBarcodeView? = null override fun getView(): View { return rootView } override fun dispose() {} init { (context as LifecycleOwner).lifecycle.addObserver(object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun connectListener() { barcodeView?.resume() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun disconnectListener() { barcodeView?.pause() } }) rootView = LayoutInflater.from(context.applicationContext).inflate(R.layout.layout, null) barcodeView = rootView.findViewById<DecoratedBarcodeView>(R.id.barcode_scanner) val formats: Collection<BarcodeFormat> = Arrays.asList(BarcodeFormat.QR_CODE, BarcodeFormat.CODE_39) barcodeView?.barcodeView?.decoderFactory = DefaultDecoderFactory(formats) barcodeView?.setStatusText("") barcodeView?.viewFinder?.visibility = View.INVISIBLE barcodeView?.initializeFromIntent(Intent()) barcodeView?.decodeContinuous(object : BarcodeCallback { override fun possibleResultPoints(resultPoints: MutableList<ResultPoint>?) { super.possibleResultPoints(resultPoints) } override fun barcodeResult(result: BarcodeResult?) { (context as ResultCallback).result(result?.result?.text ?: "no result") barcodeView?.setStatusText(result?.text) } }) barcodeView?.resume() textView = TextView(context) textView.textSize = 36f textView.setBackgroundColor(Color.rgb(255, 255, 255)) textView.text = "Rendered on a native Android view (id: $id) ${creationParams?.entries}" val flutterEngine = (context as ResultCallback).getMyFlutterEngine() MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> when (call.method) { "onFlash" -> { barcodeView?.setTorchOn() result.success("setTorchOn") } "offFlash" -> { barcodeView?.setTorchOff() result.success("setTorchOff") } else -> { result.notImplemented() } } } } }
В init блоке подписываемся на жизненный цикл activiti и в соответствующих методах вызываем resume / pause у barcodeView. Важно: что без реализации этих методов вы вместо видеопотока с камеры будет увидите черный экран:
![](https://habrastorage.org/getpro/habr/upload_files/038/b4f/e9e/038b4fe9eeecaa0dbc75b6a5cc24c352.png)
NativeView наследуется от интерфейса PlatformView это обязывает нас реализовать два метода:
![](https://habrastorage.org/getpro/habr/upload_files/b05/292/056/b052920563403f662bd3388c99092494.png)
В getView мы должны вернуть View которая является главным экраном. Нужно создать layout.xml с следующего содержания:
![](https://habrastorage.org/getpro/habr/upload_files/fef/fc1/866/feffc1866d31013ddd61fc67a1d06b1f.png)
Из него с помощью LayoutInflater мы создаём view и возвращаем ссылку на него в методе getView():
![](https://habrastorage.org/getpro/habr/upload_files/978/fcd/7d5/978fcd7d50ea551de06fd0019922bec2.png)
Поскольку наш layout содержит DecoratedBarcodeView мы можем найти его(получить ссылку на него) с помощью findViewById и настроить как на нужно:
![](https://habrastorage.org/getpro/habr/upload_files/34b/1a8/45c/34b1a845c0ca1cd1ea80427b37c38978.png)
Тут мы устанавливаем поддерживаемый формат qr кодов, дефолтную строку результата «сеттим» как пустую, убираем стандартную рамку в центре экрана. Отдельно стоит остановиться на этом куске кода:
![](https://habrastorage.org/getpro/habr/upload_files/4f3/086/243/4f3086243762854a573f99277c17c333.png)
Когда библиотека распознаёт qr код, результат этого она передаёт в callback — barcodeResult(result: BarcodeResult?). В нем имея ссылку на MainActivity через общий контекст, вызываем метод result нашего ResultCallback и через него передаём строку с результатом. И уже в самом MainActivity используя EventChannel передаём дальше — во Flutter окружение.
![](https://habrastorage.org/getpro/habr/upload_files/81f/e6d/eb5/81fe6deb59f9b8eaee61938340f1184e.png)
Код выше является обработчиком событий отправляемых из flutter среды. У MethodChannel принимает MethodCallHandler используя который мы узнаём какой метод сейчас вызывается и реагируем на него. В данном коде мы включаем или выключаем подсветку камеры.
Короткое видео с примером этого приложения:
ссылка на оригинал статьи https://habr.com/ru/articles/592129/
Добавить комментарий