react-native-fabric-barcode-scanner 1.0.0

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.
@@ -0,0 +1,245 @@
1
+ package com.rncustomscanner
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.content.pm.PackageManager
6
+ import android.graphics.Color
7
+ import android.util.Log
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import android.widget.FrameLayout
11
+ import androidx.appcompat.app.AppCompatActivity
12
+ import androidx.camera.core.Camera
13
+ import androidx.camera.core.CameraSelector
14
+ import androidx.camera.core.ImageAnalysis
15
+ import androidx.camera.core.Preview
16
+ import androidx.camera.lifecycle.ProcessCameraProvider
17
+ import androidx.camera.view.PreviewView
18
+ import androidx.core.content.ContextCompat
19
+ import com.facebook.react.bridge.LifecycleEventListener
20
+ import com.facebook.react.uimanager.ThemedReactContext
21
+ import com.facebook.react.uimanager.UIManagerHelper
22
+ import com.rncustomscanner.events.BarcodeScannedEvent
23
+ import java.util.concurrent.ExecutorService
24
+ import java.util.concurrent.Executors
25
+
26
+ @SuppressLint("ViewConstructor")
27
+ class ScannerView(context: ThemedReactContext) : FrameLayout(context) {
28
+
29
+ private val reactContext: ThemedReactContext = context
30
+ private val previewView = PreviewView(context)
31
+ private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
32
+
33
+ private var cameraProvider: ProcessCameraProvider? = null
34
+ private var camera: Camera? = null
35
+ private var preview: Preview? = null
36
+ private var imageAnalysis: ImageAnalysis? = null
37
+ private var barcodeAnalyzer: BarcodeAnalyzer? = null
38
+
39
+ private var isActive: Boolean = true
40
+ private var torchOn: Boolean = false
41
+ private var scanFormats: List<String> = BarcodeFormatMapper.supportedFormats
42
+ private var isCameraBound = false
43
+
44
+ private val lifecycleListener = object : LifecycleEventListener {
45
+ override fun onHostResume() {
46
+ if (isActive && hasCameraPermission()) {
47
+ isCameraBound = false
48
+ scheduleCameraStart()
49
+ }
50
+ }
51
+
52
+ override fun onHostPause() {
53
+ stopCamera()
54
+ }
55
+
56
+ override fun onHostDestroy() {
57
+ stopCamera()
58
+ }
59
+ }
60
+
61
+ init {
62
+ setBackgroundColor(Color.BLACK)
63
+ previewView.layoutParams = LayoutParams(
64
+ LayoutParams.MATCH_PARENT,
65
+ LayoutParams.MATCH_PARENT,
66
+ )
67
+ previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
68
+ installHierarchyFitter(previewView)
69
+ addView(previewView)
70
+
71
+ reactContext.addLifecycleEventListener(lifecycleListener)
72
+ }
73
+
74
+ override fun onAttachedToWindow() {
75
+ super.onAttachedToWindow()
76
+ scheduleCameraStart()
77
+ }
78
+
79
+ override fun onDetachedFromWindow() {
80
+ reactContext.removeLifecycleEventListener(lifecycleListener)
81
+ stopCamera()
82
+ barcodeAnalyzer?.close()
83
+ barcodeAnalyzer = null
84
+ cameraExecutor.shutdown()
85
+ super.onDetachedFromWindow()
86
+ }
87
+
88
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
89
+ super.onWindowFocusChanged(hasWindowFocus)
90
+ if (hasWindowFocus && isActive && hasCameraPermission() && !isCameraBound) {
91
+ previewView.post { scheduleCameraStart() }
92
+ }
93
+ }
94
+
95
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
96
+ super.onSizeChanged(w, h, oldw, oldh)
97
+ if (w > 0 && h > 0 && !isCameraBound) {
98
+ scheduleCameraStart()
99
+ }
100
+ }
101
+
102
+ fun setIsActive(active: Boolean) {
103
+ isActive = active
104
+ if (active) {
105
+ scheduleCameraStart()
106
+ } else {
107
+ stopCamera()
108
+ }
109
+ }
110
+
111
+ fun setTorchOn(enabled: Boolean) {
112
+ torchOn = enabled
113
+ camera?.cameraControl?.enableTorch(enabled)
114
+ }
115
+
116
+ fun setScanFormats(formats: List<String>?) {
117
+ scanFormats = formats?.takeIf { it.isNotEmpty() } ?: BarcodeFormatMapper.supportedFormats
118
+ barcodeAnalyzer?.updateFormats(BarcodeFormatMapper.toMlKitFormats(scanFormats))
119
+ }
120
+
121
+ private fun scheduleCameraStart() {
122
+ if (!isActive || !hasCameraPermission() || width <= 0 || height <= 0) {
123
+ return
124
+ }
125
+ previewView.post { configureCameraIfNeeded() }
126
+ }
127
+
128
+ private fun hasCameraPermission(): Boolean {
129
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
130
+ PackageManager.PERMISSION_GRANTED
131
+ }
132
+
133
+ private fun configureCameraIfNeeded() {
134
+ if (!isActive || !hasCameraPermission() || width <= 0 || height <= 0 || isCameraBound) {
135
+ return
136
+ }
137
+
138
+ val activity = reactContext.currentActivity
139
+ if (activity == null) {
140
+ Log.w(TAG, "Activity not available yet, retrying camera start")
141
+ previewView.postDelayed({ configureCameraIfNeeded() }, 100)
142
+ return
143
+ }
144
+
145
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
146
+ cameraProviderFuture.addListener({
147
+ try {
148
+ cameraProvider = cameraProviderFuture.get()
149
+ bindCameraUseCases()
150
+ } catch (exc: Exception) {
151
+ Log.e(TAG, "Failed to initialize camera", exc)
152
+ isCameraBound = false
153
+ }
154
+ }, ContextCompat.getMainExecutor(activity))
155
+ }
156
+
157
+ private fun bindCameraUseCases() {
158
+ val provider = cameraProvider ?: return
159
+ val activity = reactContext.currentActivity as? AppCompatActivity ?: return
160
+
161
+ provider.unbindAll()
162
+
163
+ preview = Preview.Builder().build()
164
+
165
+ val formats = BarcodeFormatMapper.toMlKitFormats(scanFormats)
166
+ barcodeAnalyzer = BarcodeAnalyzer { barcodes ->
167
+ val barcode = barcodes.firstOrNull() ?: return@BarcodeAnalyzer
168
+ val value = barcode.rawValue ?: return@BarcodeAnalyzer
169
+ emitBarcodeScanned(
170
+ value = value,
171
+ type = BarcodeFormatMapper.fromMlKitFormat(barcode.format),
172
+ timestamp = System.currentTimeMillis().toInt(),
173
+ )
174
+ }.also {
175
+ it.updateFormats(formats)
176
+ }
177
+
178
+ imageAnalysis = ImageAnalysis.Builder()
179
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
180
+ .build()
181
+ .also { analysis ->
182
+ analysis.setAnalyzer(cameraExecutor, barcodeAnalyzer!!)
183
+ }
184
+
185
+ try {
186
+ val useCases = listOfNotNull(preview, imageAnalysis)
187
+ camera = provider.bindToLifecycle(
188
+ activity,
189
+ CameraSelector.DEFAULT_BACK_CAMERA,
190
+ *useCases.toTypedArray(),
191
+ )
192
+ camera?.cameraControl?.enableTorch(torchOn)
193
+ // Attach surface AFTER bind — required for PreviewView inside React Native.
194
+ preview?.setSurfaceProvider(previewView.surfaceProvider)
195
+ isCameraBound = true
196
+ Log.d(TAG, "Camera bound successfully (${width}x${height})")
197
+ } catch (exc: Exception) {
198
+ Log.e(TAG, "Use case binding failed", exc)
199
+ isCameraBound = false
200
+ }
201
+ }
202
+
203
+ private fun stopCamera() {
204
+ cameraProvider?.unbindAll()
205
+ camera = null
206
+ preview = null
207
+ imageAnalysis = null
208
+ isCameraBound = false
209
+ }
210
+
211
+ // React Native does not always layout TextureView children correctly — without this
212
+ // the PreviewView stays black. See: https://github.com/facebook/react-native/issues/17968
213
+ private fun installHierarchyFitter(view: ViewGroup) {
214
+ view.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
215
+ override fun onChildViewRemoved(parent: View?, child: View?) = Unit
216
+
217
+ override fun onChildViewAdded(parent: View?, child: View?) {
218
+ parent?.measure(
219
+ MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
220
+ MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY),
221
+ )
222
+ parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
223
+ }
224
+ })
225
+ }
226
+
227
+ private fun emitBarcodeScanned(value: String, type: String, timestamp: Int) {
228
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
229
+ UIManagerHelper
230
+ .getEventDispatcherForReactTag(reactContext, id)
231
+ ?.dispatchEvent(
232
+ BarcodeScannedEvent(
233
+ surfaceId,
234
+ id,
235
+ value,
236
+ type,
237
+ timestamp,
238
+ ),
239
+ )
240
+ }
241
+
242
+ companion object {
243
+ private const val TAG = "ScannerView"
244
+ }
245
+ }
@@ -0,0 +1,29 @@
1
+ package com.rncustomscanner.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class BarcodeScannedEvent(
8
+ surfaceId: Int,
9
+ viewId: Int,
10
+ private val value: String,
11
+ private val type: String,
12
+ private val timestamp: Int,
13
+ ) : Event<BarcodeScannedEvent>(surfaceId, viewId) {
14
+
15
+ override fun getEventName(): String = EVENT_NAME
16
+
17
+ override fun getEventData(): WritableMap =
18
+ Arguments.createMap().apply {
19
+ putString("value", value)
20
+ putString("type", type)
21
+ putInt("timestamp", timestamp)
22
+ }
23
+
24
+ override fun canCoalesce(): Boolean = false
25
+
26
+ companion object {
27
+ const val EVENT_NAME = "topBarcodeScanned"
28
+ }
29
+ }
@@ -0,0 +1,103 @@
1
+ package com.rncustomscanner
2
+
3
+ import android.Manifest
4
+ import android.content.pm.PackageManager
5
+ import android.hardware.camera2.CameraManager
6
+ import androidx.core.app.ActivityCompat
7
+ import androidx.core.content.ContextCompat
8
+ import com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.module.annotations.ReactModule
11
+ import com.facebook.react.modules.core.PermissionAwareActivity
12
+ import com.facebook.react.modules.core.PermissionListener
13
+
14
+ @ReactModule(name = ScannerModule.NAME)
15
+ class ScannerModule(
16
+ private val reactContext: ReactApplicationContext,
17
+ ) : NativeScannerModuleSpec(reactContext) {
18
+
19
+ override fun getName(): String = NAME
20
+
21
+ override fun getTypedExportedConstants(): Map<String, Any> {
22
+ return mapOf("SUPPORTED_FORMATS" to BarcodeFormatMapper.supportedFormats)
23
+ }
24
+
25
+ override fun checkCameraPermission(promise: Promise) {
26
+ promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
27
+ }
28
+
29
+ override fun requestCameraPermission(promise: Promise) {
30
+ val current = CameraPermissionHelper.checkPermission(reactContext)
31
+
32
+ if (current == CameraPermissionHelper.STATUS_GRANTED) {
33
+ promise.resolve(current)
34
+ return
35
+ }
36
+
37
+ if (!CameraPermissionHelper.canRequestPermission(current)) {
38
+ promise.resolve(current)
39
+ return
40
+ }
41
+
42
+ val activity = reactContext.currentActivity
43
+ if (activity == null) {
44
+ promise.resolve(CameraPermissionHelper.STATUS_DENIED)
45
+ return
46
+ }
47
+
48
+ if (
49
+ ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) ==
50
+ PackageManager.PERMISSION_GRANTED
51
+ ) {
52
+ promise.resolve(CameraPermissionHelper.STATUS_GRANTED)
53
+ return
54
+ }
55
+
56
+ val listener = PermissionListener { requestCode, _, grantResults ->
57
+ if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
58
+ return@PermissionListener false
59
+ }
60
+ promise.resolve(
61
+ CameraPermissionHelper.resolveRequestResult(reactContext, grantResults),
62
+ )
63
+ true
64
+ }
65
+
66
+ CameraPermissionHelper.markPermissionRequested(reactContext)
67
+
68
+ if (activity is PermissionAwareActivity) {
69
+ activity.requestPermissions(
70
+ arrayOf(Manifest.permission.CAMERA),
71
+ CAMERA_PERMISSION_REQUEST_CODE,
72
+ listener,
73
+ )
74
+ } else {
75
+ ActivityCompat.requestPermissions(
76
+ activity,
77
+ arrayOf(Manifest.permission.CAMERA),
78
+ CAMERA_PERMISSION_REQUEST_CODE,
79
+ )
80
+ promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
81
+ }
82
+ }
83
+
84
+ override fun openCameraSettings() {
85
+ CameraPermissionHelper.openAppSettings(reactContext)
86
+ }
87
+
88
+ override fun setTorchEnabled(enabled: Boolean) {
89
+ val cameraManager =
90
+ reactContext.getSystemService(CameraManager::class.java) ?: return
91
+ try {
92
+ val cameraId = cameraManager.cameraIdList.firstOrNull() ?: return
93
+ cameraManager.setTorchMode(cameraId, enabled)
94
+ } catch (_: Exception) {
95
+ // Best-effort: torch failures are non-fatal for scanning
96
+ }
97
+ }
98
+
99
+ companion object {
100
+ const val NAME = "ScannerModule"
101
+ const val CAMERA_PERMISSION_REQUEST_CODE = 0x5343414E
102
+ }
103
+ }
@@ -0,0 +1,61 @@
1
+ package com.rncustomscanner
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.uimanager.SimpleViewManager
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.ViewManagerDelegate
8
+ import com.facebook.react.uimanager.annotations.ReactProp
9
+ import com.facebook.react.viewmanagers.ScannerViewManagerDelegate
10
+ import com.facebook.react.viewmanagers.ScannerViewManagerInterface
11
+ import com.rncustomscanner.events.BarcodeScannedEvent
12
+
13
+ class ScannerViewManager(
14
+ @Suppress("UNUSED_PARAMETER") context: ReactApplicationContext,
15
+ ) : SimpleViewManager<ScannerView>(), ScannerViewManagerInterface<ScannerView> {
16
+
17
+ private val delegate: ViewManagerDelegate<ScannerView> = ScannerViewManagerDelegate(this)
18
+
19
+ override fun getDelegate(): ViewManagerDelegate<ScannerView> = delegate
20
+
21
+ override fun getName(): String = REACT_CLASS
22
+
23
+ override fun createViewInstance(context: ThemedReactContext): ScannerView {
24
+ return ScannerView(context)
25
+ }
26
+
27
+ override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
28
+ return mapOf(
29
+ BarcodeScannedEvent.EVENT_NAME to mapOf(
30
+ "registrationName" to "onBarcodeScanned",
31
+ ),
32
+ )
33
+ }
34
+
35
+ @ReactProp(name = "isActive", defaultBoolean = true)
36
+ override fun setIsActive(view: ScannerView, value: Boolean) {
37
+ view.setIsActive(value)
38
+ }
39
+
40
+ @ReactProp(name = "torchOn", defaultBoolean = false)
41
+ override fun setTorchOn(view: ScannerView, value: Boolean) {
42
+ view.setTorchOn(value)
43
+ }
44
+
45
+ @ReactProp(name = "scanFormats")
46
+ override fun setScanFormats(view: ScannerView, value: ReadableArray?) {
47
+ if (value == null) {
48
+ view.setScanFormats(null)
49
+ return
50
+ }
51
+ val formats = ArrayList<String>(value.size())
52
+ for (i in 0 until value.size()) {
53
+ value.getString(i)?.let { formats.add(it) }
54
+ }
55
+ view.setScanFormats(formats)
56
+ }
57
+
58
+ companion object {
59
+ const val REACT_CLASS = "ScannerView"
60
+ }
61
+ }
@@ -0,0 +1,109 @@
1
+ package com.rncustomscanner
2
+
3
+ import android.Manifest
4
+ import android.content.pm.PackageManager
5
+ import android.hardware.camera2.CameraManager
6
+ import androidx.core.app.ActivityCompat
7
+ import androidx.core.content.ContextCompat
8
+ import com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
11
+ import com.facebook.react.bridge.ReactMethod
12
+ import com.facebook.react.module.annotations.ReactModule
13
+ import com.facebook.react.modules.core.PermissionAwareActivity
14
+ import com.facebook.react.modules.core.PermissionListener
15
+
16
+ @ReactModule(name = ScannerModule.NAME)
17
+ class ScannerModule(
18
+ private val reactContext: ReactApplicationContext,
19
+ ) : ReactContextBaseJavaModule(reactContext) {
20
+
21
+ override fun getName(): String = NAME
22
+
23
+ override fun getConstants(): Map<String, Any> {
24
+ return mapOf("SUPPORTED_FORMATS" to BarcodeFormatMapper.supportedFormats)
25
+ }
26
+
27
+ @ReactMethod
28
+ fun checkCameraPermission(promise: Promise) {
29
+ promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
30
+ }
31
+
32
+ @ReactMethod
33
+ fun requestCameraPermission(promise: Promise) {
34
+ val current = CameraPermissionHelper.checkPermission(reactContext)
35
+
36
+ if (current == CameraPermissionHelper.STATUS_GRANTED) {
37
+ promise.resolve(current)
38
+ return
39
+ }
40
+
41
+ if (!CameraPermissionHelper.canRequestPermission(current)) {
42
+ promise.resolve(current)
43
+ return
44
+ }
45
+
46
+ val activity = reactContext.currentActivity
47
+ if (activity == null) {
48
+ promise.resolve(CameraPermissionHelper.STATUS_DENIED)
49
+ return
50
+ }
51
+
52
+ if (
53
+ ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) ==
54
+ PackageManager.PERMISSION_GRANTED
55
+ ) {
56
+ promise.resolve(CameraPermissionHelper.STATUS_GRANTED)
57
+ return
58
+ }
59
+
60
+ val listener = PermissionListener { requestCode, _, grantResults ->
61
+ if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
62
+ return@PermissionListener false
63
+ }
64
+ promise.resolve(
65
+ CameraPermissionHelper.resolveRequestResult(reactContext, grantResults),
66
+ )
67
+ true
68
+ }
69
+
70
+ CameraPermissionHelper.markPermissionRequested(reactContext)
71
+
72
+ if (activity is PermissionAwareActivity) {
73
+ activity.requestPermissions(
74
+ arrayOf(Manifest.permission.CAMERA),
75
+ CAMERA_PERMISSION_REQUEST_CODE,
76
+ listener,
77
+ )
78
+ } else {
79
+ ActivityCompat.requestPermissions(
80
+ activity,
81
+ arrayOf(Manifest.permission.CAMERA),
82
+ CAMERA_PERMISSION_REQUEST_CODE,
83
+ )
84
+ promise.resolve(CameraPermissionHelper.checkPermission(reactContext))
85
+ }
86
+ }
87
+
88
+ @ReactMethod
89
+ fun openCameraSettings() {
90
+ CameraPermissionHelper.openAppSettings(reactContext)
91
+ }
92
+
93
+ @ReactMethod
94
+ fun setTorchEnabled(enabled: Boolean) {
95
+ val cameraManager =
96
+ reactContext.getSystemService(CameraManager::class.java) ?: return
97
+ try {
98
+ val cameraId = cameraManager.cameraIdList.firstOrNull() ?: return
99
+ cameraManager.setTorchMode(cameraId, enabled)
100
+ } catch (_: Exception) {
101
+ // Best-effort
102
+ }
103
+ }
104
+
105
+ companion object {
106
+ const val NAME = "ScannerModule"
107
+ const val CAMERA_PERMISSION_REQUEST_CODE = 0x5343414E
108
+ }
109
+ }
@@ -0,0 +1,54 @@
1
+ package com.rncustomscanner
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.uimanager.SimpleViewManager
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+ import com.rncustomscanner.events.BarcodeScannedEvent
9
+
10
+ class ScannerViewManager(
11
+ @Suppress("UNUSED_PARAMETER") context: ReactApplicationContext,
12
+ ) : SimpleViewManager<ScannerView>() {
13
+
14
+ override fun getName(): String = REACT_CLASS
15
+
16
+ override fun createViewInstance(context: ThemedReactContext): ScannerView {
17
+ return ScannerView(context)
18
+ }
19
+
20
+ override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
21
+ return mapOf(
22
+ BarcodeScannedEvent.EVENT_NAME to mapOf(
23
+ "registrationName" to "onBarcodeScanned",
24
+ ),
25
+ )
26
+ }
27
+
28
+ @ReactProp(name = "isActive", defaultBoolean = true)
29
+ fun setIsActive(view: ScannerView, value: Boolean) {
30
+ view.setIsActive(value)
31
+ }
32
+
33
+ @ReactProp(name = "torchOn", defaultBoolean = false)
34
+ fun setTorchOn(view: ScannerView, value: Boolean) {
35
+ view.setTorchOn(value)
36
+ }
37
+
38
+ @ReactProp(name = "scanFormats")
39
+ fun setScanFormats(view: ScannerView, value: ReadableArray?) {
40
+ if (value == null) {
41
+ view.setScanFormats(null)
42
+ return
43
+ }
44
+ val formats = ArrayList<String>(value.size())
45
+ for (i in 0 until value.size()) {
46
+ value.getString(i)?.let { formats.add(it) }
47
+ }
48
+ view.setScanFormats(formats)
49
+ }
50
+
51
+ companion object {
52
+ const val REACT_CLASS = "ScannerView"
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+ #import <React/RCTViewManager.h>