scanner
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
### 0.9.0
|
||||
- `UxScanner`: new platform-view widget for QR-code (and forward-compat
|
||||
barcode) scanning. iOS uses `AVCaptureMetadataOutput` (no extra pod);
|
||||
Android uses CameraX preview + ZXing decoder
|
||||
(`com.google.zxing:core:3.5.3`, ~470 KB jar, no Play Services dep).
|
||||
`UxScannerPermission.requestCamera()` requests OS permission first;
|
||||
decoded payloads stream through `EventChannel('ux/scanner/events')`.
|
||||
|
||||
### 0.8.0
|
||||
- `Log`: pretty, production-ready logger. Static entry (`Log.d/i/w/e/f/t`),
|
||||
scoped loggers via `Log.tag('KB')`, lazy messages (`Log.d(() => expensive)`),
|
||||
|
||||
@@ -52,3 +52,15 @@ android {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// CameraX for scanner preview + frame analysis. Pinned to a stable
|
||||
// train; the `view` module pulls in the rest transitively.
|
||||
def cameraxVersion = '1.3.4'
|
||||
implementation "androidx.camera:camera-core:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-camera2:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
|
||||
implementation "androidx.camera:camera-view:$cameraxVersion"
|
||||
// Pure-Kotlin/Java QR decoder. ~470 KB jar, no Play Services dep.
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
}
|
||||
|
||||
243
android/src/main/kotlin/io/swipelab/ux/ScannerPlugin.kt
Normal file
243
android/src/main/kotlin/io/swipelab/ux/ScannerPlugin.kt
Normal file
@@ -0,0 +1,243 @@
|
||||
package io.swipelab.ux
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class ScannerPlugin :
|
||||
NativePlugin,
|
||||
MethodChannel.MethodCallHandler {
|
||||
companion object {
|
||||
// Single shared event sink — the app only ever shows one scanner
|
||||
// at a time (settings → scan), so collapsing to one channel keeps
|
||||
// the platform-view factory free of per-view registrar wiring.
|
||||
@JvmStatic
|
||||
@Volatile
|
||||
var eventSink: EventChannel.EventSink? = null
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
/// EventChannel methods are @UiThread; the analyzer runs on a
|
||||
/// background pool. Bounce through the main looper.
|
||||
@JvmStatic
|
||||
fun emit(code: String) {
|
||||
mainHandler.post { eventSink?.success(code) }
|
||||
}
|
||||
|
||||
private const val PERMISSION_REQUEST_CODE = 0xC1A0
|
||||
}
|
||||
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var eventChannel: EventChannel? = null
|
||||
private var lifecycleOwner: LifecycleOwner? = null
|
||||
private var activity: Activity? = null
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
private var pendingPermissionResult: MethodChannel.Result? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
val ec = EventChannel(binding.binaryMessenger, "ux/scanner/events")
|
||||
ec.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
}
|
||||
})
|
||||
eventChannel = ec
|
||||
|
||||
val mc = MethodChannel(binding.binaryMessenger, "ux/scanner")
|
||||
mc.setMethodCallHandler(this)
|
||||
methodChannel = mc
|
||||
|
||||
binding.platformViewRegistry.registerViewFactory(
|
||||
"ux/scanner",
|
||||
ScannerViewFactory { lifecycleOwner },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
eventChannel?.setStreamHandler(null)
|
||||
eventChannel = null
|
||||
eventSink = null
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
// The host app's MainActivity is a `FlutterActivity`, which extends
|
||||
// `LifecycleOwner` indirectly via `androidx.activity.ComponentActivity`.
|
||||
lifecycleOwner = binding.activity as? LifecycleOwner
|
||||
activity = binding.activity
|
||||
activityBinding = binding
|
||||
binding.addRequestPermissionsResultListener { code, _, results ->
|
||||
if (code != PERMISSION_REQUEST_CODE) return@addRequestPermissionsResultListener false
|
||||
val granted = results.isNotEmpty() &&
|
||||
results[0] == PackageManager.PERMISSION_GRANTED
|
||||
pendingPermissionResult?.success(granted)
|
||||
pendingPermissionResult = null
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
lifecycleOwner = null
|
||||
activity = null
|
||||
activityBinding = null
|
||||
// If the activity tears down with a request still in flight, settle
|
||||
// it cleanly rather than leaking the Result.
|
||||
pendingPermissionResult?.success(false)
|
||||
pendingPermissionResult = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"requestPermission" -> handleRequestPermission(result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRequestPermission(result: MethodChannel.Result) {
|
||||
val act = activity
|
||||
?: return result.error("no_activity", "plugin not attached to an activity", null)
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
act, Manifest.permission.CAMERA,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) return result.success(true)
|
||||
if (pendingPermissionResult != null) {
|
||||
return result.error("in_progress", "another permission request is in flight", null)
|
||||
}
|
||||
pendingPermissionResult = result
|
||||
ActivityCompat.requestPermissions(
|
||||
act,
|
||||
arrayOf(Manifest.permission.CAMERA),
|
||||
PERMISSION_REQUEST_CODE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ScannerViewFactory(
|
||||
private val lifecycleOwnerProvider: () -> LifecycleOwner?,
|
||||
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
|
||||
return ScannerPlatformView(context, lifecycleOwnerProvider())
|
||||
}
|
||||
}
|
||||
|
||||
private class ScannerPlatformView(
|
||||
context: Context,
|
||||
private val lifecycleOwner: LifecycleOwner?,
|
||||
) : PlatformView {
|
||||
// COMPATIBLE forces a TextureView under the hood; SurfaceView (the
|
||||
// PERFORMANCE default) is its own window and won't composite under
|
||||
// Flutter overlays in hybrid composition.
|
||||
private val previewView = PreviewView(context).apply {
|
||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||
}
|
||||
private val analysisExecutor = Executors.newSingleThreadExecutor()
|
||||
private val reader: MultiFormatReader = MultiFormatReader().apply {
|
||||
setHints(
|
||||
mapOf(
|
||||
DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
if (lifecycleOwner != null) bindCamera(context, lifecycleOwner)
|
||||
}
|
||||
|
||||
private fun bindCamera(context: Context, lifecycleOwner: LifecycleOwner) {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
val analysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { it.setAnalyzer(analysisExecutor, ::analyze) }
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||
preview,
|
||||
analysis,
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
// Permission denied / camera unavailable — preview stays
|
||||
// black. Caller surfaces a fallback (e.g. paste-hex).
|
||||
}
|
||||
}, androidx.core.content.ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
|
||||
private fun analyze(image: ImageProxy) {
|
||||
try {
|
||||
val plane = image.planes.firstOrNull() ?: return
|
||||
val buffer = plane.buffer
|
||||
val bytes = ByteArray(buffer.remaining()).also { buffer.get(it) }
|
||||
val source = PlanarYUVLuminanceSource(
|
||||
bytes,
|
||||
plane.rowStride,
|
||||
image.height,
|
||||
0, 0,
|
||||
image.width, image.height,
|
||||
false,
|
||||
)
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
val result = try {
|
||||
reader.decode(bitmap)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
} finally {
|
||||
reader.reset()
|
||||
}
|
||||
val text = result?.text
|
||||
if (!text.isNullOrEmpty()) {
|
||||
ScannerPlugin.emit(text)
|
||||
}
|
||||
} finally {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getView(): View = previewView
|
||||
|
||||
override fun dispose() {
|
||||
analysisExecutor.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ class UxPlugin : FlutterPlugin, ActivityAware {
|
||||
KeyboardPlugin(),
|
||||
SensorPlugin(),
|
||||
FilePlugin(),
|
||||
ScannerPlugin(),
|
||||
)
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) =
|
||||
|
||||
@@ -502,7 +502,7 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.8.0"
|
||||
version: "0.9.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
161
ios/Classes/ScannerPlugin.swift
Normal file
161
ios/Classes/ScannerPlugin.swift
Normal file
@@ -0,0 +1,161 @@
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
public class ScannerPlugin: NSObject, NativePlugin {
|
||||
fileprivate static let eventEmitter = ScannerEventEmitter()
|
||||
|
||||
public func register(with registrar: FlutterPluginRegistrar) {
|
||||
let factory = ScannerViewFactory(messenger: registrar.messenger())
|
||||
registrar.register(factory, withId: "ux/scanner")
|
||||
|
||||
let events = FlutterEventChannel(
|
||||
name: "ux/scanner/events",
|
||||
binaryMessenger: registrar.messenger(),
|
||||
)
|
||||
events.setStreamHandler(ScannerPlugin.eventEmitter)
|
||||
|
||||
let methods = FlutterMethodChannel(
|
||||
name: "ux/scanner",
|
||||
binaryMessenger: registrar.messenger(),
|
||||
)
|
||||
methods.setMethodCallHandler { call, result in
|
||||
switch call.method {
|
||||
case "requestPermission":
|
||||
ScannerPlugin.requestPermission(result: result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func requestPermission(result: @escaping FlutterResult) {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
switch status {
|
||||
case .authorized:
|
||||
result(true)
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video) { granted in
|
||||
DispatchQueue.main.async { result(granted) }
|
||||
}
|
||||
default:
|
||||
result(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ScannerEventEmitter: NSObject, FlutterStreamHandler {
|
||||
private var sink: FlutterEventSink?
|
||||
|
||||
func onListen(withArguments _: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
|
||||
sink = eventSink
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments _: Any?) -> FlutterError? {
|
||||
sink = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(_ code: String) {
|
||||
DispatchQueue.main.async { [weak self] in self?.sink?(code) }
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ScannerViewFactory: NSObject, FlutterPlatformViewFactory {
|
||||
private let messenger: FlutterBinaryMessenger
|
||||
|
||||
init(messenger: FlutterBinaryMessenger) {
|
||||
self.messenger = messenger
|
||||
super.init()
|
||||
}
|
||||
|
||||
func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
|
||||
return ScannerPlatformView(frame: frame, args: args)
|
||||
}
|
||||
|
||||
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
||||
return FlutterStandardMessageCodec.sharedInstance()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ScannerPreviewView: UIView {
|
||||
let previewLayer: AVCaptureVideoPreviewLayer
|
||||
|
||||
init(session: AVCaptureSession, frame: CGRect) {
|
||||
previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
previewLayer.videoGravity = .resizeAspectFill
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .black
|
||||
layer.addSublayer(previewLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) not supported") }
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
previewLayer.frame = bounds
|
||||
if let connection = previewLayer.connection, connection.isVideoOrientationSupported {
|
||||
connection.videoOrientation = .portrait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ScannerPlatformView: NSObject, FlutterPlatformView,
|
||||
AVCaptureMetadataOutputObjectsDelegate {
|
||||
private let containerView: ScannerPreviewView
|
||||
private let session = AVCaptureSession()
|
||||
private let sessionQueue = DispatchQueue(label: "ux.scanner.session")
|
||||
|
||||
init(frame: CGRect, args: Any?) {
|
||||
containerView = ScannerPreviewView(session: session, frame: frame)
|
||||
super.init()
|
||||
|
||||
sessionQueue.async { [weak self] in
|
||||
self?.configureSession(args: args)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
sessionQueue.async { [session] in
|
||||
if session.isRunning { session.stopRunning() }
|
||||
}
|
||||
}
|
||||
|
||||
func view() -> UIView {
|
||||
return containerView
|
||||
}
|
||||
|
||||
private func configureSession(args: Any?) {
|
||||
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
|
||||
return
|
||||
}
|
||||
guard let input = try? AVCaptureDeviceInput(device: device) else {
|
||||
return
|
||||
}
|
||||
session.beginConfiguration()
|
||||
if session.canAddInput(input) { session.addInput(input) }
|
||||
|
||||
let output = AVCaptureMetadataOutput()
|
||||
if session.canAddOutput(output) {
|
||||
session.addOutput(output)
|
||||
// QR is the only format we support today; the `formats` arg is
|
||||
// accepted for forward-compat but ignored.
|
||||
_ = args
|
||||
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||
output.metadataObjectTypes = [.qr]
|
||||
}
|
||||
session.commitConfiguration()
|
||||
session.startRunning()
|
||||
}
|
||||
|
||||
func metadataOutput(_ output: AVCaptureMetadataOutput,
|
||||
didOutput metadataObjects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection) {
|
||||
for obj in metadataObjects {
|
||||
guard let readable = obj as? AVMetadataMachineReadableCodeObject else { continue }
|
||||
guard let value = readable.stringValue, !value.isEmpty else { continue }
|
||||
ScannerPlugin.eventEmitter.send(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public class UxPlugin: NSObject, FlutterPlugin {
|
||||
KeyboardPlugin(),
|
||||
SensorPlugin(),
|
||||
FilePlugin(),
|
||||
ScannerPlugin(),
|
||||
]
|
||||
for plugin in plugins {
|
||||
plugin.register(with: registrar)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ux'
|
||||
s.version = '0.6.0'
|
||||
s.summary = 'UX Kit — Flutter plugin: keyboard, sensor, and file share/open.'
|
||||
s.version = '0.9.0'
|
||||
s.summary = 'UX Kit — Flutter plugin: keyboard, sensor, file, and QR scanner.'
|
||||
s.homepage = 'https://swipelab.co/ux.html'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Swipelab' => 'hello@swipelab.co' }
|
||||
|
||||
116
lib/src/scanner.dart
Normal file
116
lib/src/scanner.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
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 UxScannerPermission {
|
||||
UxScannerPermission._();
|
||||
|
||||
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 UxScanner extends StatefulWidget {
|
||||
const UxScanner({
|
||||
super.key,
|
||||
required this.onCode,
|
||||
this.formats = const [BarcodeFormat.qr],
|
||||
});
|
||||
|
||||
final ValueChanged<String> onCode;
|
||||
final List<BarcodeFormat> formats;
|
||||
|
||||
@override
|
||||
State<UxScanner> createState() => _UxScannerState();
|
||||
}
|
||||
|
||||
class _UxScannerState extends State<UxScanner> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export 'src/bezier.dart';
|
||||
export 'src/file.dart';
|
||||
export 'src/keyboard.dart';
|
||||
export 'src/auto_map.dart';
|
||||
export 'src/scanner.dart';
|
||||
export 'src/sensor.dart';
|
||||
export 'src/functional.dart';
|
||||
export 'src/log.dart';
|
||||
@@ -3,7 +3,7 @@ description: >-
|
||||
Flutter toolkit for fluid, native-feeling UIs. Includes frame-accurate
|
||||
keyboard height tracking via FFI with interactive dismiss, bezier utilities,
|
||||
and layout primitives.
|
||||
version: 0.8.0
|
||||
version: 0.9.0
|
||||
homepage: https://swipelab.co/ux.html
|
||||
repository: https://github.com/swipelab/ux
|
||||
issue_tracker: https://github.com/swipelab/ux/issues
|
||||
|
||||
Reference in New Issue
Block a user