Files
ux/lib/src/scanner.dart
agra d68a2978eb ux: bulk WIP — UxPlugin→XPlugin rename + new anim/core/navi/reactive packages
Catch-all commit for outstanding pre-existing local changes. Mixes
several themes that would normally be split:

- Rename: UxPlugin → XPlugin across iOS, macOS, Android registrants.
- New top-level packages under lib/src/: anim/ (animated values,
  panes, sheets, dock, measured), core/ (Emitter, ReactiveBuilder
  scaffolding, presenter/widget/value/dispose primitives), navi/
  (Screen/ScreenStack/Router/hero/transitions), reactive/.
- Edits across existing plugins (clipboard, crash, file, gallery,
  keyboard, scanner, sensor, url) to align with the new core.
- Test updates and CHANGELOG/README touches accompanying the above.
2026-05-21 08:58:07 +03:00

117 lines
3.7 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Barcode formats the platform decoder will look for. Today only QR is
/// supported; the enum exists for forward-compatibility with the native
/// channel arguments.
enum BarcodeFormat { qr }
/// Static helpers exposed by the platform-side scanner plugin.
class XScannerPermission {
XScannerPermission._();
static const _channel = MethodChannel('ux/scanner');
/// Requests camera permission. Returns `true` once the OS has reported
/// authorized; `false` if the user denied or the platform is
/// unsupported. Safe to call multiple times — already-granted returns
/// `true` immediately.
static Future<bool> requestCamera() async {
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) {
return false;
}
final granted = await _channel.invokeMethod<bool>('requestPermission');
return granted ?? false;
}
}
/// Camera-preview widget that emits decoded barcode payloads.
///
/// Backed by AVFoundation on iOS (built-in QR support, no extra dep) and
/// CameraX + ZXing on Android. The widget mounts a platform-view —
/// `UiKitView` on iOS, `AndroidView` on Android — and listens to a
/// per-process event channel for decoded strings.
///
/// Camera permission must be granted by the host app before mounting.
/// On platforms other than iOS / Android the widget renders an empty
/// box.
class XScanner extends StatefulWidget {
const XScanner({
super.key,
required this.onCode,
this.formats = const [BarcodeFormat.qr],
});
final ValueChanged<String> onCode;
final List<BarcodeFormat> formats;
@override
State<XScanner> createState() => _XScannerState();
}
class _XScannerState extends State<XScanner> {
static const _events = EventChannel('ux/scanner/events');
StreamSubscription<dynamic>? _sub;
@override
void initState() {
super.initState();
_sub = _events.receiveBroadcastStream().listen((event) {
final code = event as String?;
if (code == null || !mounted) return;
widget.onCode(code);
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final creationParams = <String, Object?>{
'formats': widget.formats.map((e) => e.name).toList(),
};
if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'ux/scanner',
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
if (defaultTargetPlatform == TargetPlatform.android) {
return PlatformViewLink(
viewType: 'ux/scanner',
surfaceFactory: (context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: 'ux/scanner',
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () => params.onFocusChanged(true),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
return const SizedBox.shrink();
}
}