@cleanuidev/react-native-scanner 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +609 -0
- package/Scanner.podspec +20 -0
- package/android/build.gradle +90 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/scanner/CameraInfoModule.kt +253 -0
- package/android/src/main/java/com/scanner/ScannerPackage.kt +21 -0
- package/android/src/main/java/com/scanner/ScannerView.kt +783 -0
- package/android/src/main/java/com/scanner/ScannerViewManager.kt +181 -0
- package/android/src/main/java/com/scanner/utils/BarcodeFrameManager.kt +170 -0
- package/android/src/main/java/com/scanner/views/BarcodeFrameOverlayView.kt +43 -0
- package/android/src/main/java/com/scanner/views/FocusAreaView.kt +124 -0
- package/ios/BarcodeDetectionManager.swift +229 -0
- package/ios/BarcodeFrameManager.swift +175 -0
- package/ios/BarcodeFrameOverlayView.swift +102 -0
- package/ios/CameraManager.swift +396 -0
- package/ios/CoordinateTransformer.swift +140 -0
- package/ios/FocusAreaOverlayView.swift +161 -0
- package/ios/Models.swift +341 -0
- package/ios/Protocols.swift +194 -0
- package/ios/ScannerView.h +14 -0
- package/ios/ScannerView.mm +358 -0
- package/ios/ScannerViewImpl.swift +580 -0
- package/ios/react-native-scanner-Bridging-Header.h +26 -0
- package/lib/module/CameraInfoModule.js +8 -0
- package/lib/module/CameraInfoModule.js.map +1 -0
- package/lib/module/ScannerViewNativeComponent.ts +121 -0
- package/lib/module/hooks/useCameraInfo.js +106 -0
- package/lib/module/hooks/useCameraInfo.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +47 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/CameraInfoModule.d.ts +8 -0
- package/lib/typescript/src/CameraInfoModule.d.ts.map +1 -0
- package/lib/typescript/src/ScannerViewNativeComponent.d.ts +91 -0
- package/lib/typescript/src/ScannerViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useCameraInfo.d.ts +25 -0
- package/lib/typescript/src/hooks/useCameraInfo.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +145 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +178 -0
- package/src/CameraInfoModule.ts +11 -0
- package/src/ScannerViewNativeComponent.ts +121 -0
- package/src/hooks/useCameraInfo.ts +190 -0
- package/src/index.tsx +30 -0
- package/src/types.ts +177 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
package com.scanner
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.*
|
|
5
|
+
import android.os.PowerManager
|
|
6
|
+
import android.util.AttributeSet
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.util.Rational
|
|
9
|
+
import android.util.Size
|
|
10
|
+
import android.view.Surface
|
|
11
|
+
import android.view.View
|
|
12
|
+
import android.view.ViewGroup
|
|
13
|
+
import android.view.WindowManager
|
|
14
|
+
import android.widget.FrameLayout
|
|
15
|
+
import androidx.annotation.OptIn
|
|
16
|
+
import androidx.camera.core.*
|
|
17
|
+
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
18
|
+
import androidx.camera.view.PreviewView
|
|
19
|
+
import androidx.camera.camera2.interop.Camera2CameraControl
|
|
20
|
+
import androidx.camera.camera2.interop.CaptureRequestOptions
|
|
21
|
+
import android.hardware.camera2.CaptureRequest
|
|
22
|
+
import kotlin.math.max
|
|
23
|
+
import androidx.core.content.ContextCompat
|
|
24
|
+
import androidx.lifecycle.LifecycleOwner
|
|
25
|
+
import com.facebook.react.bridge.*
|
|
26
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
27
|
+
import com.google.mlkit.vision.barcode.BarcodeScanning
|
|
28
|
+
import com.google.mlkit.vision.barcode.common.Barcode
|
|
29
|
+
import com.google.mlkit.vision.common.InputImage
|
|
30
|
+
import java.util.concurrent.ExecutorService
|
|
31
|
+
import java.util.concurrent.Executors
|
|
32
|
+
import com.scanner.utils.BarcodeFrameManager
|
|
33
|
+
import com.scanner.views.FocusAreaView
|
|
34
|
+
import com.scanner.views.BarcodeFrameOverlayView
|
|
35
|
+
import androidx.core.graphics.toColorInt
|
|
36
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
37
|
+
|
|
38
|
+
class ScannerView : FrameLayout {
|
|
39
|
+
companion object {
|
|
40
|
+
const val TAG = "ScannerView"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private var cameraProvider: ProcessCameraProvider? = null
|
|
44
|
+
private var camera: androidx.camera.core.Camera? = null
|
|
45
|
+
private var imageAnalyzer: ImageAnalysis? = null
|
|
46
|
+
private var preview: Preview? = null
|
|
47
|
+
private var cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
48
|
+
private var barcodeScanner = BarcodeScanning.getClient()
|
|
49
|
+
private var previewView: PreviewView? = null
|
|
50
|
+
private var overlayView: FocusAreaView? = null
|
|
51
|
+
private var barcodeFrameOverlayView: BarcodeFrameOverlayView? = null
|
|
52
|
+
|
|
53
|
+
// Frame configuration
|
|
54
|
+
private var enableFrame: Boolean = false
|
|
55
|
+
private var borderColor: Int = Color.TRANSPARENT
|
|
56
|
+
private var tintColor: Int = Color.BLACK
|
|
57
|
+
private var frameSize: FrameSize = FrameSize.Square(300)
|
|
58
|
+
private var positionX: Float = 50f // Default center (50%)
|
|
59
|
+
private var positionY: Float = 50f // Default center (50%)
|
|
60
|
+
private var showBarcodeFramesOnlyInFrame: Boolean = false
|
|
61
|
+
|
|
62
|
+
// Focus area and barcode frames configuration
|
|
63
|
+
private var focusAreaEnabled: Boolean = false
|
|
64
|
+
private var barcodeFramesEnabled: Boolean = false
|
|
65
|
+
private var barcodeFramesColor: Int = Color.RED
|
|
66
|
+
|
|
67
|
+
// React context for event emission
|
|
68
|
+
private var reactContext: ThemedReactContext? = null
|
|
69
|
+
|
|
70
|
+
private var zoom: Float = 1.0f
|
|
71
|
+
private var torchEnabled: Boolean = false
|
|
72
|
+
private var isScanningPaused: Boolean = false
|
|
73
|
+
|
|
74
|
+
// Barcode scan strategy
|
|
75
|
+
private var barcodeScanStrategy: String = "ALL"
|
|
76
|
+
|
|
77
|
+
// Barcode frame management
|
|
78
|
+
private val barcodeFrameManager = BarcodeFrameManager()
|
|
79
|
+
|
|
80
|
+
// Wake lock and screen keep-awake
|
|
81
|
+
private var wakeLock: PowerManager.WakeLock? = null
|
|
82
|
+
private var keepScreenOn: Boolean = true
|
|
83
|
+
|
|
84
|
+
// Debounce mechanism to prevent multiple rapid barcode detections
|
|
85
|
+
private var lastBarcodeEmissionTime: Long = 0
|
|
86
|
+
private var barcodeEmissionDebounceInterval: Long = 500 // Default 500ms debounce
|
|
87
|
+
|
|
88
|
+
constructor(context: Context) : super(context) {
|
|
89
|
+
initScannerView(context)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
|
93
|
+
initScannerView(context)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
97
|
+
context,
|
|
98
|
+
attrs,
|
|
99
|
+
defStyleAttr
|
|
100
|
+
) {
|
|
101
|
+
initScannerView(context)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun initScannerView(context: Context) {
|
|
105
|
+
if (context is ThemedReactContext) {
|
|
106
|
+
reactContext = context
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Initialize wake lock
|
|
110
|
+
initializeWakeLock()
|
|
111
|
+
|
|
112
|
+
previewView = PreviewView(context).apply {
|
|
113
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
114
|
+
scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
115
|
+
implementationMode = PreviewView.ImplementationMode.PERFORMANCE
|
|
116
|
+
// setBackgroundColor(Color.RED) // Optional: for debugging layout
|
|
117
|
+
}
|
|
118
|
+
installHierarchyFitter(previewView!!)
|
|
119
|
+
addView(previewView)
|
|
120
|
+
|
|
121
|
+
// Create overlay view for frame drawing
|
|
122
|
+
overlayView = FocusAreaView(context).apply {
|
|
123
|
+
layoutParams = LayoutParams(
|
|
124
|
+
LayoutParams.MATCH_PARENT,
|
|
125
|
+
LayoutParams.MATCH_PARENT
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
addView(overlayView)
|
|
129
|
+
|
|
130
|
+
// Create overlay view for barcode frame drawing
|
|
131
|
+
barcodeFrameOverlayView = BarcodeFrameOverlayView(context).apply {
|
|
132
|
+
layoutParams = LayoutParams(
|
|
133
|
+
LayoutParams.MATCH_PARENT,
|
|
134
|
+
LayoutParams.MATCH_PARENT
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
addView(barcodeFrameOverlayView)
|
|
138
|
+
|
|
139
|
+
// Set up barcode frame manager
|
|
140
|
+
setupBarcodeFrameManager()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun setupBarcodeFrameManager() {
|
|
144
|
+
barcodeFrameManager.setOnFramesChangedListener {
|
|
145
|
+
// Update the overlay view with current frames
|
|
146
|
+
val currentFrames = barcodeFrameManager.getActiveFrames()
|
|
147
|
+
barcodeFrameOverlayView?.setBarcodeBoxes(currentFrames)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private fun initializeWakeLock() {
|
|
152
|
+
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
153
|
+
wakeLock = powerManager.newWakeLock(
|
|
154
|
+
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
155
|
+
"ScannerView::WakeLock"
|
|
156
|
+
)
|
|
157
|
+
wakeLock?.setReferenceCounted(false)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private fun acquireWakeLock() {
|
|
161
|
+
if (keepScreenOn && wakeLock?.isHeld == false) {
|
|
162
|
+
wakeLock?.acquire()
|
|
163
|
+
Log.d(TAG, "Wake lock acquired")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun releaseWakeLock() {
|
|
168
|
+
if (wakeLock?.isHeld == true) {
|
|
169
|
+
wakeLock?.release()
|
|
170
|
+
Log.d(TAG, "Wake lock released")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private fun updateKeepScreenOn(keepOn: Boolean) {
|
|
175
|
+
keepScreenOn = keepOn
|
|
176
|
+
if (keepOn) {
|
|
177
|
+
// Set FLAG_KEEP_SCREEN_ON on the activity window
|
|
178
|
+
val activity = reactContext?.currentActivity
|
|
179
|
+
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
180
|
+
acquireWakeLock()
|
|
181
|
+
} else {
|
|
182
|
+
// Remove FLAG_KEEP_SCREEN_ON from the activity window
|
|
183
|
+
val activity = reactContext?.currentActivity
|
|
184
|
+
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
185
|
+
releaseWakeLock()
|
|
186
|
+
}
|
|
187
|
+
Log.d(TAG, "Keep screen on: $keepOn")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fun installHierarchyFitter(view: ViewGroup) {
|
|
191
|
+
if (context is ThemedReactContext) { // only react-native setup
|
|
192
|
+
view.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
|
|
193
|
+
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
|
|
194
|
+
override fun onChildViewAdded(parent: View?, child: View?) {
|
|
195
|
+
parent?.measure(
|
|
196
|
+
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
|
|
197
|
+
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
|
|
198
|
+
)
|
|
199
|
+
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
override fun onAttachedToWindow() {
|
|
206
|
+
super.onAttachedToWindow()
|
|
207
|
+
Log.d(TAG, "View attached to window, starting camera directly")
|
|
208
|
+
startCamera()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override fun onDetachedFromWindow() {
|
|
212
|
+
super.onDetachedFromWindow()
|
|
213
|
+
stopCamera()
|
|
214
|
+
barcodeFrameManager.shutdown()
|
|
215
|
+
|
|
216
|
+
// Ensure wake lock is released when view is destroyed
|
|
217
|
+
releaseWakeLock()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private fun startCamera() {
|
|
221
|
+
Log.d(TAG, "Starting camera...")
|
|
222
|
+
|
|
223
|
+
if (!hasCameraPermission()) {
|
|
224
|
+
Log.e(TAG, "Camera permission not granted")
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Acquire wake lock when camera starts
|
|
229
|
+
updateKeepScreenOn(true)
|
|
230
|
+
|
|
231
|
+
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
|
232
|
+
cameraProviderFuture.addListener({
|
|
233
|
+
try {
|
|
234
|
+
cameraProvider = cameraProviderFuture.get()
|
|
235
|
+
Log.d(TAG, "CameraProvider obtained")
|
|
236
|
+
|
|
237
|
+
previewView?.post {
|
|
238
|
+
Log.d(TAG, "Binding camera use cases on UI thread")
|
|
239
|
+
bindCameraUseCases()
|
|
240
|
+
}
|
|
241
|
+
} catch (e: Exception) {
|
|
242
|
+
Log.e(TAG, "Failed to initialize camera", e)
|
|
243
|
+
}
|
|
244
|
+
// }, ContextCompat.getMainExecutor(context))
|
|
245
|
+
}, cameraExecutor)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private fun stopCamera() {
|
|
249
|
+
camera?.cameraControl?.cancelFocusAndMetering()
|
|
250
|
+
cameraProvider?.unbindAll()
|
|
251
|
+
cameraExecutor.shutdown()
|
|
252
|
+
barcodeScanner.close()
|
|
253
|
+
|
|
254
|
+
// Release wake lock when camera stops
|
|
255
|
+
updateKeepScreenOn(false)
|
|
256
|
+
|
|
257
|
+
Log.e(TAG, "Camera stopped.")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun bindCameraUseCases() {
|
|
261
|
+
val cameraProvider = cameraProvider ?: run {
|
|
262
|
+
Log.e(TAG, "Camera provider not available for binding.")
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
val lifecycleOwner = reactContext?.currentActivity as? LifecycleOwner ?: run {
|
|
266
|
+
Log.e(TAG, "No LifecycleOwner available for binding.")
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Unbind any previous use cases first
|
|
271
|
+
cameraProvider.unbindAll()
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
val surfaceProvider = previewView?.surfaceProvider
|
|
275
|
+
if (surfaceProvider == null) {
|
|
276
|
+
Log.e(TAG, "SurfaceProvider is null! Cannot bind.")
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Preview
|
|
281
|
+
preview = Preview.Builder()
|
|
282
|
+
.setTargetRotation(previewView?.display?.rotation ?: Surface.ROTATION_0)
|
|
283
|
+
.build()
|
|
284
|
+
.also {
|
|
285
|
+
it.setSurfaceProvider(surfaceProvider)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Image Analysis
|
|
289
|
+
imageAnalyzer = ImageAnalysis.Builder()
|
|
290
|
+
// .setTargetResolution(Size(1280, 720)) // review this later if needed
|
|
291
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
292
|
+
.build()
|
|
293
|
+
.also {
|
|
294
|
+
it.setAnalyzer(cameraExecutor) { imageProxy ->
|
|
295
|
+
processImage(imageProxy)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Select back camera as default
|
|
300
|
+
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
301
|
+
|
|
302
|
+
// Create a ViewPort that matches the PreviewView
|
|
303
|
+
val viewPort = ViewPort.Builder(
|
|
304
|
+
Rational(previewView!!.width, previewView!!.height),
|
|
305
|
+
previewView!!.display.rotation
|
|
306
|
+
)
|
|
307
|
+
.setScaleType(ViewPort.FILL_CENTER) // or FIT_CENTER as needed
|
|
308
|
+
.build()
|
|
309
|
+
|
|
310
|
+
// Create a UseCaseGroup with the ViewPort
|
|
311
|
+
val useCaseGroup = UseCaseGroup.Builder()
|
|
312
|
+
.addUseCase(preview!!)
|
|
313
|
+
.addUseCase(imageAnalyzer!!)
|
|
314
|
+
.setViewPort(viewPort)
|
|
315
|
+
.build()
|
|
316
|
+
|
|
317
|
+
// Bind use cases to camera
|
|
318
|
+
camera = cameraProvider.bindToLifecycle(
|
|
319
|
+
lifecycleOwner,
|
|
320
|
+
cameraSelector,
|
|
321
|
+
useCaseGroup
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Set up torch and zoom
|
|
325
|
+
camera?.cameraControl?.enableTorch(torchEnabled)
|
|
326
|
+
setZoom(zoom)
|
|
327
|
+
|
|
328
|
+
// Set up continuous autofocus for better barcode scanning
|
|
329
|
+
setupContinuousAutoFocus()
|
|
330
|
+
|
|
331
|
+
// Set up auto-focus on frame center (if focus area is enabled)
|
|
332
|
+
setupAutoFocusOnFrame()
|
|
333
|
+
|
|
334
|
+
} catch (exc: Exception) {
|
|
335
|
+
Log.e(TAG, "Use case binding failed", exc)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Set up continuous autofocus mode for better barcode scanning.
|
|
341
|
+
* This enables AF_MODE_CONTINUOUS_PICTURE which continuously adjusts focus,
|
|
342
|
+
* making it much easier to scan barcodes at different distances.
|
|
343
|
+
*/
|
|
344
|
+
private fun setupContinuousAutoFocus() {
|
|
345
|
+
try {
|
|
346
|
+
val camera2Control = camera?.cameraControl?.let {
|
|
347
|
+
Camera2CameraControl.from(it)
|
|
348
|
+
} ?: run {
|
|
349
|
+
Log.w(TAG, "Camera2CameraControl not available")
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Enable continuous autofocus mode
|
|
354
|
+
camera2Control.captureRequestOptions = CaptureRequestOptions.Builder()
|
|
355
|
+
.setCaptureRequestOption<Int>(
|
|
356
|
+
CaptureRequest.CONTROL_AF_MODE,
|
|
357
|
+
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|
|
358
|
+
)
|
|
359
|
+
.build()
|
|
360
|
+
|
|
361
|
+
Log.d(TAG, "✅ Continuous autofocus enabled (AF_MODE_CONTINUOUS_PICTURE)")
|
|
362
|
+
} catch (e: Exception) {
|
|
363
|
+
Log.e(TAG, "Failed to setup continuous autofocus", e)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Set up autofocus on the focus area frame center.
|
|
369
|
+
* This provides additional focus assistance when a focus area is defined.
|
|
370
|
+
*/
|
|
371
|
+
private fun setupAutoFocusOnFrame() {
|
|
372
|
+
if (!enableFrame) {
|
|
373
|
+
// Even without focus area overlay, trigger autofocus on screen center
|
|
374
|
+
triggerAutoFocusOnCenter()
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
val frame = overlayView?.frameRect ?: run {
|
|
379
|
+
triggerAutoFocusOnCenter()
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
val centerX = frame.centerX()
|
|
384
|
+
val centerY = frame.centerY()
|
|
385
|
+
|
|
386
|
+
val viewWidth = previewView?.width ?: return
|
|
387
|
+
val viewHeight = previewView?.height ?: return
|
|
388
|
+
|
|
389
|
+
val normalizedX = centerX / viewWidth
|
|
390
|
+
val normalizedY = centerY / viewHeight
|
|
391
|
+
|
|
392
|
+
triggerAutoFocusAt(normalizedX, normalizedY)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Trigger autofocus at screen center (useful when no focus area is defined)
|
|
397
|
+
*/
|
|
398
|
+
private fun triggerAutoFocusOnCenter() {
|
|
399
|
+
triggerAutoFocusAt(0.5f, 0.5f)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Trigger autofocus at a specific normalized point (0.0-1.0)
|
|
404
|
+
*/
|
|
405
|
+
private fun triggerAutoFocusAt(normalizedX: Float, normalizedY: Float) {
|
|
406
|
+
val meteringPointFactory = previewView?.meteringPointFactory ?: return
|
|
407
|
+
val afPoint = meteringPointFactory.createPoint(normalizedX, normalizedY)
|
|
408
|
+
|
|
409
|
+
val focusAction = FocusMeteringAction.Builder(afPoint, FocusMeteringAction.FLAG_AF)
|
|
410
|
+
.setAutoCancelDuration(2, java.util.concurrent.TimeUnit.SECONDS) // Auto-cancel after 2 seconds
|
|
411
|
+
.build()
|
|
412
|
+
|
|
413
|
+
camera?.cameraControl?.startFocusAndMetering(focusAction)
|
|
414
|
+
?.addListener({
|
|
415
|
+
Log.d(TAG, "Auto-focus triggered at ($normalizedX, $normalizedY)")
|
|
416
|
+
}, ContextCompat.getMainExecutor(context))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@OptIn(ExperimentalGetImage::class)
|
|
421
|
+
private fun processImage(imageProxy: ImageProxy) {
|
|
422
|
+
try {
|
|
423
|
+
// Skip processing if scanning is paused
|
|
424
|
+
if (isScanningPaused) {
|
|
425
|
+
imageProxy.close()
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
val mediaImage = imageProxy.image
|
|
430
|
+
if (mediaImage != null && previewView != null) {
|
|
431
|
+
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
|
432
|
+
val transformationInfo = ImageAnalysisTransformationInfo(
|
|
433
|
+
resolution = Size(imageProxy.width, imageProxy.height),
|
|
434
|
+
rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
barcodeScanner.process(image)
|
|
438
|
+
.addOnSuccessListener { barcodes ->
|
|
439
|
+
// Process barcode detection for the main scanning logic
|
|
440
|
+
if (barcodes.isNotEmpty() && !isScanningPaused) {
|
|
441
|
+
val processedBarcodes = processBarcodesAccordingToStrategy(barcodes, transformationInfo)
|
|
442
|
+
|
|
443
|
+
// Update barcode frames for processed barcodes (filtered)
|
|
444
|
+
updateBarcodeFrames(processedBarcodes, transformationInfo)
|
|
445
|
+
|
|
446
|
+
if (processedBarcodes.isNotEmpty()) {
|
|
447
|
+
val currentTime = System.currentTimeMillis()
|
|
448
|
+
val timeSinceLastEmission = currentTime - lastBarcodeEmissionTime
|
|
449
|
+
|
|
450
|
+
// Debounce: Prevent rapid duplicate emissions
|
|
451
|
+
// If we've emitted recently (within debounce interval), skip this emission
|
|
452
|
+
// This prevents multiple alerts when pauseScanning is set but detection callbacks are still in flight
|
|
453
|
+
if (timeSinceLastEmission < barcodeEmissionDebounceInterval) {
|
|
454
|
+
Log.d(TAG, "⏭️ Skipping barcode emission (debounced, last emission was ${timeSinceLastEmission}ms ago)")
|
|
455
|
+
return@addOnSuccessListener
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
lastBarcodeEmissionTime = currentTime
|
|
459
|
+
|
|
460
|
+
// Create array of barcode events
|
|
461
|
+
val barcodeEvents = Arguments.createArray()
|
|
462
|
+
processedBarcodes.forEach { barcode ->
|
|
463
|
+
val eventData = Arguments.createMap().apply {
|
|
464
|
+
putString("data", barcode.rawValue)
|
|
465
|
+
putString("format", barcode.format.toString())
|
|
466
|
+
putDouble("timestamp", System.currentTimeMillis().toDouble())
|
|
467
|
+
|
|
468
|
+
// Add bounding box if available
|
|
469
|
+
barcode.boundingBox?.let { box ->
|
|
470
|
+
val boundingBoxData = Arguments.createMap().apply {
|
|
471
|
+
putDouble("left", box.left.toDouble())
|
|
472
|
+
putDouble("top", box.top.toDouble())
|
|
473
|
+
putDouble("right", box.right.toDouble())
|
|
474
|
+
putDouble("bottom", box.bottom.toDouble())
|
|
475
|
+
}
|
|
476
|
+
putMap("boundingBox", boundingBoxData)
|
|
477
|
+
|
|
478
|
+
// Calculate area
|
|
479
|
+
val area = (box.right - box.left) * (box.bottom - box.top)
|
|
480
|
+
putDouble("area", area.toDouble())
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
barcodeEvents.pushMap(eventData)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
val ctx = reactContext
|
|
487
|
+
if (ctx is ThemedReactContext) {
|
|
488
|
+
ctx.runOnUiQueueThread {
|
|
489
|
+
val eventData = Arguments.createMap().apply {
|
|
490
|
+
putArray("barcodes", barcodeEvents)
|
|
491
|
+
}
|
|
492
|
+
ctx.getJSModule(RCTEventEmitter::class.java)
|
|
493
|
+
.receiveEvent(this@ScannerView.id, "onBarcodeScanned", eventData)
|
|
494
|
+
Log.d(TAG, "✅ Barcode emitted (debounced)")
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
// TODO: Review this part later
|
|
500
|
+
// Clear barcode frames when no barcodes are detected or scanning is paused
|
|
501
|
+
// updateBarcodeFrames(emptyList(), transformationInfo)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
.addOnFailureListener { e ->
|
|
505
|
+
Log.e(TAG, "Barcode scanning failed", e)
|
|
506
|
+
}
|
|
507
|
+
.addOnCompleteListener {
|
|
508
|
+
imageProxy.close()
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
imageProxy.close()
|
|
512
|
+
}
|
|
513
|
+
} catch (e: Exception) {
|
|
514
|
+
Log.e(TAG, "Error processing image", e)
|
|
515
|
+
imageProxy.close()
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Frame setter methods
|
|
520
|
+
fun setEnableFrame(enable: Boolean) {
|
|
521
|
+
enableFrame = enable
|
|
522
|
+
overlayView?.setEnableFrame(enable)
|
|
523
|
+
|
|
524
|
+
// Update focus when frame is toggled
|
|
525
|
+
if (camera != null) {
|
|
526
|
+
setupAutoFocusOnFrame()
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
fun setBorderColor(color: String) {
|
|
531
|
+
try {
|
|
532
|
+
borderColor = color.toColorInt()
|
|
533
|
+
overlayView?.setBorderColor(borderColor)
|
|
534
|
+
} catch (e: Exception) {
|
|
535
|
+
Log.e(TAG, "Invalid color format: $color")
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
fun setTintColor(color: String) {
|
|
540
|
+
try {
|
|
541
|
+
tintColor = color.toColorInt()
|
|
542
|
+
overlayView?.setTintColor(tintColor)
|
|
543
|
+
} catch (e: Exception) {
|
|
544
|
+
Log.e(TAG, "Invalid color format: $color")
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
fun setFrameSize(size: FrameSize) {
|
|
549
|
+
frameSize = size
|
|
550
|
+
overlayView?.setFrameSize(size)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fun setShowBarcodeFramesOnlyInFrame(showOnlyInFrame: Boolean) {
|
|
554
|
+
showBarcodeFramesOnlyInFrame = showOnlyInFrame
|
|
555
|
+
Log.d(TAG, "Show barcode frames only in frame: $showOnlyInFrame")
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Focus area configuration methods
|
|
559
|
+
fun setFocusAreaEnabled(enabled: Boolean) {
|
|
560
|
+
focusAreaEnabled = enabled
|
|
561
|
+
Log.d(TAG, "Focus area enabled: $enabled")
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Barcode frames configuration methods
|
|
565
|
+
fun setBarcodeFramesEnabled(enabled: Boolean) {
|
|
566
|
+
barcodeFramesEnabled = enabled
|
|
567
|
+
Log.d(TAG, "Barcode frames enabled: $enabled")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
fun setBarcodeFramesColor(color: String) {
|
|
571
|
+
try {
|
|
572
|
+
barcodeFramesColor = color.toColorInt()
|
|
573
|
+
Log.d(TAG, "Barcode frames color set: $color")
|
|
574
|
+
} catch (e: Exception) {
|
|
575
|
+
Log.e(TAG, "Invalid barcode frames color format: $color")
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
fun setZoom(zoom: Float) {
|
|
580
|
+
this.zoom = zoom
|
|
581
|
+
// Apply zoom, respecting the device's limits
|
|
582
|
+
val zoomState = camera?.cameraInfo?.zoomState?.value
|
|
583
|
+
if (zoomState != null) {
|
|
584
|
+
val newZoom = zoom.coerceIn(zoomState.minZoomRatio, zoomState.maxZoomRatio)
|
|
585
|
+
camera?.cameraControl?.setZoomRatio(newZoom)
|
|
586
|
+
Log.d(TAG, "Setting zoom to $newZoom (requested $zoom)")
|
|
587
|
+
} else {
|
|
588
|
+
camera?.cameraControl?.setZoomRatio(zoom)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fun setTorch(enabled: Boolean) {
|
|
593
|
+
torchEnabled = enabled
|
|
594
|
+
camera?.cameraControl?.enableTorch(enabled)
|
|
595
|
+
Log.d(TAG, "Torch ${if (enabled) "enabled" else "disabled"}")
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
fun resumeScanning() {
|
|
599
|
+
isScanningPaused = false
|
|
600
|
+
// Reset debounce timer when resuming to allow immediate detection
|
|
601
|
+
lastBarcodeEmissionTime = 0
|
|
602
|
+
Log.d(TAG, "Scanning resumed")
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fun pauseScanning() {
|
|
606
|
+
isScanningPaused = true
|
|
607
|
+
// Clear all barcode frames when pausing
|
|
608
|
+
barcodeFrameManager.clearAllFrames()
|
|
609
|
+
// Reset debounce timer when pausing to prevent stale emissions
|
|
610
|
+
lastBarcodeEmissionTime = 0
|
|
611
|
+
Log.d(TAG, "Scanning paused")
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
fun setBarcodeEmissionInterval(intervalSeconds: Double) {
|
|
615
|
+
// Convert seconds to milliseconds, ensure non-negative
|
|
616
|
+
barcodeEmissionDebounceInterval = max(0, (intervalSeconds * 1000).toLong())
|
|
617
|
+
Log.d(TAG, "Barcode emission interval set to: ${barcodeEmissionDebounceInterval}ms")
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
fun setBarcodeScanStrategy(strategy: String) {
|
|
621
|
+
barcodeScanStrategy = strategy
|
|
622
|
+
Log.d(TAG, "Barcode scan strategy set to: $strategy")
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
fun setPosition(x: Float, y: Float) {
|
|
626
|
+
positionX = x.coerceIn(0f, 100f) // Clamp to 0-100 range
|
|
627
|
+
positionY = y.coerceIn(0f, 100f) // Clamp to 0-100 range
|
|
628
|
+
overlayView?.setPosition(positionX, positionY)
|
|
629
|
+
Log.d(TAG, "Focus area position set to: x=$positionX%, y=$positionY%")
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
fun setKeepScreenOnEnabled(keepOn: Boolean) {
|
|
633
|
+
updateKeepScreenOn(keepOn)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private fun hasCameraPermission(): Boolean {
|
|
637
|
+
return context.checkSelfPermission(android.Manifest.permission.CAMERA) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private fun updateBarcodeFrames(
|
|
641
|
+
barcodes: List<Barcode>,
|
|
642
|
+
transformationInfo: ImageAnalysisTransformationInfo
|
|
643
|
+
) {
|
|
644
|
+
// Only update barcode frames if they are enabled
|
|
645
|
+
if (!barcodeFramesEnabled) {
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
val barcodeFrames = mutableMapOf<String, RectF>()
|
|
650
|
+
|
|
651
|
+
barcodes.forEach { barcode ->
|
|
652
|
+
val barcodeValue = barcode.rawValue ?: return@forEach
|
|
653
|
+
val boundingBox = barcode.boundingBox ?: return@forEach
|
|
654
|
+
|
|
655
|
+
val transformedRect = transformBarcodeBoundingBox(
|
|
656
|
+
boundingBox,
|
|
657
|
+
transformationInfo,
|
|
658
|
+
previewView!!
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
// If showBarcodeFramesOnlyInFrame is true, only include frames within the overlay frame
|
|
662
|
+
if (showBarcodeFramesOnlyInFrame && enableFrame) {
|
|
663
|
+
val frame = overlayView?.frameRect
|
|
664
|
+
if (frame != null && frame.contains(transformedRect)) {
|
|
665
|
+
barcodeFrames[barcodeValue] = transformedRect
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
// Show all barcode frames
|
|
669
|
+
barcodeFrames[barcodeValue] = transformedRect
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Update frames using the manager
|
|
674
|
+
barcodeFrameManager.updateBarcodeFrames(barcodeFrames)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private fun processBarcodesAccordingToStrategy(
|
|
678
|
+
barcodes: List<Barcode>,
|
|
679
|
+
transformationInfo: ImageAnalysisTransformationInfo
|
|
680
|
+
): List<Barcode> {
|
|
681
|
+
if (barcodes.isEmpty()) return emptyList()
|
|
682
|
+
|
|
683
|
+
// Filter barcodes based on focus area if enabled
|
|
684
|
+
val filteredBarcodes = if (focusAreaEnabled) {
|
|
685
|
+
val frame = overlayView?.frameRect
|
|
686
|
+
if (frame != null) {
|
|
687
|
+
barcodes.filter { barcode ->
|
|
688
|
+
barcode.boundingBox?.let { box ->
|
|
689
|
+
val transformedBox = transformBarcodeBoundingBox(box, transformationInfo, previewView!!)
|
|
690
|
+
frame.contains(transformedBox)
|
|
691
|
+
} ?: false
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
barcodes
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
barcodes
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (filteredBarcodes.isEmpty()) return emptyList()
|
|
701
|
+
|
|
702
|
+
// Apply strategy
|
|
703
|
+
return when (barcodeScanStrategy) {
|
|
704
|
+
"ONE" -> {
|
|
705
|
+
listOf(filteredBarcodes[0])
|
|
706
|
+
}
|
|
707
|
+
"BIGGEST" -> {
|
|
708
|
+
val barcodeWithArea = filteredBarcodes.mapNotNull { barcode ->
|
|
709
|
+
barcode.boundingBox?.let { box ->
|
|
710
|
+
val area = (box.right - box.left) * (box.bottom - box.top)
|
|
711
|
+
barcode to area
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (barcodeWithArea.isNotEmpty()) {
|
|
715
|
+
val biggest = barcodeWithArea.maxByOrNull { it.second }
|
|
716
|
+
listOf(biggest!!.first)
|
|
717
|
+
} else {
|
|
718
|
+
listOf(filteredBarcodes[0])
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
"SORT_BY_BIGGEST" -> {
|
|
722
|
+
val barcodeWithArea = filteredBarcodes.mapNotNull { barcode ->
|
|
723
|
+
barcode.boundingBox?.let { box ->
|
|
724
|
+
val area = (box.right - box.left) * (box.bottom - box.top)
|
|
725
|
+
barcode to area
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (barcodeWithArea.isNotEmpty()) {
|
|
729
|
+
barcodeWithArea.sortedByDescending { it.second }.map { it.first }
|
|
730
|
+
} else {
|
|
731
|
+
filteredBarcodes
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
"ALL" -> filteredBarcodes
|
|
735
|
+
else -> filteredBarcodes // Default to ALL
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private data class ImageAnalysisTransformationInfo(
|
|
741
|
+
val resolution: Size,
|
|
742
|
+
val rotationDegrees: Int
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
private fun transformBarcodeBoundingBox(
|
|
746
|
+
barcodeBoundingBox: Rect,
|
|
747
|
+
imageAnalysisInfo: ImageAnalysisTransformationInfo,
|
|
748
|
+
previewView: View
|
|
749
|
+
): RectF {
|
|
750
|
+
// Get image analysis resolution and rotation
|
|
751
|
+
val imageWidth = imageAnalysisInfo.resolution.width
|
|
752
|
+
val imageHeight = imageAnalysisInfo.resolution.height
|
|
753
|
+
val imageRotation = imageAnalysisInfo.rotationDegrees
|
|
754
|
+
|
|
755
|
+
// Adjust for rotation
|
|
756
|
+
val (rotatedWidth, rotatedHeight) = if (imageRotation == 90 || imageRotation == 270) {
|
|
757
|
+
imageHeight to imageWidth
|
|
758
|
+
} else {
|
|
759
|
+
imageWidth to imageHeight
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Get preview view dimensions
|
|
763
|
+
val viewWidth = previewView.width
|
|
764
|
+
val viewHeight = previewView.height
|
|
765
|
+
|
|
766
|
+
// Calculate scale factors
|
|
767
|
+
val scaleX = viewWidth.toFloat() / rotatedWidth
|
|
768
|
+
val scaleY = viewHeight.toFloat() / rotatedHeight
|
|
769
|
+
val scale = maxOf(scaleX, scaleY) // For FILL_CENTER
|
|
770
|
+
|
|
771
|
+
// Calculate offsets
|
|
772
|
+
val offsetX = (viewWidth - rotatedWidth * scale) / 2
|
|
773
|
+
val offsetY = (viewHeight - rotatedHeight * scale) / 2
|
|
774
|
+
|
|
775
|
+
// Transform the bounding box
|
|
776
|
+
val transformedRect = RectF(barcodeBoundingBox)
|
|
777
|
+
transformedRect.left = transformedRect.left * scale + offsetX
|
|
778
|
+
transformedRect.top = transformedRect.top * scale + offsetY
|
|
779
|
+
transformedRect.right = transformedRect.right * scale + offsetX
|
|
780
|
+
transformedRect.bottom = transformedRect.bottom * scale + offsetY
|
|
781
|
+
|
|
782
|
+
return transformedRect
|
|
783
|
+
}
|