Skip to content

Commit 0ae802a

Browse files
Merge pull request juliansteenbakker#1456 from juliansteenbakker/feat/focus-zoom-lens
feat: tap to focus, initial zoom and more lenses
2 parents e0f9f5a + d95e3ad commit 0ae802a

File tree

14 files changed

+322
-54
lines changed

14 files changed

+322
-54
lines changed

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
## NEXT
22

3+
**BREAKING CHANGES**
4+
5+
* [iOS] Increased minimum iOS level to 13 due to Flutter requirements.
6+
7+
**Highlights**
8+
9+
* [Apple & Android] Added tap to focus functionality. You can enable it in the `MobileScanner` widget.
10+
* [Apple & Android] You can now set the initial zoom factor using the `initialZoom` parameter in the `startOptions`.
11+
12+
**Bug Fixes and Improvements**
13+
314
* [Android] Update to Java 17, and update other dependencies.
4-
* [iOS] Upgraded minimum iOS level to 13 due to Flutter requirements.
15+
* [Apple] Improved fallback for when camera is not found.
516

617
## 7.0.1
718

@@ -11,7 +22,7 @@
1122

1223
This version finalizes all changes from the beta and release candidate cycles and introduces major improvements, bug fixes, and breaking changes.
1324

14-
**BREAKING CHANGES:**
25+
**BREAKING CHANGES**
1526

1627
* Requires Flutter 3.29.0 or higher.
1728
* The initial camera facing direction in `MobileScannerState` is now `CameraFacing.unknown`.

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ import androidx.camera.core.CameraSelector
2323
import androidx.camera.core.CameraXConfig
2424
import androidx.camera.core.ExperimentalGetImage
2525
import androidx.camera.core.ExperimentalLensFacing
26+
import androidx.camera.core.FocusMeteringAction
2627
import androidx.camera.core.ImageAnalysis
2728
import androidx.camera.core.ImageProxy
29+
import androidx.camera.core.MeteringPoint
30+
import androidx.camera.core.MeteringPointFactory
2831
import androidx.camera.core.Preview
32+
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
2933
import androidx.camera.core.SurfaceRequest
3034
import androidx.camera.core.TorchState
3135
import androidx.camera.core.resolutionselector.ResolutionSelector
@@ -343,6 +347,7 @@ class MobileScanner(
343347
detectionTimeout: Long,
344348
cameraResolutionWanted: Size?,
345349
invertImage: Boolean,
350+
initialZoom: Double,
346351
) {
347352
this.detectionSpeed = detectionSpeed
348353
this.detectionTimeout = detectionTimeout
@@ -471,6 +476,18 @@ class MobileScanner(
471476
if (it.cameraInfo.hasFlashUnit()) {
472477
it.cameraControl.enableTorch(torch)
473478
}
479+
480+
try {
481+
if (initialZoom in 0.0..1.0) {
482+
it.cameraControl.setLinearZoom(initialZoom.toFloat())
483+
} else {
484+
it.cameraControl.setZoomRatio(initialZoom.toFloat())
485+
}
486+
} catch (e: Exception) {
487+
mobileScannerErrorCallback(ZoomNotInRange())
488+
489+
return@addListener
490+
}
474491
}
475492

476493
val resolution = analysis.resolutionInfo!!.resolution
@@ -701,6 +718,23 @@ class MobileScanner(
701718
camera?.cameraControl?.setZoomRatio(1f)
702719
}
703720

721+
fun setFocus(x: Float, y: Float) {
722+
val cam = camera ?: throw ZoomWhenStopped()
723+
724+
// Ensure x,y are normalized (0f..1f)
725+
if (x !in 0f..1f || y !in 0f..1f) {
726+
throw IllegalArgumentException("Focus coordinates must be between 0.0 and 1.0")
727+
}
728+
729+
val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory(1f, 1f)
730+
val afPoint: MeteringPoint = factory.createPoint(x, y)
731+
732+
val action = FocusMeteringAction.Builder(afPoint, FocusMeteringAction.FLAG_AF)
733+
.build()
734+
735+
cam.cameraControl.startFocusAndMetering(action)
736+
}
737+
704738
/**
705739
* Dispose of this scanner instance.
706740
*/

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class MobileScannerHandler(
151151
"setScale" -> setScale(call, result)
152152
"resetScale" -> resetScale(result)
153153
"updateScanWindow" -> updateScanWindow(call, result)
154+
"setFocus" -> setFocus(call, result)
154155
else -> result.notImplemented()
155156
}
156157
}
@@ -172,6 +173,7 @@ class MobileScannerHandler(
172173
null
173174
}
174175
val invertImage: Boolean = call.argument<Boolean>("invertImage") ?: false
176+
val initialZoom: Double = call.argument<Double>("initialZoom") ?: 1.0
175177

176178
val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats, autoZoom)
177179

@@ -243,6 +245,7 @@ class MobileScannerHandler(
243245
timeout.toLong(),
244246
cameraResolution,
245247
invertImage,
248+
initialZoom
246249
)
247250
}
248251

@@ -327,7 +330,7 @@ class MobileScannerHandler(
327330
private fun buildBarcodeScannerOptions(formats: List<Int>?, autoZoom: Boolean): BarcodeScannerOptions? {
328331
val builder : BarcodeScannerOptions.Builder?
329332
if (formats == null) {
330-
builder = BarcodeScannerOptions.Builder()
333+
builder = BarcodeScannerOptions.Builder()
331334
} else {
332335
val formatsList: MutableList<Int> = mutableListOf()
333336

@@ -374,4 +377,36 @@ class MobileScannerHandler(
374377
}
375378
return maxZoom
376379
}
380+
381+
private fun setFocus(call: MethodCall, result: MethodChannel.Result) {
382+
val dx = call.argument<Double>("dx")?.toFloat()
383+
val dy = call.argument<Double>("dy")?.toFloat()
384+
385+
if (dx == null || dy == null || dx !in 0f..1f || dy !in 0f..1f) {
386+
result.error(
387+
MobileScannerErrorCodes.INVALID_FOCUS_POINT,
388+
MobileScannerErrorCodes.INVALID_FOCUS_POINT_MESSAGE,
389+
null
390+
)
391+
return
392+
}
393+
394+
try {
395+
mobileScanner?.setFocus(dx, dy)
396+
result.success(null)
397+
} catch (e: ZoomWhenStopped) {
398+
result.error(
399+
MobileScannerErrorCodes.GENERIC_ERROR,
400+
"Cannot set focus when camera is stopped.",
401+
null
402+
)
403+
} catch (e: Exception) {
404+
result.error(
405+
MobileScannerErrorCodes.GENERIC_ERROR,
406+
MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
407+
e.localizedMessage
408+
)
409+
}
410+
}
411+
377412
}

android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerErrorCodes.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ class MobileScannerErrorCodes {
2323
const val SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR"
2424
const val SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped."
2525
const val UNSUPPORTED_OPERATION_ERROR = "MOBILE_SCANNER_UNSUPPORTED_OPERATION" // Reserved for future use.
26+
const val INVALID_FOCUS_POINT = "MOBILE_SCANNER_INVALID_FOCUS_POINT"
27+
const val INVALID_FOCUS_POINT_MESSAGE = "The focus coordinates are not valid."
2628
}
2729
}

darwin/mobile_scanner/Sources/mobile_scanner/MobileScannerErrorCodes.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ struct MobileScannerErrorCodes {
2424
static let GENERIC_ERROR_MESSAGE = "An unknown error occurred."
2525
// This message is used with the 'GENERIC_ERROR' error code.
2626
static let INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)"
27+
static let INVALID_FOCUS_POINT = "MOBILE_SCANNER_INVALID_FOCUS_POINT"
28+
static let INVALID_FOCUS_POINT_MESSAGE = "The focus coordinates are not valid."
2729
static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
2830
static let NO_CAMERA_ERROR_MESSAGE = "No cameras available."
2931
static let SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR"

darwin/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
101101
toggleTorch(result)
102102
case "setScale":
103103
setScale(call, result)
104+
case "setFocus":
105+
setFocus(call, result)
104106
case "resetScale":
105107
resetScale(call, result)
106108
case "pause":
@@ -346,6 +348,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
346348
let facing:Int = argReader.int(key: "facing") ?? 1
347349
let speed:Int = argReader.int(key: "speed") ?? 0
348350
let timeoutMs:Int = argReader.int(key: "timeout") ?? 0
351+
let initialZoom: CGFloat = CGFloat(argReader.float(key: "initialZoom") ?? 1)
349352
symbologies = argReader.toSymbology()
350353
MobileScannerPlugin.returnImage = argReader.bool(key: "returnImage") ?? false
351354

@@ -360,12 +363,24 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
360363
#endif
361364

362365
// Open the camera device
366+
#if os(iOS)
367+
if #available(iOS 13.0, *) {
368+
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInTripleCamera, .builtInDualCamera, .builtInWideAngleCamera], mediaType: .video, position: position).devices.first
369+
}
370+
#else
363371
if #available(macOS 10.15, *) {
364372
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first
365-
} else {
373+
}
374+
#endif
375+
376+
if (device == nil) {
366377
device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first
367378
}
368379

380+
if (device == nil) {
381+
device = AVCaptureDevice.default(for: .video)
382+
}
383+
369384
if (device == nil) {
370385
result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
371386
message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
@@ -380,7 +395,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
380395
captureSession!.beginConfiguration()
381396

382397
// Check the zoom factor at switching from ultra wide camera to wide camera.
383-
standardZoomFactor = 1
398+
standardZoomFactor = initialZoom
384399
#if os(iOS)
385400
if #available(iOS 13.0, *) {
386401
for (index, actualDevice) in device.constituentDevices.enumerated() {
@@ -455,6 +470,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
455470
if (torch) {
456471
self.turnTorchOn()
457472
}
473+
474+
// Set the initial zoom factor
475+
do {
476+
try self.setScaleInternal(initialZoom)
477+
} catch {
478+
// Do nothing.
479+
}
458480

459481
#if os(iOS)
460482
// The height and width are swapped because the default video orientation for ios is landscape right, but mobile_scanner operates in portrait mode.
@@ -586,6 +608,43 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
586608

587609
}
588610

611+
private func setFocus(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
612+
guard let args = call.arguments as? [String: Any],
613+
let dx = args["dx"] as? CGFloat,
614+
let dy = args["dy"] as? CGFloat else {
615+
result(FlutterError(code: MobileScannerErrorCodes.INVALID_FOCUS_POINT,
616+
message: MobileScannerErrorCodes.INVALID_FOCUS_POINT_MESSAGE,
617+
details: nil))
618+
return
619+
}
620+
let focusPoint = CGPoint(x: dx, y: dy)
621+
622+
do {
623+
if (device == nil) {
624+
throw MobileScannerError.zoomWhenStopped
625+
}
626+
627+
#if os(iOS)
628+
if device.isFocusPointOfInterestSupported {
629+
do {
630+
try device.lockForConfiguration()
631+
device.focusPointOfInterest = focusPoint
632+
device.focusMode = .autoFocus
633+
device.unlockForConfiguration()
634+
} catch {
635+
throw MobileScannerError.zoomError(error)
636+
}
637+
}
638+
#endif
639+
640+
result(nil)
641+
} catch {
642+
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
643+
message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
644+
details: nil))
645+
}
646+
}
647+
589648
#if os(iOS)
590649
/// Set the device orientation if it differs from previous orientation
591650
func setDeviceOrientation(orientation: UIDeviceOrientation) {
@@ -891,6 +950,10 @@ class MapArgumentReader {
891950
func int(key: String) -> Int? {
892951
return (args?[key] as? NSNumber)?.intValue
893952
}
953+
954+
func float(key: String) -> Float? {
955+
return (args?[key] as? NSNumber)?.floatValue
956+
}
894957

895958
func bool(key: String) -> Bool? {
896959
return (args?[key] as? NSNumber)?.boolValue

example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 60;
6+
objectVersion = 54;
77
objects = {
88

99
/* Begin PBXBuildFile section */

0 commit comments

Comments
 (0)