diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b50c32..da6cba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog -# 55.8.1 +# 55.9.0 + diff --git a/android/build.gradle b/android/build.gradle index 9b7fe67..f002798 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,7 +4,7 @@ version '1.0' buildscript { ext { - anyline_sdk_version = '55.8.1' + anyline_sdk_version = '55.9.0' kotlin_version = "1.8.0" } repositories { diff --git a/android/src/main/java/io/anyline/flutter/AnylinePlugin.java b/android/src/main/java/io/anyline/flutter/AnylinePlugin.java index f612eef..4284102 100644 --- a/android/src/main/java/io/anyline/flutter/AnylinePlugin.java +++ b/android/src/main/java/io/anyline/flutter/AnylinePlugin.java @@ -1,8 +1,10 @@ package io.anyline.flutter; import android.content.Context; +import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.jetbrains.annotations.NotNull; import org.json.JSONArray; @@ -10,6 +12,8 @@ import java.util.List; +import io.anyline.flutter.nativeview.NativeFragmentContainerFactory; +import io.anyline.flutter.nativeview.NativeViewRegistry; import io.anyline.plugin.config.UIFeedbackElementConfig; import io.anyline.plugin.result.ExportedScanResult; import io.anyline.wrapper.config.WrapperSessionExportCachedEventsResponse; @@ -73,6 +77,8 @@ public class AnylinePlugin implements @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { onAttachedToEngine(flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger()); + flutterPluginBinding.getPlatformViewRegistry().registerViewFactory( + Constants.ANYLINE_NATIVE_VIEW_FACTORY_ID, new NativeFragmentContainerFactory()); } private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { @@ -103,6 +109,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { call.argument(Constants.EXTRA_LICENSE_KEY), customModelsPath, Boolean.TRUE.equals(call.argument(Constants.EXTRA_ENABLE_OFFLINE_CACHE))); + } else if (call.method.equals(Constants.METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS)) { + setDefaultScanStartPlatformOptions(call.argument(Constants.EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS), result); } else if (call.method.equals(Constants.METHOD_START_ANYLINE)) { startScanMethodResult = result; routeScanMode( @@ -110,6 +118,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { call.argument(Constants.EXTRA_INITIALIZATION_PARAMETERS), viewConfigsPath, call.argument(Constants.EXTRA_SCAN_CALLBACK_CONFIG)); + } else if (call.method.equals(Constants.METHOD_REPLACE_ANYLINE)) { + requestScanSwitchWithScanViewConfigContentString( + call.argument(Constants.EXTRA_CONFIG_JSON)); } else if (call.method.equals(Constants.METHOD_STOP_ANYLINE)) { tryStopScan(call.argument(Constants.EXTRA_STOP_CONFIG)); } else if (call.method.equals(Constants.METHOD_EXPORT_CACHED_EVENTS)) { @@ -135,6 +146,11 @@ private void setupWrapperSession(final String pluginVersion) { return this.context; } + @Override + public @Nullable ViewGroup getContainerView() { + return (ViewGroup) NativeViewRegistry.getLastOrNull(); + } + private void initSdk(String sdkLicenseKey, String sdkAssetsFolder, boolean enableOfflineCache) { @@ -163,6 +179,15 @@ public void onSdkInitializationResponse(@NotNull WrapperSessionSdkInitialization } } + private void setDefaultScanStartPlatformOptions(final String scanStartPlatformOptionsString, @NonNull Result resultListener) { + try { + WrapperSessionProvider.setDefaultScanStartPlatformOptions(scanStartPlatformOptionsString); + returnSuccess(resultListener, ""); + } catch (Exception e) { + returnDefaultError(resultListener, e.getMessage()); + } + } + private void routeScanMode( String scanViewConfigContent, String scanViewInitializationParametersString, @@ -192,6 +217,10 @@ private void routeScanMode( ResultReporter.setListener(this); } + private void requestScanSwitchWithScanViewConfigContentString(String scanViewConfigContent) { + WrapperSessionProvider.requestScanSwitchWithScanViewConfigContentString(scanViewConfigContent); + } + @Override public void onScanResults(@NotNull WrapperSessionScanResultsResponse scanResultsResponse) { WrapperSessionScanResultConfig scanResultConfig = scanResultsResponse.getScanResultConfig(); diff --git a/android/src/main/java/io/anyline/flutter/Constants.java b/android/src/main/java/io/anyline/flutter/Constants.java index 1187b49..47998d3 100644 --- a/android/src/main/java/io/anyline/flutter/Constants.java +++ b/android/src/main/java/io/anyline/flutter/Constants.java @@ -7,7 +7,9 @@ public class Constants { public static final String METHOD_SET_CUSTOM_MODELS_PATH = "METHOD_SET_CUSTOM_MODELS_PATH"; public static final String METHOD_SET_VIEW_CONFIGS_PATH = "METHOD_SET_VIEW_CONFIGS_PATH"; public static final String METHOD_SET_LICENSE_KEY = "METHOD_SET_LICENSE_KEY"; + public static final String METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS = "METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS"; public static final String METHOD_START_ANYLINE = "METHOD_START_ANYLINE"; + public static final String METHOD_REPLACE_ANYLINE = "METHOD_REPLACE_ANYLINE"; public static final String METHOD_STOP_ANYLINE = "METHOD_STOP_ANYLINE"; public static final String METHOD_EXPORT_CACHED_EVENTS = "METHOD_EXPORT_CACHED_EVENTS"; public static final String METHOD_REPORT_UCR = "METHOD_REPORT_UCR"; @@ -19,6 +21,7 @@ public class Constants { public static final String EXTRA_PLUGIN_VERSION = "EXTRA_PLUGIN_VERSION"; public static final String EXTRA_CUSTOM_MODELS_PATH = "EXTRA_CUSTOM_MODELS_PATH"; public static final String EXTRA_VIEW_CONFIGS_PATH = "EXTRA_VIEW_CONFIGS_PATH"; + public static final String EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS = "EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS"; public static final String EXTRA_SCAN_CALLBACK_CONFIG = "EXTRA_SCAN_CALLBACK_CONFIG"; public static final String EXTRA_STOP_CONFIG = "EXTRA_STOP_CONFIG"; public static final String EXTRA_REPORT_UCR_BLOBKEY = "EXTRA_REPORT_UCR_BLOBKEY"; @@ -32,4 +35,6 @@ public class Constants { public static final int RESULT_CANCELLED = 0; public static final int RESULT_OK = 1; public static final int RESULT_ERROR = 2; + + public static final String ANYLINE_NATIVE_VIEW_FACTORY_ID = "AnylineNativeView"; } diff --git a/android/src/main/java/io/anyline/flutter/nativeview/NativeFragmentContainerFactory.java b/android/src/main/java/io/anyline/flutter/nativeview/NativeFragmentContainerFactory.java new file mode 100644 index 0000000..8140c6c --- /dev/null +++ b/android/src/main/java/io/anyline/flutter/nativeview/NativeFragmentContainerFactory.java @@ -0,0 +1,23 @@ + +package io.anyline.flutter.nativeview; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import io.flutter.plugin.common.StandardMessageCodec; +import java.util.Map; + +public class NativeFragmentContainerFactory extends PlatformViewFactory { + public NativeFragmentContainerFactory() { + super(StandardMessageCodec.INSTANCE); + } + + @NonNull + @Override + public PlatformView create(Context context, int viewId, Object args) { + return new NativeFragmentContainerView(context, viewId, (Map) args); + } +} diff --git a/android/src/main/java/io/anyline/flutter/nativeview/NativeFragmentContainerView.java b/android/src/main/java/io/anyline/flutter/nativeview/NativeFragmentContainerView.java new file mode 100644 index 0000000..87054ae --- /dev/null +++ b/android/src/main/java/io/anyline/flutter/nativeview/NativeFragmentContainerView.java @@ -0,0 +1,48 @@ + +package io.anyline.flutter.nativeview; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.graphics.PixelFormat; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import io.flutter.plugin.platform.PlatformView; +import java.util.Map; + +public class NativeFragmentContainerView implements PlatformView { + private final FrameLayout container; + private final String containerId; + + NativeFragmentContainerView(Context context, int id, Map args) { + container = new FrameLayout(context); + container.setLayoutParams( + new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT + )); + + container.isHardwareAccelerated(); + + container.setId(View.generateViewId()); + + containerId = String.valueOf(container.getId()); + + NativeViewRegistry.register(containerId, container); + } + + @Override + public View getView() { + return container; + } + + @Override + public void dispose() { + NativeViewRegistry.remove(containerId); + } +} diff --git a/android/src/main/java/io/anyline/flutter/nativeview/NativeViewRegistry.java b/android/src/main/java/io/anyline/flutter/nativeview/NativeViewRegistry.java new file mode 100644 index 0000000..d14410e --- /dev/null +++ b/android/src/main/java/io/anyline/flutter/nativeview/NativeViewRegistry.java @@ -0,0 +1,35 @@ + +package io.anyline.flutter.nativeview; + +import android.view.View; + +import java.util.HashMap; +import java.util.Map; + +public class NativeViewRegistry { + private static final Map registry = new HashMap<>(); + + private NativeViewRegistry () { + //empty constructor + } + + public static void register(String id, View view) { + registry.put(id, view); + } + + public static View get(String id) { + return registry.get(id); + } + + public static View getLastOrNull() { + if (registry.isEmpty()) { + return null; + } else { + return (View) registry.values().toArray()[registry.size() - 1]; + } + } + + public static void remove(String id) { + registry.remove(id); + } +} diff --git a/example/android/app/src/main/java/io/anyline/flutter/examples/MainActivity.java b/example/android/app/src/main/java/io/anyline/flutter/examples/MainActivity.java index d0608a7..e22cf8a 100644 --- a/example/android/app/src/main/java/io/anyline/flutter/examples/MainActivity.java +++ b/example/android/app/src/main/java/io/anyline/flutter/examples/MainActivity.java @@ -1,6 +1,6 @@ package io.anyline.flutter.examples; -import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterFragmentActivity; -public class MainActivity extends FlutterActivity { +public class MainActivity extends FlutterFragmentActivity { } diff --git a/example/config/NativeViewBarcodeConfig.json b/example/config/NativeViewBarcodeConfig.json new file mode 100644 index 0000000..9ece8a9 --- /dev/null +++ b/example/config/NativeViewBarcodeConfig.json @@ -0,0 +1,74 @@ +{ + "options": { + + }, + "flashConfig": { + "mode": "manual_off", + "alignment": "top_left" + }, + "viewPluginConfig": { + "pluginConfig": { + "id": "barcode_all", + "barcodeConfig": { + "fastProcessMode": true, + "parseAAMVA": false, + "barcodeFormats": [ + "ALL" + ] + }, + "consecutiveEqualResultFilter": -1, + "cancelOnResult": false, + "startScanDelay": 1000 + }, + "cutoutConfig": { + "animation": "none", + "maxWidthPercent": "70%", + "maxHeightPercent": "70%", + "alignment": "center", + "ratioFromSize": { + "width": 3, + "height": 2 + }, + "offset": { + "x": 0, + "y": 0 + }, + "cropOffset": { + "x": 0, + "y": 0 + }, + "cropPadding": { + "x": 0, + "y": 0 + }, + "cornerRadius": 4, + "strokeColor": "0099ff", + "strokeWidth": 2, + "outerColor": "000000", + "feedbackStrokeColor": "0099FF", + "outerAlpha": 0.3 + }, + "scanFeedbackConfig": { + "style": "rect", + "strokeWidth": 0, + "strokeColor": "0099FF", + "fillColor": "330099FF", + "beepOnResult": false, + "vibrateOnResult": false, + "blinkAnimationOnResult": false + }, + "uiFeedbackConfig": { + "presets": [ + { + "presetName": "simple_instruction_label", + "presetAttributes": [ + { + "attributeName": "instruction_text", + "attributeValue": "Scanning different barcodes." + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/example/config/NativeViewDrivingLicenseConfig.json b/example/config/NativeViewDrivingLicenseConfig.json new file mode 100644 index 0000000..21906c8 --- /dev/null +++ b/example/config/NativeViewDrivingLicenseConfig.json @@ -0,0 +1,75 @@ +{ + "options": { + "segmentConfig": { + "titles": ["Back Camera", "Front Camera"], + "viewConfigs": ["NativeViewDrivingLicenseConfig.json", "NativeViewDrivingLicenseFrontConfig.json"], + "offset.y": -24, + "tintColor": "0099ff" + } + }, + "cameraConfig": { + "captureResolution": "1080p", + "defaultCamera": "BACK" + }, + "flashConfig": { + "mode": "manual", + "alignment": "top_left" + }, + "viewPluginConfig": { + "pluginConfig": { + "id": "driver_license_eu", + "universalIdConfig": { + "allowedLayouts": { + "drivingLicense": [] + }, + "alphabet": "latin" + }, + "cancelOnResult": true + }, + "cutoutConfig" : { + "maxWidthPercent": "90%", + "maxHeightPercent": "90%", + "alignment": "center", + "strokeWidth": 2, + "cornerRadius": 4, + "strokeColor": "0099FF", + "outerColor": "000000", + "outerAlpha": 0.3, + "ratioFromSize" : { + "width": 50, + "height": 31 + }, + "cropPadding": { + "x": 25, + "y": 25 + }, + "cropOffset": { + "x": 0, + "y": 0 + }, + "feedbackStrokeColor": "0099FF" + }, + "scanFeedbackConfig" : { + "style": "contour_rect", + "redrawTimeout": 100, + "strokeColor": "0099FF", + "fillColor" : "220099FF", + "beepOnResult": true, + "vibrateOnResult": true, + "strokeWidth": 2 + }, + "uiFeedbackConfig": { + "presets": [ + { + "presetName": "simple_instruction_label", + "presetAttributes": [ + { + "attributeName": "instruction_text", + "attributeValue": "Position the entire Driving License within the cutout" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/example/config/NativeViewDrivingLicenseFrontConfig.json b/example/config/NativeViewDrivingLicenseFrontConfig.json new file mode 100644 index 0000000..c681d87 --- /dev/null +++ b/example/config/NativeViewDrivingLicenseFrontConfig.json @@ -0,0 +1,75 @@ +{ + "options": { + "segmentConfig": { + "titles": ["Back Camera", "Front Camera"], + "viewConfigs": ["NativeViewDrivingLicenseConfig.json", "NativeViewDrivingLicenseFrontConfig.json"], + "offset.y": -24, + "tintColor": "0099ff" + } + }, + "cameraConfig": { + "captureResolution": "1080p", + "defaultCamera": "FRONT" + }, + "flashConfig": { + "mode": "manual", + "alignment": "top_left" + }, + "viewPluginConfig": { + "pluginConfig": { + "id": "driver_license_eu", + "universalIdConfig": { + "allowedLayouts": { + "drivingLicense": [] + }, + "alphabet": "latin" + }, + "cancelOnResult": true + }, + "cutoutConfig" : { + "maxWidthPercent": "90%", + "maxHeightPercent": "90%", + "alignment": "center", + "strokeWidth": 2, + "cornerRadius": 4, + "strokeColor": "0099FF", + "outerColor": "000000", + "outerAlpha": 0.3, + "ratioFromSize" : { + "width": 50, + "height": 31 + }, + "cropPadding": { + "x": 25, + "y": 25 + }, + "cropOffset": { + "x": 0, + "y": 0 + }, + "feedbackStrokeColor": "0099FF" + }, + "scanFeedbackConfig" : { + "style": "contour_rect", + "redrawTimeout": 100, + "strokeColor": "0099FF", + "fillColor" : "220099FF", + "beepOnResult": true, + "vibrateOnResult": true, + "strokeWidth": 2 + }, + "uiFeedbackConfig": { + "presets": [ + { + "presetName": "simple_instruction_label", + "presetAttributes": [ + { + "attributeName": "instruction_text", + "attributeValue": "Position the entire Driving License within the cutout" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/example/config/NativeViewVINConfig.json b/example/config/NativeViewVINConfig.json new file mode 100644 index 0000000..6d7c1ab --- /dev/null +++ b/example/config/NativeViewVINConfig.json @@ -0,0 +1,121 @@ +{ + "options": { + + }, + "cameraConfig": { + "captureResolution": "1080p", + "zoomGesture": true + }, + "flashConfig": { + "mode": "manual", + "alignment": "top_left" + }, + "viewPluginConfig": { + "pluginConfig": { + "id": "vehicle_identification_number", + "cancelOnResult": true, + "vinConfig": { + "validateCheckDigit": true + } + }, + "cutoutConfig": { + "maxWidthPercent": "70%", + "maxHeightPercent": "70%", + "alignment": "top_half", + "ratioFromSize": { + "width": 689, + "height": 100 + }, + "outerColor": "000000", + "outerAlpha": 0.3, + "strokeWidth": 2, + "strokeColor": "0099FF", + "cornerRadius": 4, + "feedbackStrokeColor": "0099FF" + }, + "scanFeedbackConfig": { + "animation": "traverse_multi", + "animationDuration": 250, + "style": "contour_rect", + "strokeWidth": 2, + "strokeColor": "0099FF", + "fillColor": "220099FF", + "beepOnResult": true, + "vibrateOnResult": true, + "blinkAnimationOnResult": true + }, + "uiFeedbackConfig": { + "presets": [ + { + "presetName": "vin_with_instruction_image_text_sound_feedback", + "presetAttributes": [ + { + "attributeName": "instruction_text", + "attributeValue": "Please make sure the entire VIN number is inside the cutout." + }, + { + "attributeName": "lighting_toodark_image", + "attributeValue": "uifeedback_default_toodark" + }, + { + "attributeName": "lighting_toobright_image", + "attributeValue": "uifeedback_default_toobright" + }, + { + "attributeName": "distance_moveback_image", + "attributeValue": "uifeedback_default_moveback" + }, + { + "attributeName": "distance_movecloser_image", + "attributeValue": "uifeedback_default_movecloser" + }, + { + "attributeName": "format_invalid_image", + "attributeValue": "uifeedback_vin_invalid" + }, + { + "attributeName": "lighting_toodark_text", + "attributeValue": "" + }, + { + "attributeName": "lighting_toobright_text", + "attributeValue": "" + }, + { + "attributeName": "distance_moveback_text", + "attributeValue": "" + }, + { + "attributeName": "distance_movecloser_text", + "attributeValue": "" + }, + { + "attributeName": "format_invalid_text", + "attributeValue": "" + }, + { + "attributeName": "lighting_toodark_sound", + "attributeValue": "info_sound_default.wav" + }, + { + "attributeName": "lighting_toobright_sound", + "attributeValue": "info_sound_default.wav" + }, + { + "attributeName": "distance_moveback_sound", + "attributeValue": "info_sound_default.wav" + }, + { + "attributeName": "distance_movecloser_sound", + "attributeValue": "info_sound_default.wav" + }, + { + "attributeName": "format_invalid_sound", + "attributeValue": "info_sound_default.wav" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5b263f9..a28d722 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - Anyline (55.8.1) - - anyline_plugin (55.8.1): - - Anyline (= 55.8.1) + - Anyline (55.9.0) + - anyline_plugin (55.9.0): + - Anyline (= 55.9.0) - Flutter - Flutter (1.0.0) - shared_preferences_foundation (0.0.1): @@ -26,8 +26,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - Anyline: 04fd17f7bbf08b7c5cde9a5c095add7ebc5b4ecd - anyline_plugin: a28671d32fe098ce15b9d586ee466b7831b4782b + Anyline: 8c190d726cc22d5596f9fd271b557962a9ac720b + anyline_plugin: cb72085001f724c319de8b15aa865a92c0e1f393 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 442cb01..dd3316c 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -407,7 +407,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 55.8.1; + MARKETING_VERSION = 55.9.0; PRODUCT_BUNDLE_IDENTIFIER = io.anyline.flutter.examples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -545,7 +545,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 55.8.1; + MARKETING_VERSION = 55.9.0; PRODUCT_BUNDLE_IDENTIFIER = io.anyline.flutter.examples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -578,7 +578,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 55.8.1; + MARKETING_VERSION = 55.9.0; PRODUCT_BUNDLE_IDENTIFIER = io.anyline.flutter.examples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/example/lib/anyline_service.dart b/example/lib/anyline_service.dart index 3e91727..ceecedb 100644 --- a/example/lib/anyline_service.dart +++ b/example/lib/anyline_service.dart @@ -12,6 +12,12 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:path/path.dart' as path; abstract class AnylineService { + MethodChannel getChannel(); + + Future ensureSdkIsInitialized(); + + Future setDefaultScanStartPlatformOptions(String? scanStartPlatformOptionsString); + Future scan(ScanMode mode); List getResultList(); @@ -19,14 +25,28 @@ abstract class AnylineService { String? getSdkVersion(); String? getPluginVersion(); + + Future getConfigJson(ScanMode mode); + + AnylinePlugin getAnylinePlugin(); } class AnylineServiceImpl implements AnylineService { + + AnylineServiceImpl.skipInit(); + AnylineServiceImpl() { _initAnylinePlugin(); _initResultListFromSharedPreferences(); } + static Future create() async { + var instance = AnylineServiceImpl.skipInit(); + await instance._initAnylinePlugin(); + await instance._initResultListFromSharedPreferences(); + return instance; + } + static const MethodChannel _channel = MethodChannel('anyline_plugin'); static const String NATIVE_METHOD_ON_RESULT_EVENT = @@ -46,6 +66,26 @@ class AnylineServiceImpl implements AnylineService { var _continuousResults = ''; int _continuousCount = 0; + @override + Future ensureSdkIsInitialized() async { + if (!licenseState.initialized) { + String externalLicenseKey = await _getExternalLicenseKey(); + return _initSdk(externalLicenseKey); + } else { + return LicenseState(true, ''); + } + } + + @override + MethodChannel getChannel() { + return _channel; + } + + @override + AnylinePlugin getAnylinePlugin() { + return anylinePlugin; + } + Future _initSdk(String licenseKey) async { try { await anylinePlugin.initSdk(licenseKey); @@ -57,6 +97,12 @@ class AnylineServiceImpl implements AnylineService { return licenseState; } + // This method can be used to provide platform-specific options to be used during scan process + @override + Future setDefaultScanStartPlatformOptions(String? scanStartPlatformOptionsString) async { + return anylinePlugin.setDefaultScanStartPlatformOptions(scanStartPlatformOptionsString); + } + @override Future scan(ScanMode mode) async { Result? result = await _callAnyline(mode); @@ -82,7 +128,7 @@ class AnylineServiceImpl implements AnylineService { return _pluginVersion; } - void _initAnylinePlugin() async { + Future _initAnylinePlugin() async { String? sdkVersion; try { sdkVersion = await AnylinePlugin.sdkVersion; @@ -97,7 +143,7 @@ class AnylineServiceImpl implements AnylineService { anylinePlugin.setViewConfigsPath('flutter_assets/config'); } - void _initResultListFromSharedPreferences() async { + Future _initResultListFromSharedPreferences() async { SharedPreferences prefs = await SharedPreferences.getInstance(); List list = prefs.getStringList('results') ?? []; @@ -145,12 +191,9 @@ class AnylineServiceImpl implements AnylineService { } Future _callAnyline(ScanMode mode) async { - if (!licenseState.initialized) { - String externalLicenseKey = await _getExternalLicenseKey(); - await _initSdk(externalLicenseKey); - } + ensureSdkIsInitialized(); - String configJson = await _getConfigJson(mode); + String configJson = await getConfigJson(mode); String? stringResult; if (mode.isContinuous()) { @@ -217,7 +260,8 @@ class AnylineServiceImpl implements AnylineService { /// Returns the config string for a given scan mode in JSON format, reading the /// associated file from the config folder. - Future _getConfigJson(ScanMode mode) async { + @override + Future getConfigJson(ScanMode mode) async { String configJson = await _loadJsonConfigFromFile(mode.key); return configJson; } diff --git a/example/lib/home.dart b/example/lib/home.dart index a822c90..d16a303 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:anyline_plugin/exceptions.dart'; import 'package:anyline_plugin_example/anyline_service.dart'; +import 'package:anyline_plugin_example/native_view_scan.dart'; import 'package:anyline_plugin_example/result.dart'; import 'package:anyline_plugin_example/styles.dart'; import 'package:flutter/foundation.dart'; @@ -24,6 +25,7 @@ class AnylineDemoApp extends StatelessWidget { ResultDisplay.routeName: (context) => ResultDisplay(), FullScreenImage.routeName: (context) => FullScreenImage(), CompositeResultDisplay.routeName: (context) => CompositeResultDisplay(), + NativeViewScan.routeName: (context) => const NativeViewScan(), }, home: Home(), theme: ThemeData.light().copyWith( @@ -61,7 +63,21 @@ class _HomeState extends State { DeviceOrientation.portraitUp, ]); - _anylineService = AnylineServiceImpl(); + Future(() async { + try { + _anylineService = await AnylineServiceImpl.create(); + } catch (e) { + String errorMessage; + if (e is AnylineException) { + errorMessage = e.message ?? ''; + } else { + errorMessage = e.toString(); + } + if (mounted) { + _showErrorDialog(context, errorMessage); + } + } + }); _scanTab = _buildUseCases(); } @@ -79,7 +95,7 @@ class _HomeState extends State { Future scan(ScanMode mode) async { try { Result? result = await _anylineService.scan(mode); - if (result != null) { + if (result != null && mounted) { _openResultDisplay(result); } } catch (e) { @@ -109,34 +125,40 @@ class _HomeState extends State { print('[ERROR] Final error message: $message'); } - showDialog( - context: context, - builder: (_) => AlertDialog( - elevation: 0, - title: const Text( - 'Error', - style: TextStyle( - fontFamily: 'Roboto', fontWeight: FontWeight.bold), - ), - content: Text( - message, - style: TextStyle(fontFamily: 'Roboto'), - textAlign: TextAlign.start, - ), - actions: [ - TextButton( - child: Text('OK', - style: TextStyle( - fontFamily: 'Roboto', fontWeight: FontWeight.bold)), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - )); + if (mounted) { + _showErrorDialog(context, message); + } } } + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (_) => AlertDialog( + elevation: 0, + title: const Text( + 'Error', + style: TextStyle( + fontFamily: 'Roboto', fontWeight: FontWeight.bold), + ), + content: Text( + message, + style: TextStyle(fontFamily: 'Roboto'), + textAlign: TextAlign.start, + ), + actions: [ + TextButton( + child: Text('OK', + style: TextStyle( + fontFamily: 'Roboto', fontWeight: FontWeight.bold)), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + )); + } + void _openResultDisplay(Result result) { Navigator.pushNamed( context, @@ -367,6 +389,22 @@ class _HomeState extends State { ], ), ), + Expanded( + child: Row( + children: [ + UseCaseButton( + text: 'Native View Scan', + image: const AssetImage('assets/Other.png'), + onPressed: () { + Navigator.pushNamed( + context, + NativeViewScan.routeName, + arguments: _anylineService); + }, + ), + ], + ), + ), ], ), ), diff --git a/example/lib/native_view_scan.dart b/example/lib/native_view_scan.dart new file mode 100644 index 0000000..8778c69 --- /dev/null +++ b/example/lib/native_view_scan.dart @@ -0,0 +1,369 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:anyline_plugin/anyline_native_view.dart'; +import 'package:anyline_plugin_example/anyline_service.dart'; +import 'package:anyline_plugin_example/scan_modes.dart'; +import 'package:anyline_plugin_example/result.dart'; + +class NativeViewScan extends StatefulWidget { + const NativeViewScan({Key? key}) : super(key: key); + + static const routeName = '/nativeViewScan'; + + @override + State createState() => _NativeViewScanState(); +} + +class _NativeViewScanState extends State { + static const String NATIVE_METHOD_ON_RESULT_EVENT = + 'NATIVE_METHOD_ON_RESULT_EVENT'; + static const String NATIVE_METHOD_ON_UI_ELEMENT_CLICKED = + 'NATIVE_METHOD_ON_UI_ELEMENT_CLICKED'; + + bool isScanning = false; + ScanMode? selectedScanMode; + List scanResults = []; + late AnylineService _anylineService; + + // Available scan modes for the segmented buttons + final List availableScanModes = [ + ScanMode.NativeViewBarcode, + ScanMode.NativeViewDrivingLicense, + ScanMode.NativeViewVIN, + ]; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Extract AnylineService from route arguments + final args = ModalRoute.of(context)?.settings.arguments; + if (args is AnylineService) { + _anylineService = args; + } + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (!didPop) { + final navigator = Navigator.of(context); + if (await _onBackPressed()) { + navigator.pop(result); + } + } + }, + child: Scaffold( + appBar: AppBar( + title: const Text('Native View Scan'), + backgroundColor: Colors.blue, + ), + body: Column( + children: [ + // Top segmented button bar (horizontally scrollable) + _buildScanModeSelector(), + + // Native view area (AndroidView or UiKitView) + // When not scanning, shrink to minimal size to give space to results + Expanded( + flex: isScanning ? 5 : 1, + child: _buildNativeView(), + ), + + // Bottom sheet for scan results (horizontally scrollable) + // When not scanning, expand to take most of the space + if (isScanning) + _buildScanResultsSheet() + else + Expanded( + flex: 4, + child: _buildScanResultsSheet(), + ), + ], + ), + ), + ); + } + + Widget _buildScanModeSelector() { + return Container( + height: 60, + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: availableScanModes.length, + itemBuilder: (context, index) { + final scanMode = availableScanModes[index]; + final isSelected = selectedScanMode == scanMode; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: ChoiceChip( + label: Text(scanMode.label), + selected: isSelected, + onSelected: (selected) { + if (selected) { + _onScanModeSelected(scanMode); + } + }, + selectedColor: Colors.blue, + labelStyle: TextStyle( + color: isSelected ? Colors.white : Colors.black, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ); + }, + ), + ); + } + + Widget _buildNativeView() { + // Platform-specific native view + return Container( + color: Colors.black, + child: const Center( + child: Stack( + children: [ + // This is a placeholder for the native view + AnylineNativeView(), + ], + ), + ), + ); + } + + Widget _buildScanResultsSheet() { + if (scanResults.isEmpty) { + return Container( + height: isScanning ? 100 : null, + decoration: BoxDecoration( + color: Colors.grey[200], + border: Border(top: BorderSide(color: Colors.grey[400]!)), + ), + child: const Center( + child: Text( + 'No scan results yet', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ), + ); + } + + return Container( + height: isScanning ? 120 : null, + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey[400]!)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, -2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Scan Results (${scanResults.length})', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + TextButton( + onPressed: _clearResults, + child: const Text('Clear All'), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + itemCount: scanResults.length, + itemBuilder: (context, index) { + return _buildResultCard(scanResults[index], index); + }, + ), + ), + ], + ), + ); + } + + Widget _buildResultCard(Result result, int index) { + // Use larger card when not scanning (expanded view) + final cardWidth = isScanning ? 150.0 : 200.0; + final imageHeight = isScanning ? 40.0 : 150.0; + final labelFontSize = isScanning ? 10.0 : 14.0; + final resultFontSize = isScanning ? 9.0 : 12.0; + + return Card( + margin: const EdgeInsets.only(right: 8.0), + child: Container( + width: cardWidth, + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image preview + Container( + height: imageHeight, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Image.file( + File(result.resultInfo.imageMap?['imagePath'] as String), + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.image_not_supported, + color: Colors.grey[600], + size: 32, + ); + }, + ), + ), + ), + const SizedBox(height: 8), + // Result info + Text( + result.scanMode.label, + style: TextStyle( + fontSize: labelFontSize, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Expanded( + child: SingleChildScrollView( + child: Text( + (result.resultInfo.orderedJson?[0].toString() as String), + style: TextStyle( + fontSize: resultFontSize, + color: Colors.grey[600], + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _onScanModeSelected(ScanMode scanMode) async { + setState(() { + selectedScanMode = scanMode; + }); + + String configJson = await _anylineService.getConfigJson(scanMode); + if (isScanning) { + _anylineService.getAnylinePlugin().trySwitchScan(configJson); + } else { + _startScanning(configJson); + } + + if (kDebugMode) { + print('Selected scan mode: ${scanMode.label}'); + } + } + + void _startScanning(String configJson) async { + setState(() { + isScanning = true; + }); + + _anylineService.ensureSdkIsInitialized(); + + _anylineService.getChannel().setMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case NATIVE_METHOD_ON_RESULT_EVENT: + + String stringResult = _anylineService.getAnylinePlugin() + .convertResultsWithImagePathString(methodCall.arguments.toString(), 0); + + Map? jsonResult = jsonDecode('{$stringResult}') as Map; + + if (selectedScanMode != null) { + setState(() { + Result results = Result(jsonResult, selectedScanMode!, DateTime.now()); + for (var i = 0; (i < results.length); i++) { + scanResults.insert(0, Result( + results.values[i] as Map, + selectedScanMode!, + DateTime.now())); + } + }); + } + + break; + case NATIVE_METHOD_ON_UI_ELEMENT_CLICKED: + // clicked UI elements clicked events can be handled here + break; + } + }); + + String callbackConfigString = + '{ "onResultEventName": "$NATIVE_METHOD_ON_RESULT_EVENT" }'; + + if (kDebugMode) { + print('Started scanning: ${selectedScanMode?.label}'); + } + + await _anylineService.getAnylinePlugin().startScanning( + configJson, + null, + callbackConfigString); + + setState(() { + isScanning = false; + selectedScanMode = null; + }); + } + + void _stopScanning() { + _anylineService.getAnylinePlugin().tryStopScan(); + + setState(() { + isScanning = false; + selectedScanMode = null; + }); + + if (kDebugMode) { + print('Stopped scanning'); + } + } + + void _clearResults() { + setState(() { + scanResults.clear(); + }); + } + + Future _onBackPressed() async { + if (isScanning) { + // If currently scanning, stop scanning first + _stopScanning(); + return false; // Don't exit the page + } + return true; // Allow back navigation + } +} \ No newline at end of file diff --git a/example/lib/result.dart b/example/lib/result.dart index 6133f51..bb42610 100644 --- a/example/lib/result.dart +++ b/example/lib/result.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:anyline_plugin_example/scan_modes.dart'; class Result { @@ -6,12 +8,17 @@ class Result { DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int), scanMode = ScanMode.values .firstWhere((element) => element.key == json['scanMode']), - jsonMap = json['jsonMap'] as Map; + jsonMap = json['jsonMap'] as Map, + resultInfo = ResultInfo(json['jsonMap'] as Map); + + Result(this.jsonMap, this.scanMode, this.timestamp) { + resultInfo = ResultInfo(jsonMap); + } - Result(this.jsonMap, this.scanMode, this.timestamp); DateTime timestamp; ScanMode scanMode; Map? jsonMap; + late ResultInfo resultInfo; int get length { return jsonMap!.length; @@ -31,3 +38,58 @@ class Result { 'jsonMap': jsonMap, }; } + +class ResultInfo { + ResultInfo(this.json) { + orderedJson = []; + imageMap = {}; + nativeBarcodesDetected = []; + + var actualResultMap = {}; + + // NOTE: keep xxxResult on top, nativeBarcodesDetected, imagePath and fullImagePath at the bottom + json?.forEach((key, value) { + if (key.toLowerCase().endsWith('imagepath')) { + imageMap![key] = value; + return; + } + if (key.toLowerCase().endsWith('result')) { + // but not native barcode results + actualResultMap[key] = value; + return; + } + if (key.toLowerCase() == 'nativebarcodesdetected') { + nativeBarcodesDetected?.add(value); + return; + } + + orderedJson!.add({key: value}); + }); + + actualResultMap.forEach((key, value) { + var encoder = JsonEncoder.withIndent(' ' * 2); + var prettyJSON = encoder.convert(value); + orderedJson!.insert(0, {key: prettyJSON}); + }); + + if (nativeBarcodesDetected != null && nativeBarcodesDetected!.isNotEmpty) { + orderedJson!.add({'nativeBarcodesDetected': nativeBarcodesDetected}); + } + + dynamic imagePath; + + imagePath = imageMap?['imagePath']; + if (imagePath != null && imagePath.toString().isNotEmpty) { + orderedJson!.add({'imagePath': imagePath}); + } + + imagePath = imageMap?['fullImagePath']; + if (imagePath != null && imagePath.toString().isNotEmpty) { + orderedJson!.add({'fullImagePath': imagePath}); + } + } + final Map? json; + late final Map? imageMap; + late final List>? orderedJson; + late final List? nativeBarcodesDetected; +} diff --git a/example/lib/result_display.dart b/example/lib/result_display.dart index 12d8fa3..24e82c3 100644 --- a/example/lib/result_display.dart +++ b/example/lib/result_display.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:anyline_plugin_example/result.dart'; @@ -32,7 +31,7 @@ class ResultDisplay extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(20)), child: Container( color: Colors.white, - child: ResultDetails(result.jsonMap), + child: ResultDetails(ResultInfo(result.jsonMap)), ), ), ); @@ -97,67 +96,29 @@ class CompositeResultDisplay extends StatelessWidget { List createResultTabViews(List> results) { List resultTabViews = []; for (var i = 0; (i < results.length) && (i < displayResultMax); i++) { - resultTabViews.add(ResultDetails(results[i])); + resultTabViews.add(ResultDetails(ResultInfo(results[i]))); } return resultTabViews; } } class ResultDetails extends StatelessWidget { - ResultDetails(Map? json, {Key? key}) - : json = json, - super(key: key) { - orderedJson = []; - imageMap = Map(); - nativeBarcodesDetected = []; - - var actualResultMap = Map(); - - // NOTE: keep xxxResult on top, nativeBarcodesDetected, imagePath and fullImagePath at the bottom - json?.forEach((key, value) { - if (key.toLowerCase().endsWith('imagepath')) { - imageMap![key] = value; - return; - } - if (key.toLowerCase().endsWith('result')) { - // but not native barcode results - actualResultMap[key] = value; - return; - } - if (key.toLowerCase() == 'nativebarcodesdetected') { - nativeBarcodesDetected?.add(value); - return; - } - - orderedJson!.add({key: value}); - }); - - actualResultMap.forEach((key, value) { - var encoder = JsonEncoder.withIndent(' ' * 2); - var prettyJSON = encoder.convert(value); - orderedJson!.insert(0, {key: prettyJSON}); - }); - - if (nativeBarcodesDetected != null && nativeBarcodesDetected!.isNotEmpty) { - orderedJson!.add({'nativeBarcodesDetected': nativeBarcodesDetected}); - } - - dynamic imagePath; - - imagePath = imageMap?['imagePath']; - if (imagePath != null && imagePath.toString().isNotEmpty) { - orderedJson!.add({'imagePath': imagePath}); - } + const ResultDetails(this.resultInfo, {Key? key}) + : super(key: key); - imagePath = imageMap?['fullImagePath']; - if (imagePath != null && imagePath.toString().isNotEmpty) { - orderedJson!.add({'fullImagePath': imagePath}); - } + final ResultInfo resultInfo; + Map? get json { + return resultInfo.json; + } + Map? get imageMap { + return resultInfo.imageMap; + } + List>? get orderedJson { + return resultInfo.orderedJson; + } + List? get nativeBarcodesDetected { + return resultInfo.nativeBarcodesDetected; } - final Map? json; - late final Map? imageMap; - late final List>? orderedJson; - late final List? nativeBarcodesDetected; @override Widget build(BuildContext context) { diff --git a/example/lib/scan_modes.dart b/example/lib/scan_modes.dart index 5becaa0..61cf161 100644 --- a/example/lib/scan_modes.dart +++ b/example/lib/scan_modes.dart @@ -23,7 +23,10 @@ enum ScanMode { VerticalContainer, VIN, VRC, - CowTag + CowTag, + NativeViewBarcode, + NativeViewDrivingLicense, + NativeViewVIN } extension ScanModeInfo on ScanMode { @@ -79,6 +82,12 @@ extension ScanModeInfo on ScanMode { return 'Vehicle Registration Certificate'; case ScanMode.CowTag: return 'Cow Tag'; + case ScanMode.NativeViewBarcode: + return 'Barcode'; + case ScanMode.NativeViewDrivingLicense: + return 'Driving License'; + case ScanMode.NativeViewVIN: + return 'Vehicle Identification Number'; } } diff --git a/example/pubspec.lock b/example/pubspec.lock index ceb59b9..32a0dad 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: path: ".." relative: true source: path - version: "55.8.1" + version: "55.9.0" async: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index a13feed..d4aca75 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: anyline_plugin_example description: Demonstrates how to use the anyline_plugin plugin. -version: 55.8.1 +version: 55.9.0 # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. diff --git a/ios/Classes/AnylinePlugin.m b/ios/Classes/AnylinePlugin.m index 7859f05..2f1ddb6 100644 --- a/ios/Classes/AnylinePlugin.m +++ b/ios/Classes/AnylinePlugin.m @@ -1,4 +1,6 @@ #import "AnylinePlugin.h" +#import "NativeView/NativeViewContainerFactory.h" +#import "NativeView/NativeViewRegistry.h" // Static instance to retain the ALWrapperSessionProvider instance static ALWrapperSessionProvider *_wrapperSessionProvider; @@ -33,6 +35,9 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [registrar addMethodCallDelegate:instance channel:channel]; instance.registrar = registrar; + + NativeViewContainerFactory *factory = [[NativeViewContainerFactory alloc] init]; + [registrar registerViewFactory:factory withId:@"AnylineNativeView"]; } -(NSString * _Nullable)getStringFromArgument:(id) argument { @@ -65,6 +70,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result BOOL enableOfflineCache = [call.arguments[@"EXTRA_ENABLE_OFFLINE_CACHE"] boolValue] == true; [self initSdkWithLicenseKey:licenseKey sdkAssetsFolder:self.customModelsPath enableOfflineCache:enableOfflineCache]; + } else if ([@"METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS" isEqualToString:call.method]) { + [self setDefaultScanStartPlatformOptions:call.arguments[@"EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS"] + resultListener:result]; } else if ([@"METHOD_START_ANYLINE" isEqualToString:call.method]) { _startScanMethodResult = result; @@ -72,6 +80,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result scanViewInitializationParametersString:[self getStringFromArgument:call.arguments[@"EXTRA_INITIALIZATION_PARAMETERS"]] scanViewConfigPath:_viewConfigsPath scanCallbackConfigString:[self getStringFromArgument:call.arguments[@"EXTRA_SCAN_CALLBACK_CONFIG"]]]; + } else if ([@"METHOD_REPLACE_ANYLINE" isEqualToString:call.method]) { + [self requestScanSwitchWithScanViewConfigContentString:call.arguments[@"EXTRA_CONFIG_JSON"]]; } else if ([@"METHOD_STOP_ANYLINE" isEqualToString:call.method]) { [self tryStopScan:[self getStringFromArgument:call.arguments[@"EXTRA_STOP_CONFIG"]]]; } else if ([@"METHOD_GET_APPLICATION_CACHE_PATH" isEqualToString:call.method]) { @@ -146,6 +156,17 @@ - (void)initSdkWithLicenseKey:(NSString *)sdkLicenseKey requestSdkInitializationWithInitializationRequestParamsString:[wrapperSessionSdkInitializationRequestJson asJSONString]]; } +- (void)setDefaultScanStartPlatformOptions:(nullable NSString *)scanStartPlatformOptionsString + resultListener:(FlutterResult)resultListener { + @try { + [ALWrapperSessionProvider setDefaultScanStartPlatformOptionsWithString:scanStartPlatformOptionsString]; + resultListener(@""); + } + @catch (NSException *exception) { + resultListener(exception); + } +} + - (void)requestScanStartWithScanViewConfigContent:(NSString *)scanViewConfigContent scanViewInitializationParametersString:(NSString * _Nullable)scanViewInitializationParametersString scanViewConfigPath:(NSString * _Nullable)scanViewConfigPath @@ -172,6 +193,10 @@ - (void)requestScanStartWithScanViewConfigContent:(NSString *)scanViewConfigCont } } +- (void)requestScanSwitchWithScanViewConfigContentString:(NSString *)scanViewConfigContent { + [ALWrapperSessionProvider requestScanSwitchWithScanViewConfigContentString:scanViewConfigContent]; +} + - (void)tryStopScan:(NSString * _Nullable)scanStopRequestParams { [ALWrapperSessionProvider requestScanStopWithScanStopRequestParamsString:scanStopRequestParams]; } @@ -186,6 +211,10 @@ - (nullable UIViewController *)getTopViewController { return nil; } +- (nullable UIView *)getContainerView { + return [[NativeViewRegistry shared] getLastOrNull]; +} + - (void)onSdkInitializationResponse:(nonnull ALWrapperSessionSDKInitializationResponse *)initializationResponse { if (_initSdkMethodResult) { if ([initializationResponse.initialized isEqualToNumber:@YES]) { diff --git a/ios/Classes/NativeView/NativeViewContainer.h b/ios/Classes/NativeView/NativeViewContainer.h new file mode 100644 index 0000000..4ec471f --- /dev/null +++ b/ios/Classes/NativeView/NativeViewContainer.h @@ -0,0 +1,8 @@ + +#import + +@interface NativeViewContainer : NSObject +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args; +@end diff --git a/ios/Classes/NativeView/NativeViewContainer.m b/ios/Classes/NativeView/NativeViewContainer.m new file mode 100644 index 0000000..2315fc0 --- /dev/null +++ b/ios/Classes/NativeView/NativeViewContainer.m @@ -0,0 +1,37 @@ + +#import "NativeViewContainer.h" +#import "NativeViewRegistry.h" +#import + +@implementation NativeViewContainer { + UIView *_containerView; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + self = [super init]; + if (self) { + _containerView = [[UIView alloc] initWithFrame:frame]; + + //NSString *containerId = args[@"id"]; + NSString *containerId = [NSString stringWithFormat:@"%lld", viewId]; + [[NativeViewRegistry shared] registerView:_containerView withId:containerId]; + + UIViewController *nativeVC = [[UIViewController alloc] init]; + nativeVC.view.backgroundColor = [UIColor lightGrayColor]; + nativeVC.view.frame = _containerView.bounds; + + UIViewController *rootVC = UIApplication.sharedApplication.delegate.window.rootViewController; + [rootVC addChildViewController:nativeVC]; + [_containerView addSubview:nativeVC.view]; + [nativeVC didMoveToParentViewController:rootVC]; + } + return self; +} + +- (UIView *)view { + return _containerView; +} + +@end diff --git a/ios/Classes/NativeView/NativeViewContainerFactory.h b/ios/Classes/NativeView/NativeViewContainerFactory.h new file mode 100644 index 0000000..215e7ab --- /dev/null +++ b/ios/Classes/NativeView/NativeViewContainerFactory.h @@ -0,0 +1,5 @@ + +#import + +@interface NativeViewContainerFactory : NSObject +@end diff --git a/ios/Classes/NativeView/NativeViewContainerFactory.m b/ios/Classes/NativeView/NativeViewContainerFactory.m new file mode 100644 index 0000000..2493bd3 --- /dev/null +++ b/ios/Classes/NativeView/NativeViewContainerFactory.m @@ -0,0 +1,17 @@ + +#import "NativeViewContainerFactory.h" +#import "NativeViewContainer.h" + +@implementation NativeViewContainerFactory + +- (NSObject *)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject *)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[NativeViewContainer alloc] initWithFrame:frame viewIdentifier:viewId arguments:args]; +} + +@end diff --git a/ios/Classes/NativeView/NativeViewRegistry.h b/ios/Classes/NativeView/NativeViewRegistry.h new file mode 100644 index 0000000..6e88ea5 --- /dev/null +++ b/ios/Classes/NativeView/NativeViewRegistry.h @@ -0,0 +1,12 @@ + +#import +#import + +@interface NativeViewRegistry : NSObject + ++ (instancetype)shared; +- (void)registerView:(UIView *)view withId:(NSString *)viewId; +- (UIView *)getViewById:(NSString *)viewId; +- (UIView *)getLastOrNull; + +@end diff --git a/ios/Classes/NativeView/NativeViewRegistry.m b/ios/Classes/NativeView/NativeViewRegistry.m new file mode 100644 index 0000000..de74c6c --- /dev/null +++ b/ios/Classes/NativeView/NativeViewRegistry.m @@ -0,0 +1,38 @@ + +#import "NativeViewRegistry.h" + +@interface NativeViewRegistry () +@property(nonatomic, strong) NSMapTable *views; +@end + +@implementation NativeViewRegistry + ++ (instancetype)shared { + static NativeViewRegistry *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[NativeViewRegistry alloc] init]; + instance.views = [NSMapTable strongToWeakObjectsMapTable]; + }); + return instance; +} + +- (void)registerView:(UIView *)view withId:(NSString *)viewId { + [self.views setObject:view forKey:viewId]; +} + +- (UIView *)getViewById:(NSString *)viewId { + return [self.views objectForKey:viewId]; +} + +- (UIView *)getLastOrNull { + NSArray *allKeys = self.views.keyEnumerator.allObjects; + if (allKeys.count > 0) { + NSString *lastKey = [allKeys lastObject]; + return [self.views objectForKey:lastKey]; + } else { + return nil; + } +} + +@end diff --git a/ios/anyline_plugin.podspec b/ios/anyline_plugin.podspec index 59020dc..7831694 100644 --- a/ios/anyline_plugin.podspec +++ b/ios/anyline_plugin.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'anyline_plugin' - s.version = '55.8.1' + s.version = '55.9.0' s.summary = 'Anyline SDK' s.description = <<-DESC Anyline OCR Module @@ -16,7 +16,7 @@ Anyline OCR Module s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.dependency 'Anyline', '55.8.1' + s.dependency 'Anyline', '55.9.0' s.static_framework = true s.platform = :ios, '12.0' s.ios.deployment_target = '12.0' diff --git a/lib/anyline_native_view.dart b/lib/anyline_native_view.dart new file mode 100644 index 0000000..acf29bb --- /dev/null +++ b/lib/anyline_native_view.dart @@ -0,0 +1,51 @@ +import 'package:anyline_plugin/constants.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +class AnylineNativeView extends StatelessWidget { + const AnylineNativeView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return PlatformViewLink( + viewType: Constants.ANYLINE_NATIVE_VIEW_FACTORY_ID, + surfaceFactory: (context, controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (params) { + return PlatformViewsService.initExpensiveAndroidView( + id: params.id, + viewType: Constants.ANYLINE_NATIVE_VIEW_FACTORY_ID, + layoutDirection: TextDirection.ltr, + creationParams: {'id': 'scan-view-container-id'}, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + case TargetPlatform.iOS: + return const Center( + child: UiKitView( + viewType: Constants.ANYLINE_NATIVE_VIEW_FACTORY_ID, + creationParams: {'id': 'scan-view-container-id'}, + creationParamsCodec: StandardMessageCodec(), + ), + ); + + default: + return const SizedBox.shrink(); + } + } +} diff --git a/lib/anyline_plugin.dart b/lib/anyline_plugin.dart index b95149e..f7c5375 100644 --- a/lib/anyline_plugin.dart +++ b/lib/anyline_plugin.dart @@ -71,6 +71,20 @@ class AnylinePlugin { } } + Future setDefaultScanStartPlatformOptions( + String? scanStartPlatformOptionsString) async { + try { + final Map params = { + Constants.EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS: + scanStartPlatformOptionsString + }; + await _channel.invokeMethod( + Constants.METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS, params); + } on PlatformException catch (e) { + throw AnylineException.parse(e); + } + } + /// Starts the Anyline SDK and invokes the scanning process with the given [configJson]. /// /// Returns the result as a JSON string which can be parsed into an object of @@ -97,6 +111,13 @@ class AnylinePlugin { } } + void trySwitchScan(String configJson) { + final Map params = { + Constants.EXTRA_CONFIG_JSON: configJson + }; + _channel.invokeMethod(Constants.METHOD_REPLACE_ANYLINE, params); + } + void tryStopScan([String? scanStopRequestParams]) { final Map params = { Constants.EXTRA_STOP_CONFIG: scanStopRequestParams diff --git a/lib/constants.dart b/lib/constants.dart index 9cbfe4f..d513526 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -8,7 +8,10 @@ abstract class Constants { static const String METHOD_SET_VIEW_CONFIGS_PATH = 'METHOD_SET_VIEW_CONFIGS_PATH'; static const String METHOD_SET_LICENSE_KEY = 'METHOD_SET_LICENSE_KEY'; + static const String METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS = + 'METHOD_SET_DEFAULT_SCAN_START_PLATFORM_OPTIONS'; static const String METHOD_START_ANYLINE = 'METHOD_START_ANYLINE'; + static const String METHOD_REPLACE_ANYLINE = 'METHOD_REPLACE_ANYLINE'; static const String METHOD_GET_APPLICATION_CACHE_PATH = 'METHOD_GET_APPLICATION_CACHE_PATH'; static const String METHOD_STOP_ANYLINE = 'METHOD_STOP_ANYLINE'; @@ -24,6 +27,8 @@ abstract class Constants { static const String EXTRA_PLUGIN_VERSION = 'EXTRA_PLUGIN_VERSION'; static const String EXTRA_CUSTOM_MODELS_PATH = 'EXTRA_CUSTOM_MODELS_PATH'; static const String EXTRA_VIEW_CONFIGS_PATH = 'EXTRA_VIEW_CONFIGS_PATH'; + static const String EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS = + 'EXTRA_DEFAULT_SCAN_START_PLATFORM_OPTIONS'; static const String EXTRA_SCAN_CALLBACK_CONFIG = 'EXTRA_SCAN_CALLBACK_CONFIG'; static const String EXTRA_STOP_CONFIG = 'EXTRA_STOP_CONFIG'; static const String EXTRA_REPORT_UCR_BLOBKEY = 'EXTRA_REPORT_UCR_BLOBKEY'; @@ -36,4 +41,6 @@ abstract class Constants { static const String EXCEPTION_NO_CAMERA_PERMISSION = 'AnylineCameraPermissionException'; static const String EXCEPTION_CORE = 'AnylineCoreException'; + + static const String ANYLINE_NATIVE_VIEW_FACTORY_ID = 'AnylineNativeView'; } diff --git a/pubspec.yaml b/pubspec.yaml index ea26dba..0bb610b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: anyline_plugin description: Flutter Plugin for Anyline OCR, which allows you to scan all kinds of numbers, characters, text and codes. -version: 55.8.1 +version: 55.9.0 homepage: https://anyline.com repository: https://github.com/Anyline/anyline-ocr-flutter-module documentation: https://documentation.anyline.com