Из бесплатных доступных библиотек для работы с 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 проект. В консоли терминала вышей любимой ОС выполним команду:
Откроем 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. Но в нашем примере параметров для передачи у нас не будет:
В качестве ViewGroup используем Stack для того чтоб расположить дополнительные UI элементы. В моём примере это рамка в центр экрана(Container с прозрачным фоном и BoxDecoration) и ElevatedButton над ней для включения подсветки.
Взглянем на Android реализацию:
В build.gradle модуля app (android/app/build.gradle) подключим библиотеку. В раздел dependencies добавим:
В 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 кодом.
В методе выше мы регистрируем фабрику которая может возвращать разные View в зависимости от переданных аргументов, но мы не будем усложнять пример и возвращаем наш единственное NativeView:
Взглянем на интерфейс ResultCallback, который имплементирует MainActivity:
Метод 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. Важно: что без реализации этих методов вы вместо видеопотока с камеры будет увидите черный экран:
NativeView наследуется от интерфейса PlatformView это обязывает нас реализовать два метода:
В getView мы должны вернуть View которая является главным экраном. Нужно создать layout.xml с следующего содержания:
Из него с помощью LayoutInflater мы создаём view и возвращаем ссылку на него в методе getView():
Поскольку наш layout содержит DecoratedBarcodeView мы можем найти его(получить ссылку на него) с помощью findViewById и настроить как на нужно:
Тут мы устанавливаем поддерживаемый формат qr кодов, дефолтную строку результата «сеттим» как пустую, убираем стандартную рамку в центре экрана. Отдельно стоит остановиться на этом куске кода:
Когда библиотека распознаёт qr код, результат этого она передаёт в callback - barcodeResult(result: BarcodeResult?). В нем имея ссылку на MainActivity через общий контекст, вызываем метод result нашего ResultCallback и через него передаём строку с результатом. И уже в самом MainActivity используя EventChannel передаём дальше — во Flutter окружение.
Код выше является обработчиком событий отправляемых из flutter среды. У MethodChannel принимает MethodCallHandler используя который мы узнаём какой метод сейчас вызывается и реагируем на него. В данном коде мы включаем или выключаем подсветку камеры.
Короткое видео с примером этого приложения:
Исходный код приложения
zxing-android-embedded