Julian Steenbakker
Committed by GitHub

Merge pull request #399 from juliansteenbakker/feature/zoom

feat: add zoomScale api for android and iOS
@@ -39,6 +39,8 @@ class AlreadyStopped : Exception() @@ -39,6 +39,8 @@ class AlreadyStopped : Exception()
39 class TorchError : Exception() 39 class TorchError : Exception()
40 class CameraError : Exception() 40 class CameraError : Exception()
41 class TorchWhenStopped : Exception() 41 class TorchWhenStopped : Exception()
  42 +class ZoomWhenStopped : Exception()
  43 +class ZoomNotInRange : Exception()
42 44
43 class MobileScanner( 45 class MobileScanner(
44 private val activity: Activity, 46 private val activity: Activity,
@@ -353,4 +355,13 @@ class MobileScanner( @@ -353,4 +355,13 @@ class MobileScanner(
353 } 355 }
354 } 356 }
355 357
  358 + /**
  359 + * Set the zoom rate of the camera.
  360 + */
  361 + fun setScale(scale: Double) {
  362 + if (camera == null) throw ZoomWhenStopped()
  363 + if (scale > 1.0 || scale < 0) throw ZoomNotInRange()
  364 + camera!!.cameraControl.setLinearZoom(scale.toFloat())
  365 + }
  366 +
356 } 367 }
@@ -79,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -79,6 +79,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
79 "torch" -> toggleTorch(call, result) 79 "torch" -> toggleTorch(call, result)
80 "stop" -> stop(result) 80 "stop" -> stop(result)
81 "analyzeImage" -> analyzeImage(call, result) 81 "analyzeImage" -> analyzeImage(call, result)
  82 + "setScale" -> setScale(call, result)
82 "updateScanWindow" -> updateScanWindow(call) 83 "updateScanWindow" -> updateScanWindow(call)
83 else -> result.notImplemented() 84 else -> result.notImplemented()
84 } 85 }
@@ -219,6 +220,17 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -219,6 +220,17 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
219 } 220 }
220 } 221 }
221 222
  223 + private fun setScale(call: MethodCall, result: MethodChannel.Result) {
  224 + try {
  225 + handler!!.setScale(call.arguments as Double)
  226 + result.success(null)
  227 + } catch (e: ZoomWhenStopped) {
  228 + result.error("MobileScanner", "Called setScale() while stopped!", null)
  229 + } catch (e: ZoomNotInRange) {
  230 + result.error("MobileScanner", "Scale should be within 0 and 1", null)
  231 + }
  232 + }
  233 +
222 private fun updateScanWindow(call: MethodCall) { 234 private fun updateScanWindow(call: MethodCall) {
223 handler!!.scanWindow = call.argument<List<Float>>("rect") 235 handler!!.scanWindow = call.argument<List<Float>>("rect")
224 } 236 }
  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 + );
  19 +
  20 + bool isStarted = true;
  21 + double _zoomFactor = 0.0;
  22 +
  23 + @override
  24 + Widget build(BuildContext context) {
  25 + return Scaffold(
  26 + backgroundColor: Colors.black,
  27 + body: Builder(
  28 + builder: (context) {
  29 + return Stack(
  30 + children: [
  31 + MobileScanner(
  32 + controller: controller,
  33 + fit: BoxFit.contain,
  34 + onDetect: (barcode) {
  35 + setState(() {
  36 + this.barcode = barcode;
  37 + });
  38 + },
  39 + ),
  40 + Align(
  41 + alignment: Alignment.bottomCenter,
  42 + child: Container(
  43 + alignment: Alignment.bottomCenter,
  44 + height: 100,
  45 + color: Colors.black.withOpacity(0.4),
  46 + child: Column(
  47 + children: [
  48 + Slider(
  49 + value: _zoomFactor,
  50 + onChanged: (value) {
  51 + setState(() {
  52 + _zoomFactor = value;
  53 + controller.setZoomScale(value);
  54 + });
  55 + },
  56 + ),
  57 + Row(
  58 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  59 + children: [
  60 + IconButton(
  61 + color: Colors.white,
  62 + icon: ValueListenableBuilder(
  63 + valueListenable: controller.torchState,
  64 + builder: (context, state, child) {
  65 + if (state == null) {
  66 + return const Icon(
  67 + Icons.flash_off,
  68 + color: Colors.grey,
  69 + );
  70 + }
  71 + switch (state as TorchState) {
  72 + case TorchState.off:
  73 + return const Icon(
  74 + Icons.flash_off,
  75 + color: Colors.grey,
  76 + );
  77 + case TorchState.on:
  78 + return const Icon(
  79 + Icons.flash_on,
  80 + color: Colors.yellow,
  81 + );
  82 + }
  83 + },
  84 + ),
  85 + iconSize: 32.0,
  86 + onPressed: () => controller.toggleTorch(),
  87 + ),
  88 + IconButton(
  89 + color: Colors.white,
  90 + icon: isStarted
  91 + ? const Icon(Icons.stop)
  92 + : const Icon(Icons.play_arrow),
  93 + iconSize: 32.0,
  94 + onPressed: () => setState(() {
  95 + isStarted
  96 + ? controller.stop()
  97 + : controller.start();
  98 + isStarted = !isStarted;
  99 + }),
  100 + ),
  101 + Center(
  102 + child: SizedBox(
  103 + width: MediaQuery.of(context).size.width - 200,
  104 + height: 50,
  105 + child: FittedBox(
  106 + child: Text(
  107 + barcode?.barcodes.first.rawValue ??
  108 + 'Scan something!',
  109 + overflow: TextOverflow.fade,
  110 + style: Theme.of(context)
  111 + .textTheme
  112 + .headline4!
  113 + .copyWith(color: Colors.white),
  114 + ),
  115 + ),
  116 + ),
  117 + ),
  118 + IconButton(
  119 + color: Colors.white,
  120 + icon: ValueListenableBuilder(
  121 + valueListenable: controller.cameraFacingState,
  122 + builder: (context, state, child) {
  123 + if (state == null) {
  124 + return const Icon(Icons.camera_front);
  125 + }
  126 + switch (state as CameraFacing) {
  127 + case CameraFacing.front:
  128 + return const Icon(Icons.camera_front);
  129 + case CameraFacing.back:
  130 + return const Icon(Icons.camera_rear);
  131 + }
  132 + },
  133 + ),
  134 + iconSize: 32.0,
  135 + onPressed: () => controller.switchCamera(),
  136 + ),
  137 + IconButton(
  138 + color: Colors.white,
  139 + icon: const Icon(Icons.image),
  140 + iconSize: 32.0,
  141 + onPressed: () async {
  142 + final ImagePicker picker = ImagePicker();
  143 + // Pick an image
  144 + final XFile? image = await picker.pickImage(
  145 + source: ImageSource.gallery,
  146 + );
  147 + if (image != null) {
  148 + if (await controller.analyzeImage(image.path)) {
  149 + if (!mounted) return;
  150 + ScaffoldMessenger.of(context).showSnackBar(
  151 + const SnackBar(
  152 + content: Text('Barcode found!'),
  153 + backgroundColor: Colors.green,
  154 + ),
  155 + );
  156 + } else {
  157 + if (!mounted) return;
  158 + ScaffoldMessenger.of(context).showSnackBar(
  159 + const SnackBar(
  160 + content: Text('No barcode found!'),
  161 + backgroundColor: Colors.red,
  162 + ),
  163 + );
  164 + }
  165 + }
  166 + },
  167 + ),
  168 + ],
  169 + ),
  170 + ],
  171 + ),
  172 + ),
  173 + ),
  174 + ],
  175 + );
  176 + },
  177 + ),
  178 + );
  179 + }
  180 +}
@@ -4,6 +4,7 @@ import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; @@ -4,6 +4,7 @@ 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_window.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_window.dart';
6 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 6 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
  7 +import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
7 8
8 void main() => runApp(const MaterialApp(home: MyHome())); 9 void main() => runApp(const MaterialApp(home: MyHome()));
9 10
@@ -73,6 +74,16 @@ class MyHome extends StatelessWidget { @@ -73,6 +74,16 @@ class MyHome extends StatelessWidget {
73 }, 74 },
74 child: const Text('MobileScanner without Controller'), 75 child: const Text('MobileScanner without Controller'),
75 ), 76 ),
  77 + ElevatedButton(
  78 + onPressed: () {
  79 + Navigator.of(context).push(
  80 + MaterialPageRoute(
  81 + builder: (context) => const BarcodeScannerWithZoom(),
  82 + ),
  83 + );
  84 + },
  85 + child: const Text('MobileScanner with zoom slider'),
  86 + ),
76 ], 87 ],
77 ), 88 ),
78 ), 89 ),
@@ -222,6 +222,33 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -222,6 +222,33 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
222 } 222 }
223 } 223 }
224 224
  225 + /// Set the zoom factor of the camera
  226 + func setScale(_ scale: CGFloat) throws {
  227 + if (device == nil) {
  228 + throw MobileScannerError.torchWhenStopped
  229 + }
  230 +
  231 + do {
  232 + try device.lockForConfiguration()
  233 + var maxZoomFactor = device.activeFormat.videoMaxZoomFactor
  234 +
  235 + var actualScale = (scale * 4) + 1
  236 +
  237 + // Set maximum zoomrate of 5x
  238 + actualScale = min(5.0, actualScale)
  239 +
  240 + // Limit to max rate of camera
  241 + actualScale = min(maxZoomFactor, actualScale)
  242 +
  243 + // Limit to 1.0 scale
  244 + device.ramp(toVideoZoomFactor: actualScale, withRate: 5)
  245 + device.unlockForConfiguration()
  246 + } catch {
  247 + throw MobileScannerError.zoomError(error)
  248 + }
  249 +
  250 + }
  251 +
225 /// Analyze a single image 252 /// Analyze a single image
226 func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { 253 func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
227 let image = VisionImage(image: image) 254 let image = VisionImage(image: image)
@@ -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 }
@@ -83,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -83,6 +83,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
83 toggleTorch(call, result) 83 toggleTorch(call, result)
84 case "analyzeImage": 84 case "analyzeImage":
85 analyzeImage(call, result) 85 analyzeImage(call, result)
  86 + case "setScale":
  87 + setScale(call, result)
86 case "updateScanWindow": 88 case "updateScanWindow":
87 updateScanWindow(call, result) 89 updateScanWindow(call, result)
88 default: 90 default:
@@ -159,6 +161,33 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -159,6 +161,33 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
159 result(nil) 161 result(nil)
160 } 162 }
161 163
  164 + /// Toggles the zoomScale
  165 + private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  166 + var scale = call.arguments as? CGFloat
  167 + if (scale == nil) {
  168 + result(FlutterError(code: "MobileScanner",
  169 + message: "You must provide a scale when calling setScale!",
  170 + details: nil))
  171 + return
  172 + }
  173 + do {
  174 + try mobileScanner.setScale(scale!)
  175 + } catch MobileScannerError.zoomWhenStopped {
  176 + result(FlutterError(code: "MobileScanner",
  177 + message: "Called setScale() while stopped!",
  178 + details: nil))
  179 + } catch MobileScannerError.zoomError(let error) {
  180 + result(FlutterError(code: "MobileScanner",
  181 + message: "Error while zooming.",
  182 + details: error))
  183 + } catch {
  184 + result(FlutterError(code: "MobileScanner",
  185 + message: "Error while zooming.",
  186 + details: nil))
  187 + }
  188 + result(nil)
  189 + }
  190 +
162 /// Toggles the torch 191 /// Toggles the torch
163 func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 192 func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
164 let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat] 193 let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat]
@@ -295,6 +295,22 @@ class MobileScannerController { @@ -295,6 +295,22 @@ class MobileScannerController {
295 .then<bool>((bool? value) => value ?? false); 295 .then<bool>((bool? value) => value ?? false);
296 } 296 }
297 297
  298 + /// Set the zoomScale of the camera.
  299 + ///
  300 + /// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0
  301 + /// is zoomed out.
  302 + Future<void> setZoomScale(double zoomScale) async {
  303 + if (zoomScale < 0 || zoomScale > 1) {
  304 + throw const MobileScannerException(
  305 + errorCode: MobileScannerErrorCode.genericError,
  306 + errorDetails: MobileScannerErrorDetails(
  307 + message: 'The zoomScale must be between 0 and 1.',
  308 + ),
  309 + );
  310 + }
  311 + await _methodChannel.invokeMethod('setScale', zoomScale);
  312 + }
  313 +
298 /// Disposes the MobileScannerController and closes all listeners. 314 /// Disposes the MobileScannerController and closes all listeners.
299 /// 315 ///
300 /// If you call this, you cannot use this controller object anymore. 316 /// If you call this, you cannot use this controller object anymore.