Julian Steenbakker

feat: add zoomScale api for android and iOS

@@ -33,6 +33,8 @@ class AlreadyStopped : Exception() @@ -33,6 +33,8 @@ class AlreadyStopped : Exception()
33 class TorchError : Exception() 33 class TorchError : Exception()
34 class CameraError : Exception() 34 class CameraError : Exception()
35 class TorchWhenStopped : Exception() 35 class TorchWhenStopped : Exception()
  36 +class ZoomWhenStopped : Exception()
  37 +class ZoomNotInRange : Exception()
36 38
37 class MobileScanner( 39 class MobileScanner(
38 private val activity: Activity, 40 private val activity: Activity,
@@ -312,4 +314,13 @@ class MobileScanner( @@ -312,4 +314,13 @@ class MobileScanner(
312 } 314 }
313 } 315 }
314 316
  317 + /**
  318 + * Set the zoom rate of the camera.
  319 + */
  320 + fun setScale(scale: Double) {
  321 + if (camera == null) throw ZoomWhenStopped()
  322 + if (scale > 1.0 || scale < 0) throw ZoomNotInRange()
  323 + camera!!.cameraControl.setLinearZoom(scale.toFloat())
  324 + }
  325 +
315 } 326 }
@@ -83,6 +83,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -83,6 +83,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
83 "torch" -> toggleTorch(call, result) 83 "torch" -> toggleTorch(call, result)
84 "stop" -> stop(result) 84 "stop" -> stop(result)
85 "analyzeImage" -> analyzeImage(call, result) 85 "analyzeImage" -> analyzeImage(call, result)
  86 + "setScale" -> setScale(call, result)
86 else -> result.notImplemented() 87 else -> result.notImplemented()
87 } 88 }
88 } 89 }
@@ -226,4 +227,15 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -226,4 +227,15 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
226 result.error("MobileScanner", "Called toggleTorch() while stopped!", null) 227 result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
227 } 228 }
228 } 229 }
  230 +
  231 + private fun setScale(call: MethodCall, result: MethodChannel.Result) {
  232 + try {
  233 + handler!!.setScale(call.arguments as Double)
  234 + result.success(null)
  235 + } catch (e: ZoomWhenStopped) {
  236 + result.error("MobileScanner", "Called setScale() while stopped!", null)
  237 + } catch (e: ZoomNotInRange) {
  238 + result.error("MobileScanner", "Scale should be within 0 and 1", null)
  239 + }
  240 + }
229 } 241 }
  1 +import 'package:flutter/material.dart';
  2 +import 'package:image_picker/image_picker.dart';
  3 +import 'package:mobile_scanner/mobile_scanner.dart';
  4 +
  5 +class BarcodeScannerWithZoom extends StatefulWidget {
  6 + const BarcodeScannerWithZoom({Key? key}) : super(key: key);
  7 +
  8 + @override
  9 + _BarcodeScannerWithZoomState createState() => _BarcodeScannerWithZoomState();
  10 +}
  11 +
  12 +class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
  13 + with SingleTickerProviderStateMixin {
  14 + BarcodeCapture? barcode;
  15 +
  16 + MobileScannerController controller = MobileScannerController(
  17 + torchEnabled: true,
  18 + onPermissionSet: (hasPermission) {
  19 + // Do something with permission callback
  20 + },
  21 + );
  22 +
  23 + bool isStarted = true;
  24 + double _zoomFactor = 0.0;
  25 +
  26 + @override
  27 + Widget build(BuildContext context) {
  28 + return Scaffold(
  29 + backgroundColor: Colors.black,
  30 + body: Builder(
  31 + builder: (context) {
  32 + return Stack(
  33 + children: [
  34 + MobileScanner(
  35 + controller: controller,
  36 + fit: BoxFit.contain,
  37 + onDetect: (barcode) {
  38 + setState(() {
  39 + this.barcode = barcode;
  40 + });
  41 + },
  42 + ),
  43 + Align(
  44 + alignment: Alignment.bottomCenter,
  45 + child: Container(
  46 + alignment: Alignment.bottomCenter,
  47 + height: 100,
  48 + color: Colors.black.withOpacity(0.4),
  49 + child: Column(
  50 + children: [
  51 + Slider(
  52 + value: _zoomFactor,
  53 + onChanged: (value) {
  54 + setState(() {
  55 + _zoomFactor = value;
  56 + controller.setZoomScale(value);
  57 + });
  58 + },
  59 + ),
  60 + Row(
  61 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  62 + children: [
  63 + IconButton(
  64 + color: Colors.white,
  65 + icon: ValueListenableBuilder(
  66 + valueListenable: controller.torchState,
  67 + builder: (context, state, child) {
  68 + if (state == null) {
  69 + return const Icon(
  70 + Icons.flash_off,
  71 + color: Colors.grey,
  72 + );
  73 + }
  74 + switch (state as TorchState) {
  75 + case TorchState.off:
  76 + return const Icon(
  77 + Icons.flash_off,
  78 + color: Colors.grey,
  79 + );
  80 + case TorchState.on:
  81 + return const Icon(
  82 + Icons.flash_on,
  83 + color: Colors.yellow,
  84 + );
  85 + }
  86 + },
  87 + ),
  88 + iconSize: 32.0,
  89 + onPressed: () => controller.toggleTorch(),
  90 + ),
  91 + IconButton(
  92 + color: Colors.white,
  93 + icon: isStarted
  94 + ? const Icon(Icons.stop)
  95 + : const Icon(Icons.play_arrow),
  96 + iconSize: 32.0,
  97 + onPressed: () => setState(() {
  98 + isStarted
  99 + ? controller.stop()
  100 + : controller.start();
  101 + isStarted = !isStarted;
  102 + }),
  103 + ),
  104 + Center(
  105 + child: SizedBox(
  106 + width: MediaQuery.of(context).size.width - 200,
  107 + height: 50,
  108 + child: FittedBox(
  109 + child: Text(
  110 + barcode?.barcodes.first.rawValue ??
  111 + 'Scan something!',
  112 + overflow: TextOverflow.fade,
  113 + style: Theme.of(context)
  114 + .textTheme
  115 + .headline4!
  116 + .copyWith(color: Colors.white),
  117 + ),
  118 + ),
  119 + ),
  120 + ),
  121 + IconButton(
  122 + color: Colors.white,
  123 + icon: ValueListenableBuilder(
  124 + valueListenable: controller.cameraFacingState,
  125 + builder: (context, state, child) {
  126 + if (state == null) {
  127 + return const Icon(Icons.camera_front);
  128 + }
  129 + switch (state as CameraFacing) {
  130 + case CameraFacing.front:
  131 + return const Icon(Icons.camera_front);
  132 + case CameraFacing.back:
  133 + return const Icon(Icons.camera_rear);
  134 + }
  135 + },
  136 + ),
  137 + iconSize: 32.0,
  138 + onPressed: () => controller.switchCamera(),
  139 + ),
  140 + IconButton(
  141 + color: Colors.white,
  142 + icon: const Icon(Icons.image),
  143 + iconSize: 32.0,
  144 + onPressed: () async {
  145 + final ImagePicker picker = ImagePicker();
  146 + // Pick an image
  147 + final XFile? image = await picker.pickImage(
  148 + source: ImageSource.gallery,
  149 + );
  150 + if (image != null) {
  151 + if (await controller.analyzeImage(image.path)) {
  152 + if (!mounted) return;
  153 + ScaffoldMessenger.of(context).showSnackBar(
  154 + const SnackBar(
  155 + content: Text('Barcode found!'),
  156 + backgroundColor: Colors.green,
  157 + ),
  158 + );
  159 + } else {
  160 + if (!mounted) return;
  161 + ScaffoldMessenger.of(context).showSnackBar(
  162 + const SnackBar(
  163 + content: Text('No barcode found!'),
  164 + backgroundColor: Colors.red,
  165 + ),
  166 + );
  167 + }
  168 + }
  169 + },
  170 + ),
  171 + ],
  172 + ),
  173 + ],
  174 + ),
  175 + ),
  176 + ),
  177 + ],
  178 + );
  179 + },
  180 + ),
  181 + );
  182 + }
  183 +}
@@ -3,6 +3,7 @@ import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; @@ -3,6 +3,7 @@ import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
5 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
  6 +import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
6 7
7 void main() => runApp(const MaterialApp(home: MyHome())); 8 void main() => runApp(const MaterialApp(home: MyHome()));
8 9
@@ -62,6 +63,16 @@ class MyHome extends StatelessWidget { @@ -62,6 +63,16 @@ class MyHome extends StatelessWidget {
62 }, 63 },
63 child: const Text('MobileScanner without Controller'), 64 child: const Text('MobileScanner without Controller'),
64 ), 65 ),
  66 + ElevatedButton(
  67 + onPressed: () {
  68 + Navigator.of(context).push(
  69 + MaterialPageRoute(
  70 + builder: (context) => const BarcodeScannerWithZoom(),
  71 + ),
  72 + );
  73 + },
  74 + child: const Text('MobileScanner with zoom slider'),
  75 + ),
65 ], 76 ],
66 ), 77 ),
67 ), 78 ),
@@ -206,6 +206,33 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -206,6 +206,33 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
206 throw MobileScannerError.torchError(error) 206 throw MobileScannerError.torchError(error)
207 } 207 }
208 } 208 }
  209 +
  210 + /// Set the zoom factor of the camera
  211 + func setScale(_ scale: CGFloat) throws {
  212 + if (device == nil) {
  213 + throw MobileScannerError.torchWhenStopped
  214 + }
  215 +
  216 + do {
  217 + try device.lockForConfiguration()
  218 + var maxZoomFactor = device.activeFormat.videoMaxZoomFactor
  219 +
  220 + var actualScale = (scale * 4) + 1
  221 +
  222 + // Set maximum zoomrate of 5x
  223 + actualScale = min(5.0, actualScale)
  224 +
  225 + // Limit to max rate of camera
  226 + actualScale = min(maxZoomFactor, actualScale)
  227 +
  228 + // Limit to 1.0 scale
  229 + device.ramp(toVideoZoomFactor: actualScale, withRate: 5)
  230 + device.unlockForConfiguration()
  231 + } catch {
  232 + throw MobileScannerError.zoomError(error)
  233 + }
  234 +
  235 + }
209 236
210 /// Analyze a single image 237 /// Analyze a single image
211 func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { 238 func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
@@ -13,5 +13,7 @@ enum MobileScannerError: Error { @@ -13,5 +13,7 @@ enum MobileScannerError: Error {
13 case torchError(_ error: Error) 13 case torchError(_ error: Error)
14 case cameraError(_ error: Error) 14 case cameraError(_ error: Error)
15 case torchWhenStopped 15 case torchWhenStopped
  16 + case zoomWhenStopped
  17 + case zoomError(_ error: Error)
16 case analyzerError(_ error: Error) 18 case analyzerError(_ error: Error)
17 } 19 }
@@ -49,6 +49,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -49,6 +49,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
49 toggleTorch(call, result) 49 toggleTorch(call, result)
50 case "analyzeImage": 50 case "analyzeImage":
51 analyzeImage(call, result) 51 analyzeImage(call, result)
  52 + case "setScale":
  53 + setScale(call, result)
52 default: 54 default:
53 result(FlutterMethodNotImplemented) 55 result(FlutterMethodNotImplemented)
54 } 56 }
@@ -127,6 +129,33 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -127,6 +129,33 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
127 result(nil) 129 result(nil)
128 } 130 }
129 131
  132 + /// Toggles the zoomScale
  133 + private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  134 + var scale = call.arguments as? CGFloat
  135 + if (scale == nil) {
  136 + result(FlutterError(code: "MobileScanner",
  137 + message: "You must provide a scale when calling setScale!",
  138 + details: nil))
  139 + return
  140 + }
  141 + do {
  142 + try mobileScanner.setScale(scale!)
  143 + } catch MobileScannerError.zoomWhenStopped {
  144 + result(FlutterError(code: "MobileScanner",
  145 + message: "Called setScale() while stopped!",
  146 + details: nil))
  147 + } catch MobileScannerError.zoomError(let error) {
  148 + result(FlutterError(code: "MobileScanner",
  149 + message: "Error while zooming.",
  150 + details: error))
  151 + } catch {
  152 + result(FlutterError(code: "MobileScanner",
  153 + message: "Error while zooming.",
  154 + details: nil))
  155 + }
  156 + result(nil)
  157 + }
  158 +
130 /// Analyzes a single image 159 /// Analyzes a single image
131 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 160 private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
132 let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") 161 let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
@@ -249,6 +249,19 @@ class MobileScannerController { @@ -249,6 +249,19 @@ class MobileScannerController {
249 .then<bool>((bool? value) => value ?? false); 249 .then<bool>((bool? value) => value ?? false);
250 } 250 }
251 251
  252 + /// Set the zoomScale of the camera.
  253 + ///
  254 + /// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0
  255 + /// is zoomed out.
  256 + Future<void> setZoomScale(double zoomScale) async {
  257 + if (zoomScale < 0 || zoomScale > 1) {
  258 + throw MobileScannerException(
  259 + 'The zoomScale must be between 0 and 1.',
  260 + );
  261 + }
  262 + await _methodChannel.invokeMethod('setScale', zoomScale);
  263 + }
  264 +
252 /// Disposes the MobileScannerController and closes all listeners. 265 /// Disposes the MobileScannerController and closes all listeners.
253 /// 266 ///
254 /// If you call this, you cannot use this controller object anymore. 267 /// If you call this, you cannot use this controller object anymore.