diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 4cad657..0000000 --- a/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset=utf-8 -end_of_line=lf -insert_final_newline=true -indent_style=space -indent_size=2 \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrDetector.java b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrDetector.java index 7d085f5..2980cc5 100644 --- a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrDetector.java +++ b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrDetector.java @@ -21,77 +21,77 @@ */ class QrDetector implements OnSuccessListener>, OnFailureListener { - private static final String TAG = "cgr.qrmv.QrDetector"; - private final QrReaderCallbacks communicator; - private final BarcodeScanner detector; + private static final String TAG = "cgr.qrmv.QrDetector"; + private final QrReaderCallbacks communicator; + private final BarcodeScanner detector; - public interface Frame { - InputImage toImage(); + public interface Frame { + InputImage toImage(); - void close(); - } + void close(); + } - @GuardedBy("this") - private Frame latestFrame; + @GuardedBy("this") + private Frame latestFrame; - @GuardedBy("this") - private Frame processingFrame; + @GuardedBy("this") + private Frame processingFrame; - QrDetector(QrReaderCallbacks communicator, BarcodeScannerOptions options) { - this.communicator = communicator; - this.detector = BarcodeScanning.getClient(options); - } + QrDetector(QrReaderCallbacks communicator, BarcodeScannerOptions options) { + this.communicator = communicator; + this.detector = BarcodeScanning.getClient(options); + } - void detect(Frame frame) { - if (latestFrame != null) latestFrame.close(); - latestFrame = frame; + void detect(Frame frame) { + if (latestFrame != null) latestFrame.close(); + latestFrame = frame; - if (processingFrame == null) { - processLatest(); - } + if (processingFrame == null) { + processLatest(); } - - private synchronized void processLatest() { - if (processingFrame != null) processingFrame.close(); - processingFrame = latestFrame; - latestFrame = null; - if (processingFrame != null) { - processFrame(processingFrame); - } + } + + private synchronized void processLatest() { + if (processingFrame != null) processingFrame.close(); + processingFrame = latestFrame; + latestFrame = null; + if (processingFrame != null) { + processFrame(processingFrame); } - - private void processFrame(Frame frame) { - InputImage image; - try { - image = frame.toImage(); - } catch (IllegalStateException ex) { - // ignore state exception from making frame to image - // as the image may be closed already. - return; - } - - if (image != null) { - detector.process(image) - .addOnSuccessListener(this) - .addOnFailureListener(this) - .addOnCompleteListener((Task> firebaseVisionBarcodes) -> { - // regardless of failure or success, close the previous frame - // and process the next one. - frame.close(); - processLatest();; - }); - } + } + + private void processFrame(Frame frame) { + InputImage image; + try { + image = frame.toImage(); + } catch (IllegalStateException ex) { + // ignore state exception from making frame to image + // as the image may be closed already. + return; } - @Override - public void onSuccess(List firebaseVisionBarcodes) { - for (Barcode barcode : firebaseVisionBarcodes) { - communicator.qrRead(barcode.getRawValue()); - } + if (image != null) { + detector.process(image) + .addOnSuccessListener(this) + .addOnFailureListener(this) + .addOnCompleteListener((Task> firebaseVisionBarcodes) -> { + // regardless of failure or success, close the previous frame + // and process the next one. + frame.close(); + processLatest(); + }); } + } - @Override - public void onFailure(@NonNull Exception e) { - Log.w(TAG, "Barcode Reading Failure: ", e); + @Override + public void onSuccess(List firebaseVisionBarcodes) { + for (Barcode barcode : firebaseVisionBarcodes) { + communicator.qrRead(new ScannedData(barcode.getCornerPoints(), barcode.getBoundingBox(), barcode.getRawValue())); } + } + + @Override + public void onFailure(@NonNull Exception e) { + Log.w(TAG, "Barcode Reading Failure: ", e); + } } diff --git a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrMobileVisionPlugin.java b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrMobileVisionPlugin.java index 954aa6a..c355fd0 100644 --- a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrMobileVisionPlugin.java +++ b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrMobileVisionPlugin.java @@ -4,6 +4,7 @@ import android.app.Activity; import android.app.Application; import android.content.pm.PackageManager; +import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.NonNull; @@ -185,8 +186,8 @@ public void onMethodCall(MethodCall methodCall, @NonNull Result result) { } @Override - public void qrRead(String data) { - channel.invokeMethod("qrRead", data); + public void qrRead(ScannedData data) { + channel.invokeMethod("qrRead", data.geJson()); } @Override diff --git a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrReaderCallbacks.java b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrReaderCallbacks.java index 1396822..66a1072 100644 --- a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrReaderCallbacks.java +++ b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/QrReaderCallbacks.java @@ -1,5 +1,6 @@ package com.github.rmtmckenzie.qr_mobile_vision; + public interface QrReaderCallbacks { - void qrRead(String data); + void qrRead(ScannedData data); } diff --git a/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/ScannedData.java b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/ScannedData.java new file mode 100644 index 0000000..d90757e --- /dev/null +++ b/android/src/main/java/com/github/rmtmckenzie/qr_mobile_vision/ScannedData.java @@ -0,0 +1,39 @@ +package com.github.rmtmckenzie.qr_mobile_vision; + +import android.graphics.Point; +import android.graphics.Rect; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ScannedData { + private final Point[] corners; + private final String rawValue; + + private final Rect boundingBox; + + ScannedData(Point[] corners, Rect boundingBox, String rawValue) { + this.corners = corners; + this.rawValue = rawValue; + this.boundingBox = boundingBox; + } + + Map geJson() { + HashMap result = new HashMap<>(); + List> points = new ArrayList<>(); + for (Point corner : corners) { + List point = new ArrayList<>(); + point.add(corner.x); + point.add(corner.y); + points.add(point); + } + result.put("corners", points); + result.put("rawValue", rawValue); + result.put("width", boundingBox.width()); + result.put("height", boundingBox.height()); + return result; + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index e6b5516..c47ebdc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,7 +29,7 @@ class MyApp extends StatefulWidget { } class MyAppState extends State { - String? qr; + BarcodeData? qr; bool camState = false; bool dirState = false; @@ -64,34 +64,43 @@ class MyAppState extends State { child: camState ? Center( child: SizedBox( - width: 300.0, - height: 600.0, - child: QrCamera( - onError: (context, error) => Text( - error.toString(), - style: const TextStyle(color: Colors.red), - ), - cameraDirection: dirState ? CameraDirection.FRONT : CameraDirection.BACK, - qrCodeCallback: (code) { - setState(() { - qr = code; - }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.all( - color: Colors.orange, - width: 10.0, - style: BorderStyle.solid, + width: double.infinity, + height: 400.0, + child: Stack( + children: [ + Positioned.fill( + child: QrCamera( + timeout: 1000, + detectionSpeed: QrDetectionSpeed.unrestricted, + onError: (context, error) => Text( + error.toString(), + style: const TextStyle(color: Colors.red), + ), + cameraDirection: dirState ? CameraDirection.FRONT : CameraDirection.BACK, + qrCodeCallback: (code) { + debugPrint(code.toString()); + setState(() { + qr = code; + }); + }, ), ), - ), + if (qr != null) + Positioned( + left: qr!.getLeft, + top: qr!.getTop, + child: Container( + width: qr!.barcodeSize.width, + height: qr!.barcodeSize.height, + decoration: BoxDecoration(border: Border.all()), + ), + ), + ], ), ), ) : const Center(child: Text("Camera inactive"))), - Text("QRCODE: $qr"), + Text("QRCODE: ${qr?.rawValue}"), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 15d9d30..4c7ef1b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.0" boolean_selector: @@ -14,7 +14,7 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" characters: @@ -22,7 +22,7 @@ packages: description: name: characters sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" clock: @@ -30,7 +30,7 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" collection: @@ -38,23 +38,23 @@ packages: description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.18.0" device_info_plus: dependency: transitive description: name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.dev" + sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95 + url: "https://pub.flutter-io.cn" source: hosted - version: "10.1.2" + version: "11.1.1" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" fake_async: @@ -62,7 +62,7 @@ packages: description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" ffi: @@ -70,7 +70,7 @@ packages: description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" file: @@ -78,7 +78,7 @@ packages: description: name: file sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.0" flutter: @@ -96,7 +96,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" flutter_test: @@ -124,7 +124,7 @@ packages: description: name: leak_tracker sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.0.5" leak_tracker_flutter_testing: @@ -132,7 +132,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.5" leak_tracker_testing: @@ -140,7 +140,7 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" lints: @@ -148,7 +148,7 @@ packages: description: name: lints sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" matcher: @@ -156,7 +156,7 @@ packages: description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.16+1" material_color_utilities: @@ -164,7 +164,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.11.1" meta: @@ -172,7 +172,7 @@ packages: description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.15.0" native_device_orientation: @@ -180,7 +180,7 @@ packages: description: name: native_device_orientation sha256: "0c330c068575e4be72cce5968ca479a3f8d5d1e5dfce7d89d5c13a1e943b338c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" path: @@ -188,7 +188,7 @@ packages: description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" platform: @@ -196,7 +196,7 @@ packages: description: name: platform sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.5" plugin_platform_interface: @@ -204,7 +204,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" process: @@ -212,7 +212,7 @@ packages: description: name: process sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.2" qr_mobile_vision: @@ -221,7 +221,7 @@ packages: path: ".." relative: true source: path - version: "5.0.1" + version: "5.0.2" sky_engine: dependency: transitive description: flutter @@ -232,7 +232,7 @@ packages: description: name: source_span sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stack_trace: @@ -240,7 +240,7 @@ packages: description: name: stack_trace sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" stream_channel: @@ -248,7 +248,7 @@ packages: description: name: stream_channel sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" string_scanner: @@ -256,7 +256,7 @@ packages: description: name: string_scanner sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" sync_http: @@ -264,7 +264,7 @@ packages: description: name: sync_http sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.1" term_glyph: @@ -272,7 +272,7 @@ packages: description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" test_api: @@ -280,7 +280,7 @@ packages: description: name: test_api sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.2" vector_math: @@ -288,7 +288,7 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" vm_service: @@ -296,7 +296,7 @@ packages: description: name: vm_service sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "14.2.5" web: @@ -304,7 +304,7 @@ packages: description: name: web sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" webdriver: @@ -312,23 +312,23 @@ packages: description: name: webdriver sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" - url: "https://pub.dev" + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.5.5" + version: "5.8.0" win32_registry: dependency: transitive description: name: win32_registry sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.5" sdks: diff --git a/lib/qr_camera.dart b/lib/qr_camera.dart index 92b0071..1f47e0e 100644 --- a/lib/qr_camera.dart +++ b/lib/qr_camera.dart @@ -1,3 +1,5 @@ export 'package:qr_mobile_vision/src/barcode_formats.dart'; export 'package:qr_mobile_vision/src/camera_direction.dart'; export 'package:qr_mobile_vision/src/qr_camera.dart'; +export 'package:qr_mobile_vision/src/barcode_data.dart'; +export 'package:qr_mobile_vision/src/exceptions.dart'; diff --git a/lib/qr_mobile_vision.dart b/lib/qr_mobile_vision.dart index 729739e..87242ef 100644 --- a/lib/qr_mobile_vision.dart +++ b/lib/qr_mobile_vision.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:qr_mobile_vision/src/barcode_data.dart'; import 'package:qr_mobile_vision/src/barcode_formats.dart'; import 'package:qr_mobile_vision/src/camera_direction.dart'; import 'package:qr_mobile_vision/src/preview_details.dart'; @@ -15,7 +16,7 @@ class QrMobileVision { static Future start({ required int width, required int height, - required ValueChanged qrCodeHandler, + required ValueChanged qrCodeHandler, CameraDirection cameraDirection = CameraDirection.BACK, List? formats = defaultBarcodeFormats, }) async { diff --git a/lib/src/barcode_data.dart b/lib/src/barcode_data.dart new file mode 100644 index 0000000..8970ec9 --- /dev/null +++ b/lib/src/barcode_data.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; + +final class BarcodeData { + final String? rawValue; + final Size barcodeSize; + final List corners; + final Size cameraSize; + final BoxFit boxFit; + + const BarcodeData({ + required this.corners, + required this.rawValue, + required this.barcodeSize, + required this.cameraSize, + required this.boxFit, + }); + + factory BarcodeData.fromNative(Map data) { + final rawValue = data['rawValue']; + final width = data['width']; + final height = data['height']; + final barcodeSize = width is num && height is num ? Size(width.toDouble(), height.toDouble()) : Size.zero; + final List corners; + final items = data['corners']; + if (items is! List) { + corners = []; + } else { + corners = [ + for (int i = 0; i < items.length; i++) + Offset(((items[i] as List).first as num).toDouble(), ((items[i] as List).last as num).toDouble()) + ]; + } + return BarcodeData( + corners: corners, + rawValue: rawValue, + barcodeSize: barcodeSize, + cameraSize: Size.zero, + boxFit: BoxFit.none, + ); + } + + double get getLeft => corners.isEmpty ? 0 : corners.first.dx; + + double get getTop => corners.isEmpty ? 0 : corners.first.dy; + + @override + String toString() => 'Raw Value:$rawValue\nbarcodeSize: $barcodeSize\nCameraSize: $cameraSize\nCorners: ${corners.join(', ')}'; +} diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart new file mode 100644 index 0000000..fa7f740 --- /dev/null +++ b/lib/src/exceptions.dart @@ -0,0 +1,37 @@ +import 'package:flutter/services.dart'; + +sealed class QrException implements Exception { + const QrException(); + + factory QrException.fromError(Object? error) { + switch (error) { + case PlatformException(code: final code): + if (code == 'ALREADY_RUNNING') return AlreadyRunningException(); + if (code == 'QRREADER_ERROR') return CameraPermissionException(); + if (code == 'noHardware') return NoCameraException(); + return UnknownException(); + default: + return UnknownException(); + } + } +} + +/// ALREADY_RUNNING +final class AlreadyRunningException extends QrException { + const AlreadyRunningException(); +} + +/// QRREADER_ERROR +final class CameraPermissionException extends QrException { + const CameraPermissionException(); +} + +/// noHardware +final class NoCameraException extends QrException { + const NoCameraException(); +} + +/// OTHERS +final class UnknownException extends QrException { + const UnknownException(); +} diff --git a/lib/src/qr_camera.dart b/lib/src/qr_camera.dart index c46eba1..e564d00 100644 --- a/lib/src/qr_camera.dart +++ b/lib/src/qr_camera.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:qr_mobile_vision/qr_mobile_vision.dart'; +import 'package:qr_mobile_vision/src/barcode_data.dart'; +import 'package:qr_mobile_vision/src/exceptions.dart'; import 'package:qr_mobile_vision/src/preview.dart'; import 'package:qr_mobile_vision/src/preview_details.dart'; @@ -12,14 +14,59 @@ Widget _defaultOnError(BuildContext context, Object? error) { return const Text("Error reading from camera..."); } -typedef ErrorCallback = Widget Function(BuildContext context, Object? error); +typedef ErrorCallback = Widget Function(BuildContext context, QrException error); + +class ScannerController { + final _controller = StreamController.broadcast(); + + void restart() { + if (_controller.isClosed) return; + _controller.add(null); + } + + void dispose() => _controller.close(); + + Stream get stream => _controller.stream; +} + +enum QrDetectionSpeed { + noDuplicates, + unrestricted; +} + +class _Throttler { + Timer? _timer; + bool _isRunning = false; + final Duration duration; + + _Throttler(this.duration); + + void run(void Function() func) { + if (_isRunning) return; + _isRunning = true; + dispose(); + func(); + _timer = Timer(duration, () { + _isRunning = false; + dispose(); + }); + } + + void dispose() { + _timer?.cancel(); + _timer = null; + } +} class QrCamera extends StatefulWidget { const QrCamera({ super.key, required this.qrCodeCallback, + this.controller, this.child, this.fit = BoxFit.cover, + this.detectionSpeed = QrDetectionSpeed.noDuplicates, + this.timeout = 250, WidgetBuilder? notStartedBuilder, WidgetBuilder? offscreenBuilder, ErrorCallback? onError, @@ -29,14 +76,17 @@ class QrCamera extends StatefulWidget { offscreenBuilder = offscreenBuilder ?? notStartedBuilder ?? _defaultOffscreenBuilder, onError = onError ?? _defaultOnError; + final QrDetectionSpeed detectionSpeed; + final ScannerController? controller; final BoxFit fit; - final ValueChanged qrCodeCallback; + final ValueChanged qrCodeCallback; final Widget? child; final WidgetBuilder notStartedBuilder; final WidgetBuilder offscreenBuilder; final ErrorCallback onError; final List? formats; final CameraDirection cameraDirection; + final int timeout; static toggleFlash() { QrMobileVision.toggleFlash(); @@ -49,11 +99,19 @@ class QrCamera extends StatefulWidget { class QrCameraState extends State with WidgetsBindingObserver { // needed for flutter < 3.0 to still be supported T? _ambiguate(T? value) => value; - + String? _lastScannedValue; + StreamSubscription? _sub; + late final _throttler = _Throttler(Duration(milliseconds: widget.timeout)); @override void initState() { super.initState(); _ambiguate(WidgetsBinding.instance)!.addObserver(this); + if (widget.controller != null) { + _sub?.cancel(); + _sub = widget.controller?.stream.listen((_) { + restart(); + }); + } } @override @@ -65,6 +123,9 @@ class QrCameraState extends State with WidgetsBindingObserver { @override dispose() { _ambiguate(WidgetsBinding.instance)!.removeObserver(this); + _throttler.dispose(); + _sub?.cancel(); + _sub = null; super.dispose(); } @@ -97,12 +158,12 @@ class QrCameraState extends State with WidgetsBindingObserver { bool onScreen = true; Future? _asyncInitOnce; - Future _asyncInit(num width, num height) async { + Future _asyncInit(double width, double height) async { final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; return await QrMobileVision.start( - width: (devicePixelRatio * width.toInt()).ceil(), - height: (devicePixelRatio * height.toInt()).ceil(), - qrCodeHandler: widget.qrCodeCallback, + width: (devicePixelRatio * width).ceil(), + height: (devicePixelRatio * height).ceil(), + qrCodeHandler: (value) => _qrCodeHandler(value, Size(width, height)), formats: widget.formats, cameraDirection: widget.cameraDirection, ); @@ -115,6 +176,7 @@ class QrCameraState extends State with WidgetsBindingObserver { await QrMobileVision.stop(); setState(() { _asyncInitOnce = null; + _lastScannedValue = null; }); })(); } @@ -141,7 +203,6 @@ class QrCameraState extends State with WidgetsBindingObserver { } else if (!onScreen) { return widget.offscreenBuilder(context); } - return FutureBuilder( future: _asyncInitOnce, builder: (BuildContext context, AsyncSnapshot details) { @@ -152,7 +213,7 @@ class QrCameraState extends State with WidgetsBindingObserver { case ConnectionState.done: if (details.hasError) { debugPrint(details.error.toString()); - return widget.onError(context, details.error); + return widget.onError(context, QrException.fromError(details.error)); } Widget preview = SizedBox( width: constraints.maxWidth, @@ -182,4 +243,31 @@ class QrCameraState extends State with WidgetsBindingObserver { ); }); } + + void _qrCodeHandler(BarcodeData barcode, Size cameraSize) async { + switch (widget.detectionSpeed) { + case QrDetectionSpeed.noDuplicates: + if (_lastScannedValue != null && barcode.rawValue == _lastScannedValue) return; + break; + case QrDetectionSpeed.unrestricted: + break; + } + _throttler.run(() { + _lastScannedValue = barcode.rawValue; + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + widget.qrCodeCallback(barcode._normalize(devicePixelRatio, cameraSize, widget.fit)); + }); + } +} + +extension _BarcodeDataExt on BarcodeData { + BarcodeData _normalize(double pr, Size cameraSize, BoxFit boxFit) { + return BarcodeData( + corners: corners.map((e) => e / pr).toList(), + rawValue: rawValue, + barcodeSize: barcodeSize / pr, + cameraSize: cameraSize, + boxFit: boxFit, + ); + } } diff --git a/lib/src/qr_channel_reader.dart b/lib/src/qr_channel_reader.dart index 403388a..4c2699f 100644 --- a/lib/src/qr_channel_reader.dart +++ b/lib/src/qr_channel_reader.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:qr_mobile_vision/src/barcode_data.dart'; class QrChannelReader { QrChannelReader(this.channel) { @@ -7,8 +8,9 @@ class QrChannelReader { switch (call.method) { case 'qrRead': if (qrCodeHandler != null) { - assert(call.arguments is String); - qrCodeHandler!(call.arguments); + final data = call.arguments; + assert(data is Map); + qrCodeHandler!(BarcodeData.fromNative(data)); } break; default: @@ -18,10 +20,10 @@ class QrChannelReader { }); } - void setQrCodeHandler(ValueChanged? qrch) { + void setQrCodeHandler(ValueChanged? qrch) { qrCodeHandler = qrch; } MethodChannel channel; - ValueChanged? qrCodeHandler; + ValueChanged? qrCodeHandler; } diff --git a/lib/src/qr_mobile_vision_method_channel.dart b/lib/src/qr_mobile_vision_method_channel.dart index 0a6ecd4..44b0b4a 100644 --- a/lib/src/qr_mobile_vision_method_channel.dart +++ b/lib/src/qr_mobile_vision_method_channel.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:qr_mobile_vision/src/barcode_data.dart'; import 'package:qr_mobile_vision/src/barcode_formats.dart'; import 'package:qr_mobile_vision/src/camera_direction.dart'; import 'package:qr_mobile_vision/src/preview_details.dart'; @@ -22,7 +23,7 @@ class MethodChannelQrMobileVision extends QrMobileVisionPlatform { Future start({ required int width, required int height, - required ValueChanged qrCodeHandler, + required ValueChanged qrCodeHandler, CameraDirection cameraDirection = CameraDirection.BACK, List? formats = defaultBarcodeFormats, }) async { diff --git a/lib/src/qr_mobile_vision_platform_interface.dart b/lib/src/qr_mobile_vision_platform_interface.dart index edbf3c3..a1d4b1d 100644 --- a/lib/src/qr_mobile_vision_platform_interface.dart +++ b/lib/src/qr_mobile_vision_platform_interface.dart @@ -5,6 +5,8 @@ import 'package:qr_mobile_vision/src/camera_direction.dart'; import 'package:qr_mobile_vision/src/preview_details.dart'; import 'package:qr_mobile_vision/src/qr_mobile_vision_method_channel.dart'; +import 'barcode_data.dart'; + abstract class QrMobileVisionPlatform extends PlatformInterface { /// Constructs a QrMobileVisionPlatform. QrMobileVisionPlatform() : super(token: _token); @@ -29,7 +31,7 @@ abstract class QrMobileVisionPlatform extends PlatformInterface { Future start({ required int width, required int height, - required ValueChanged qrCodeHandler, + required ValueChanged qrCodeHandler, CameraDirection cameraDirection = CameraDirection.BACK, List? formats = defaultBarcodeFormats, }); diff --git a/test/qr_mobile_vision_test.dart b/test/qr_mobile_vision_test.dart index 395b246..161b211 100644 --- a/test/qr_mobile_vision_test.dart +++ b/test/qr_mobile_vision_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:qr_mobile_vision/qr_mobile_vision.dart'; +import 'package:qr_mobile_vision/src/barcode_data.dart'; import 'package:qr_mobile_vision/src/preview_details.dart'; import 'package:qr_mobile_vision/src/qr_mobile_vision_method_channel.dart'; import 'package:qr_mobile_vision/src/qr_mobile_vision_platform_interface.dart'; @@ -18,7 +19,7 @@ class MockQrMobileVisionPlatform with MockPlatformInterfaceMixin implements QrMo Future start({ required int width, required int height, - required ValueChanged qrCodeHandler, + required ValueChanged qrCodeHandler, CameraDirection cameraDirection = CameraDirection.BACK, List? formats = defaultBarcodeFormats, }) async { @@ -62,7 +63,7 @@ void main() { }); test('start', () async { - handler(String? code) => print(code); + handler(BarcodeData code) => print(code); final details = await QrMobileVision.start( width: 100, height: 100,