Julian Steenbakker

feat: add zoomScale api for android and iOS

... ... @@ -33,6 +33,8 @@ class AlreadyStopped : Exception()
class TorchError : Exception()
class CameraError : Exception()
class TorchWhenStopped : Exception()
class ZoomWhenStopped : Exception()
class ZoomNotInRange : Exception()
class MobileScanner(
private val activity: Activity,
... ... @@ -312,4 +314,13 @@ class MobileScanner(
}
}
/**
* Set the zoom rate of the camera.
*/
fun setScale(scale: Double) {
if (camera == null) throw ZoomWhenStopped()
if (scale > 1.0 || scale < 0) throw ZoomNotInRange()
camera!!.cameraControl.setLinearZoom(scale.toFloat())
}
}
... ...
... ... @@ -83,6 +83,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"setScale" -> setScale(call, result)
else -> result.notImplemented()
}
}
... ... @@ -226,4 +227,15 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
private fun setScale(call: MethodCall, result: MethodChannel.Result) {
try {
handler!!.setScale(call.arguments as Double)
result.success(null)
} catch (e: ZoomWhenStopped) {
result.error("MobileScanner", "Called setScale() while stopped!", null)
} catch (e: ZoomNotInRange) {
result.error("MobileScanner", "Scale should be within 0 and 1", null)
}
}
}
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerWithZoom extends StatefulWidget {
const BarcodeScannerWithZoom({Key? key}) : super(key: key);
@override
_BarcodeScannerWithZoomState createState() => _BarcodeScannerWithZoomState();
}
class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
torchEnabled: true,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
);
bool isStarted = true;
double _zoomFactor = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Column(
children: [
Slider(
value: _zoomFactor,
onChanged: (value) {
setState(() {
_zoomFactor = value;
controller.setZoomScale(value);
});
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
if (state == null) {
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
}
switch (state as TorchState) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted
? controller.stop()
: controller.start();
isStarted = !isStarted;
}),
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
if (state == null) {
return const Icon(Icons.camera_front);
}
switch (state as CameraFacing) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
// Pick an image
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
),
);
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
),
);
}
}
},
),
],
),
],
),
),
),
],
);
},
),
);
}
}
... ...
... ... @@ -3,6 +3,7 @@ import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
void main() => runApp(const MaterialApp(home: MyHome()));
... ... @@ -62,6 +63,16 @@ class MyHome extends StatelessWidget {
},
child: const Text('MobileScanner without Controller'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerWithZoom(),
),
);
},
child: const Text('MobileScanner with zoom slider'),
),
],
),
),
... ...
... ... @@ -206,6 +206,33 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
throw MobileScannerError.torchError(error)
}
}
/// Set the zoom factor of the camera
func setScale(_ scale: CGFloat) throws {
if (device == nil) {
throw MobileScannerError.torchWhenStopped
}
do {
try device.lockForConfiguration()
var maxZoomFactor = device.activeFormat.videoMaxZoomFactor
var actualScale = (scale * 4) + 1
// Set maximum zoomrate of 5x
actualScale = min(5.0, actualScale)
// Limit to max rate of camera
actualScale = min(maxZoomFactor, actualScale)
// Limit to 1.0 scale
device.ramp(toVideoZoomFactor: actualScale, withRate: 5)
device.unlockForConfiguration()
} catch {
throw MobileScannerError.zoomError(error)
}
}
/// Analyze a single image
func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
... ...
... ... @@ -13,5 +13,7 @@ enum MobileScannerError: Error {
case torchError(_ error: Error)
case cameraError(_ error: Error)
case torchWhenStopped
case zoomWhenStopped
case zoomError(_ error: Error)
case analyzerError(_ error: Error)
}
... ...
... ... @@ -49,6 +49,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
toggleTorch(call, result)
case "analyzeImage":
analyzeImage(call, result)
case "setScale":
setScale(call, result)
default:
result(FlutterMethodNotImplemented)
}
... ... @@ -127,6 +129,33 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
result(nil)
}
/// Toggles the zoomScale
private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
var scale = call.arguments as? CGFloat
if (scale == nil) {
result(FlutterError(code: "MobileScanner",
message: "You must provide a scale when calling setScale!",
details: nil))
return
}
do {
try mobileScanner.setScale(scale!)
} catch MobileScannerError.zoomWhenStopped {
result(FlutterError(code: "MobileScanner",
message: "Called setScale() while stopped!",
details: nil))
} catch MobileScannerError.zoomError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
details: error))
} catch {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
details: nil))
}
result(nil)
}
/// Analyzes a single image
private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
... ...
... ... @@ -249,6 +249,19 @@ class MobileScannerController {
.then<bool>((bool? value) => value ?? false);
}
/// Set the zoomScale of the camera.
///
/// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0
/// is zoomed out.
Future<void> setZoomScale(double zoomScale) async {
if (zoomScale < 0 || zoomScale > 1) {
throw MobileScannerException(
'The zoomScale must be between 0 and 1.',
);
}
await _methodChannel.invokeMethod('setScale', zoomScale);
}
/// Disposes the MobileScannerController and closes all listeners.
///
/// If you call this, you cannot use this controller object anymore.
... ...