@unif/react-native-hms-scan 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # @unif/react-native-hms-scan
2
2
 
3
- 华为 **HMS 统一扫码服务**(HUAWEI Scan Kit)的 React Native 封装,新架构(TurboModule + Fabric)。聚焦两件事:
3
+ [![npm](https://img.shields.io/npm/v/@unif/react-native-hms-scan.svg?color=cb3837&logo=npm)](https://www.npmjs.com/package/@unif/react-native-hms-scan)
4
+ [![CI](https://github.com/unif-design/react-native-hms-scan/actions/workflows/ci.yml/badge.svg)](https://github.com/unif-design/react-native-hms-scan/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/npm/l/@unif/react-native-hms-scan.svg?color=blue)](https://github.com/unif-design/react-native-hms-scan/blob/main/LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-unif--design.github.io-orange.svg)](https://unif-design.github.io/react-native-hms-scan/)
4
7
 
5
- - **定制视图扫码** —— 底层 headless `<HmsScanView>` 相机组件,外加按设计稿做好的成品 `<Scanner>` 扫一扫页(聚焦款,浅色)。
6
- - **图片识别** —— `decodeImage(uri)` 从本地图片解码条码 / 二维码。
8
+ 华为 **HMS 统一扫码**(HUAWEI Scan Kit)的 React Native 封装(新架构):定制视图扫码(成品页 `<Scanner>` / headless `<HmsScanView>`)+ 图片识别(`decodeImage`)。Android 用 Scan SDK-Plus(内置引擎,**不依赖 HMS Core APK**,非华为机型可用),iOS 用 ScanKitFrameWork;两端均无需 AppGallery Connect / API Key。
7
9
 
8
- > Android 用 **Scan SDK-Plus**(内置引擎,**不依赖设备安装 HMS Core APK**,非华为机型可用);iOS 用 CocoaPods 的 **ScanKitFrameWork**。两端**都不需要 AppGallery Connect / agconnect 配置 / API Key**。
10
+ > 📖 **完整文档**(安装 · 平台配置 · API · 平台差异 · 故障排查):
11
+ > **https://unif-design.github.io/react-native-hms-scan/**
9
12
 
10
13
  ## 安装
11
14
 
@@ -14,44 +17,10 @@ yarn add @unif/react-native-hms-scan react-native-svg
14
17
  cd ios && pod install
15
18
  ```
16
19
 
17
- `react-native-svg` 是 peer 依赖(`<Scanner>` 的图标用它绘制)。本库为 New Architecture 库,宿主需开启新架构。
18
-
19
- ### Android
20
-
21
- 库的 gradle 已自带华为 maven 仓库与 `com.huawei.hms:scanplus` 依赖,**无需改宿主 gradle**。只需在宿主 `AndroidManifest.xml` 声明权限:
22
-
23
- ```xml
24
- <uses-permission android:name="android.permission.CAMERA" />
25
- <uses-feature android:name="android.hardware.camera" android:required="false" />
26
- <!-- 仅当用 decodeImage 解相册图时需要 -->
27
- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- API 33+ -->
28
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
29
- android:maxSdkVersion="32" />
30
- ```
31
-
32
- - minSdkVersion ≥ 24。
33
- - **不需要** `agconnect-services.json` / agconnect 插件 / API Key。
34
-
35
- ### iOS
36
-
37
- `pod install` 会自动拉取 `ScanKitFrameWork`。在宿主 `Info.plist` 加:
38
-
39
- ```xml
40
- <key>NSCameraUsageDescription</key>
41
- <string>用于扫描商品条码与门店二维码</string>
42
- <!-- 仅当用 decodeImage 解相册图时需要 -->
43
- <key>NSPhotoLibraryUsageDescription</key>
44
- <string>用于从相册选取图片识别条码</string>
45
- ```
46
-
47
- - 最低 iOS 版本随宿主 RN 工程。
48
- - `ScanKitFrameWork` 是 **arm64-only 静态库**:podspec 已对 Apple 芯片模拟器排掉 arm64(模拟器走 x86_64),**真机 arm64 不受影响**;建议在真机调试扫码。
49
- - **不需要** AppGallery Connect / API Key。
20
+ 本库为新架构(TurboModule + Fabric)库,宿主需开启新架构。Android 权限、iOS `ScanKitFrameWork`(arm64-only 静态库,模拟器排除 / 建议真机)等见[文档站 · 安装](https://unif-design.github.io/react-native-hms-scan/docs/getting-started/installation)。
50
21
 
51
22
  ## 用法
52
23
 
53
- ### 1) 成品扫一扫页 `<Scanner>`(推荐)
54
-
55
24
  ```tsx
56
25
  import { Scanner, type ScanResult, type ScanProduct } from '@unif/react-native-hms-scan';
57
26
 
@@ -60,89 +29,19 @@ function ScanScreen({ navigation }) {
60
29
  <Scanner
61
30
  title="扫一扫"
62
31
  onClose={() => navigation.goBack()}
63
- // 扫到条码后解析商品(你的业务:查接口/本地库)。返回 null = 未识别。
64
32
  resolveProduct={async (r: ScanResult): Promise<ScanProduct | null> => {
65
33
  const p = await api.lookupByBarcode(r.value);
66
- return p && {
67
- name: p.name, brand: p.brand, price: `¥${p.price}`,
68
- spec: p.spec, stockShort: p.stockText, barcode: r.value,
69
- };
70
- }}
71
- // 点"确认":把结果带回上一级
72
- onConfirm={(product, result) => {
73
- navigation.navigate('Order', { barcode: result.value, product });
74
- }}
75
- // 点"相册":用你自己的图片选择器返回本地 uri
76
- pickImage={async () => {
77
- const res = await launchImageLibrary({ mediaType: 'photo' });
78
- return res.assets?.[0]?.uri ?? null;
34
+ return p
35
+ ? { name: p.name, brand: p.brand, price: `¥${p.price}`, barcode: r.value }
36
+ : null;
79
37
  }}
38
+ onConfirm={(product, result) => navigation.navigate('Order', { product })}
80
39
  />
81
40
  );
82
41
  }
83
42
  ```
84
43
 
85
- `<Scanner>` 内部状态:取景 → 识别中 → 识别成功(浮层确认卡)/ 未识别(重试弹层)/ 无权限(去设置)。单次扫一个,确认后带回上一级。手电筒 / 相册在底部工具栏。
86
-
87
- > 安全区:默认 `topInset=54 / bottomInset=34`。若用 `react-native-safe-area-context`,把 `insets.top/bottom` 传进来更精准。
88
-
89
- ### 2) 底层 headless 组件 `<HmsScanView>`
90
-
91
- 完全自定义 UI 时,只用相机预览 + 结果事件,自己画取景框:
92
-
93
- ```tsx
94
- import { HmsScanView, type ScanResult } from '@unif/react-native-hms-scan';
95
-
96
- <HmsScanView
97
- style={{ flex: 1 }}
98
- formats={['QR_CODE', 'EAN_13']} // 不传 = 全部码制
99
- torch={torchOn} // Android 可控;iOS 为尽力而为
100
- paused={paused} // 命中后想停在结果时置 true
101
- onScanResult={(results: ScanResult[]) => {
102
- const code = results[0]?.value;
103
- }}
104
- onScanError={(e) => console.warn(e.code, e.message)}
105
- />
106
- ```
107
-
108
- ### 3) 图片识别 `decodeImage`
109
-
110
- ```tsx
111
- import { decodeImage } from '@unif/react-native-hms-scan';
112
-
113
- const results = await decodeImage('file:///path/photo.jpg', { formats: ['QR_CODE'] });
114
- // results: ScanResult[](可能为空)。只吃本地 uri,远程图请先下载。
115
- ```
116
-
117
- ### 4) 相机权限
118
-
119
- ```tsx
120
- import { getCameraPermissionStatus, requestCameraPermission } from '@unif/react-native-hms-scan';
121
- const status = await requestCameraPermission(); // 'granted' | 'denied' | 'blocked' | 'undetermined'
122
- ```
123
-
124
- ## 平台差异(务必知悉)
125
-
126
- | 能力 | Android (`RemoteView`) | iOS (`HmsCustomScanViewController`) |
127
- | --- | --- | --- |
128
- | 手电筒 `torch` | ✅ 可编程控制 | ⚠️ HMS 无公开手电 API;本库走 `AVCaptureDevice` **尽力而为**,不保证 |
129
- | 变焦 | ❌ 仅内部自动 | ❌ 仅内部自动 |
130
- | 暗光提示 `onTorchStatus.available` | ✅ 上报 | ❌ 不上报 |
131
-
132
- ## 测试(Jest)
133
-
134
- 消费者在测试里可用内置 mock 替换本库(避免加载原生):
135
-
136
- ```ts
137
- jest.mock('@unif/react-native-hms-scan', () =>
138
- require('@unif/react-native-hms-scan/mock')
139
- );
140
- // decodeImage / 权限方法是 jest.fn;<HmsScanView>/<Scanner> 渲染为 null。
141
- ```
142
-
143
- ## 类型一览
144
-
145
- `BarcodeFormat`(QR_CODE / EAN_13 / … / UNKNOWN)、`BarcodeContentType`、`ScanResult { value, format, contentType?, cornerPoints? }`、`ScanProduct`、`CameraPermissionStatus`、`HmsScanError`。
44
+ headless `<HmsScanView>`、图片识别 `decodeImage`、权限、平台差异 —— 见[文档站](https://unif-design.github.io/react-native-hms-scan/)。
146
45
 
147
46
  ## 许可
148
47
 
@@ -16,14 +16,22 @@ Pod::Spec.new do |s|
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
17
  s.private_header_files = "ios/**/*.h"
18
18
 
19
+ # hms-scan 自己用 AVFoundation(摄像头权限 + torch);UIKit / Foundation 显式声明保险。
20
+ # 之前 podspec 缺这个 —— 被 EXCLUDED_ARCHS 排掉模拟器 arm64 编译掩盖了;现在去掉
21
+ # EXCLUDED_ARCHS(xcframework 已补 arm64-sim 切片)、arm64 模拟器真编译就暴露了缺链接。
22
+ s.frameworks = "AVFoundation", "UIKit", "Foundation"
23
+
19
24
  # 华为 HUAWEI Scan Kit(iOS 自包含,无需 AppGallery Connect / API Key)。
20
25
  # 提供 HmsCustomScanViewController(定制视图)与 HmsBitMap(图片识别)。
21
- s.dependency "ScanKitFrameWork", "~> 1.1.2"
22
-
23
- # ScanKitFrameWork 是 arm64-only 的静态 framework,不含模拟器 arm64 切片。
24
- # 在 Apple 芯片上排掉模拟器 arm64(模拟器走 x86_64 / Rosetta),真机 arm64 不受影响。
25
- s.pod_target_xcconfig = { "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "arm64" }
26
- s.user_target_xcconfig = { "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "arm64" }
26
+ #
27
+ # 华为官方 ScanKitFrameWork 1.1.2 是老式 fat framework(arm64 真机 + x86_64 Intel 模拟器),
28
+ # 不含 arm64-simulator 切片 → Apple Silicon 模拟器会链接失败(旧版只能 EXCLUDED_ARCHS 排掉、
29
+ # 导致模拟器 / CI 编不过)。这里改为:prepare_command 用 Apple 官方 vtool 把真机 arm64
30
+ # 改写成 simulator 平台、补出 arm64-sim,打成 xcframework(device 片 / simulator 片分目录)。
31
+ # 详见 scripts/prepare-scankit-xcframework.sh。产物 ios/vendor/ 不入 git,pod install 时按需生成。
32
+ s.prepare_command = "bash scripts/prepare-scankit-xcframework.sh"
33
+ s.vendored_frameworks = "ios/vendor/ScanKitFrameWork.xcframework"
34
+ s.resources = "ios/vendor/ScanKitFrameWorkBundle.bundle"
27
35
 
28
36
  # 接入 New Architecture(Fabric 组件 + TurboModule)的 codegen 依赖。
29
37
  install_modules_dependencies(s)
@@ -23,9 +23,9 @@ import java.io.InputStream
23
23
  * - decodeImage(): decode a local image into a ScanResult[] JSON string.
24
24
  * - getCameraPermissionStatus() / requestCameraPermission(): camera permission.
25
25
  */
26
- class HmsScanModule(reactContext: ReactApplicationContext) :
27
- NativeHmsScanSpec(reactContext) {
28
-
26
+ class HmsScanModule(
27
+ reactContext: ReactApplicationContext,
28
+ ) : NativeHmsScanSpec(reactContext) {
29
29
  override fun getName(): String = NAME
30
30
 
31
31
  // ── decodeImage ──────────────────────────────────────────────────────────
@@ -36,7 +36,11 @@ class HmsScanModule(reactContext: ReactApplicationContext) :
36
36
  * (photo mode), and resolves the ScanResult[] JSON. Never rejects on "no code
37
37
  * found" — it resolves an empty array, matching the JS contract.
38
38
  */
39
- override fun decodeImage(uri: String, formatsCsv: String, promise: Promise) {
39
+ override fun decodeImage(
40
+ uri: String,
41
+ formatsCsv: String,
42
+ promise: Promise,
43
+ ) {
40
44
  val bitmap = loadBitmap(uri)
41
45
  if (bitmap == null) {
42
46
  promise.reject(E_IMAGE_LOAD_FAILED, "Failed to load image from uri: $uri")
@@ -67,8 +71,8 @@ class HmsScanModule(reactContext: ReactApplicationContext) :
67
71
  }
68
72
 
69
73
  /** Resolve a uri/path to a decoded [Bitmap], or null on any failure. */
70
- private fun loadBitmap(uri: String): Bitmap? {
71
- return try {
74
+ private fun loadBitmap(uri: String): Bitmap? =
75
+ try {
72
76
  val parsed = Uri.parse(uri)
73
77
  val scheme = parsed.scheme?.lowercase()
74
78
  when (scheme) {
@@ -82,11 +86,13 @@ class HmsScanModule(reactContext: ReactApplicationContext) :
82
86
  stream?.close()
83
87
  }
84
88
  }
89
+
85
90
  null -> {
86
91
  // No scheme -> treat as an absolute filesystem path.
87
92
  val path = parsed.path ?: uri
88
93
  decodeFile(path)
89
94
  }
95
+
90
96
  else -> {
91
97
  // Unsupported scheme (e.g. http/https): JS contract forbids remote URLs.
92
98
  null
@@ -95,7 +101,6 @@ class HmsScanModule(reactContext: ReactApplicationContext) :
95
101
  } catch (e: Throwable) {
96
102
  null
97
103
  }
98
- }
99
104
 
100
105
  private fun decodeFile(path: String): Bitmap? {
101
106
  val file = File(path)
@@ -137,43 +142,48 @@ class HmsScanModule(reactContext: ReactApplicationContext) :
137
142
  // current (not-granted) state rather than hanging the promise.
138
143
  promise.reject(
139
144
  E_NO_ACTIVITY,
140
- "Cannot request camera permission: no current PermissionAwareActivity"
145
+ "Cannot request camera permission: no current PermissionAwareActivity",
141
146
  )
142
147
  return
143
148
  }
144
149
 
145
150
  val permissionAwareActivity = activity as PermissionAwareActivity
146
- val listener = PermissionListener { requestCode, _, grantResults ->
147
- if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
148
- return@PermissionListener false
149
- }
150
- val granted = grantResults.isNotEmpty() &&
151
- grantResults[0] == PackageManager.PERMISSION_GRANTED
152
- val status = when {
153
- granted -> GRANTED
154
- // After a denial: rationale==true means the user can be asked again;
155
- // rationale==false means "don't ask again" (blocked).
156
- ActivityCompat.shouldShowRequestPermissionRationale(
157
- activity,
158
- Manifest.permission.CAMERA
159
- ) -> DENIED
160
- else -> BLOCKED
151
+ val listener =
152
+ PermissionListener { requestCode, _, grantResults ->
153
+ if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
154
+ return@PermissionListener false
155
+ }
156
+ val granted =
157
+ grantResults.isNotEmpty() &&
158
+ grantResults[0] == PackageManager.PERMISSION_GRANTED
159
+ val status =
160
+ when {
161
+ granted -> GRANTED
162
+
163
+ // After a denial: rationale==true means the user can be asked again;
164
+ // rationale==false means "don't ask again" (blocked).
165
+ ActivityCompat.shouldShowRequestPermissionRationale(
166
+ activity,
167
+ Manifest.permission.CAMERA,
168
+ ) -> DENIED
169
+
170
+ else -> BLOCKED
171
+ }
172
+ promise.resolve(status)
173
+ true
161
174
  }
162
- promise.resolve(status)
163
- true
164
- }
165
175
 
166
176
  permissionAwareActivity.requestPermissions(
167
177
  arrayOf(Manifest.permission.CAMERA),
168
178
  CAMERA_PERMISSION_REQUEST_CODE,
169
- listener
179
+ listener,
170
180
  )
171
181
  }
172
182
 
173
183
  private fun hasCameraPermission(): Boolean =
174
184
  ContextCompat.checkSelfPermission(
175
185
  reactApplicationContext,
176
- Manifest.permission.CAMERA
186
+ Manifest.permission.CAMERA,
177
187
  ) == PackageManager.PERMISSION_GRANTED
178
188
 
179
189
  companion object {
@@ -16,7 +16,6 @@ import org.json.JSONObject
16
16
  * ```
17
17
  */
18
18
  internal object HmsScanResultMapper {
19
-
20
19
  /**
21
20
  * Serialize an array of [HmsScan] into the ScanResult[] JSON string. The element
22
21
  * type is nullable because the HMS APIs (decodeWithBitmap / OnResultCallback)
@@ -37,7 +36,10 @@ internal object HmsScanResultMapper {
37
36
  return array.toString()
38
37
  }
39
38
 
40
- private fun toJsonObject(scan: HmsScan, value: String): JSONObject {
39
+ private fun toJsonObject(
40
+ scan: HmsScan,
41
+ value: String,
42
+ ): JSONObject {
41
43
  val obj = JSONObject()
42
44
  obj.put("value", value)
43
45
  obj.put("format", mapScanType(scan.getScanType()))
@@ -53,7 +55,7 @@ internal object HmsScanResultMapper {
53
55
  JSONObject().apply {
54
56
  put("x", point.x)
55
57
  put("y", point.y)
56
- }
58
+ },
57
59
  )
58
60
  }
59
61
  if (points.length() > 0) {
@@ -65,23 +67,24 @@ internal object HmsScanResultMapper {
65
67
  }
66
68
 
67
69
  /** HmsScan.getScanType() (barcode symbology) -> unified BarcodeFormat string. */
68
- fun mapScanType(scanType: Int): String = when (scanType) {
69
- HmsScan.QRCODE_SCAN_TYPE -> "QR_CODE"
70
- HmsScan.AZTEC_SCAN_TYPE -> "AZTEC"
71
- HmsScan.DATAMATRIX_SCAN_TYPE -> "DATA_MATRIX"
72
- HmsScan.PDF417_SCAN_TYPE -> "PDF417"
73
- HmsScan.CODABAR_SCAN_TYPE -> "CODABAR"
74
- HmsScan.CODE39_SCAN_TYPE -> "CODE_39"
75
- HmsScan.CODE93_SCAN_TYPE -> "CODE_93"
76
- HmsScan.CODE128_SCAN_TYPE -> "CODE_128"
77
- HmsScan.EAN8_SCAN_TYPE -> "EAN_8"
78
- HmsScan.EAN13_SCAN_TYPE -> "EAN_13"
79
- HmsScan.UPCCODE_A_SCAN_TYPE -> "UPC_A"
80
- HmsScan.UPCCODE_E_SCAN_TYPE -> "UPC_E"
81
- HmsScan.ITF14_SCAN_TYPE -> "ITF14"
82
- HmsScan.MULTI_FUNCTIONAL_SCAN_TYPE -> "MULTI_FUNCTIONAL"
83
- else -> "UNKNOWN"
84
- }
70
+ fun mapScanType(scanType: Int): String =
71
+ when (scanType) {
72
+ HmsScan.QRCODE_SCAN_TYPE -> "QR_CODE"
73
+ HmsScan.AZTEC_SCAN_TYPE -> "AZTEC"
74
+ HmsScan.DATAMATRIX_SCAN_TYPE -> "DATA_MATRIX"
75
+ HmsScan.PDF417_SCAN_TYPE -> "PDF417"
76
+ HmsScan.CODABAR_SCAN_TYPE -> "CODABAR"
77
+ HmsScan.CODE39_SCAN_TYPE -> "CODE_39"
78
+ HmsScan.CODE93_SCAN_TYPE -> "CODE_93"
79
+ HmsScan.CODE128_SCAN_TYPE -> "CODE_128"
80
+ HmsScan.EAN8_SCAN_TYPE -> "EAN_8"
81
+ HmsScan.EAN13_SCAN_TYPE -> "EAN_13"
82
+ HmsScan.UPCCODE_A_SCAN_TYPE -> "UPC_A"
83
+ HmsScan.UPCCODE_E_SCAN_TYPE -> "UPC_E"
84
+ HmsScan.ITF14_SCAN_TYPE -> "ITF14"
85
+ HmsScan.MULTI_FUNCTIONAL_SCAN_TYPE -> "MULTI_FUNCTIONAL"
86
+ else -> "UNKNOWN"
87
+ }
85
88
 
86
89
  /**
87
90
  * Resolve a comma-separated formats CSV into the int varargs for
@@ -92,33 +95,35 @@ internal object HmsScanResultMapper {
92
95
  */
93
96
  fun parseFormatsCsv(formatsCsv: String?): IntArray? {
94
97
  if (formatsCsv.isNullOrBlank()) return null
95
- val types = formatsCsv
96
- .split(",")
97
- .map { it.trim() }
98
- .filter { it.isNotEmpty() }
99
- .mapNotNull { mapFormatString(it) }
100
- .distinct()
98
+ val types =
99
+ formatsCsv
100
+ .split(",")
101
+ .map { it.trim() }
102
+ .filter { it.isNotEmpty() }
103
+ .mapNotNull { mapFormatString(it) }
104
+ .distinct()
101
105
  return if (types.isEmpty()) null else types.toIntArray()
102
106
  }
103
107
 
104
108
  /** Unified BarcodeFormat string -> HmsScan.*_SCAN_TYPE (null when unknown). */
105
- private fun mapFormatString(format: String): Int? = when (format) {
106
- "QR_CODE" -> HmsScan.QRCODE_SCAN_TYPE
107
- "AZTEC" -> HmsScan.AZTEC_SCAN_TYPE
108
- "DATA_MATRIX" -> HmsScan.DATAMATRIX_SCAN_TYPE
109
- "PDF417" -> HmsScan.PDF417_SCAN_TYPE
110
- "CODABAR" -> HmsScan.CODABAR_SCAN_TYPE
111
- "CODE_39" -> HmsScan.CODE39_SCAN_TYPE
112
- "CODE_93" -> HmsScan.CODE93_SCAN_TYPE
113
- "CODE_128" -> HmsScan.CODE128_SCAN_TYPE
114
- "EAN_8" -> HmsScan.EAN8_SCAN_TYPE
115
- "EAN_13" -> HmsScan.EAN13_SCAN_TYPE
116
- "UPC_A" -> HmsScan.UPCCODE_A_SCAN_TYPE
117
- "UPC_E" -> HmsScan.UPCCODE_E_SCAN_TYPE
118
- "ITF14" -> HmsScan.ITF14_SCAN_TYPE
119
- "MULTI_FUNCTIONAL" -> HmsScan.MULTI_FUNCTIONAL_SCAN_TYPE
120
- else -> null
121
- }
109
+ private fun mapFormatString(format: String): Int? =
110
+ when (format) {
111
+ "QR_CODE" -> HmsScan.QRCODE_SCAN_TYPE
112
+ "AZTEC" -> HmsScan.AZTEC_SCAN_TYPE
113
+ "DATA_MATRIX" -> HmsScan.DATAMATRIX_SCAN_TYPE
114
+ "PDF417" -> HmsScan.PDF417_SCAN_TYPE
115
+ "CODABAR" -> HmsScan.CODABAR_SCAN_TYPE
116
+ "CODE_39" -> HmsScan.CODE39_SCAN_TYPE
117
+ "CODE_93" -> HmsScan.CODE93_SCAN_TYPE
118
+ "CODE_128" -> HmsScan.CODE128_SCAN_TYPE
119
+ "EAN_8" -> HmsScan.EAN8_SCAN_TYPE
120
+ "EAN_13" -> HmsScan.EAN13_SCAN_TYPE
121
+ "UPC_A" -> HmsScan.UPCCODE_A_SCAN_TYPE
122
+ "UPC_E" -> HmsScan.UPCCODE_E_SCAN_TYPE
123
+ "ITF14" -> HmsScan.ITF14_SCAN_TYPE
124
+ "MULTI_FUNCTIONAL" -> HmsScan.MULTI_FUNCTIONAL_SCAN_TYPE
125
+ else -> null
126
+ }
122
127
 
123
128
  /**
124
129
  * HmsScan.getScanTypeForm() (content semantic) -> unified BarcodeContentType
@@ -127,19 +132,20 @@ internal object HmsScanResultMapper {
127
132
  * Uses the named HmsScan.*_FORM constants (public static final ints) rather than
128
133
  * hard-coded numbers so the mapping stays correct across SDK versions.
129
134
  */
130
- private fun mapContentType(form: Int): String = when (form) {
131
- HmsScan.URL_FORM -> "URL"
132
- HmsScan.EMAIL_CONTENT_FORM -> "EMAIL"
133
- HmsScan.TEL_PHONE_NUMBER_FORM -> "PHONE"
134
- HmsScan.SMS_FORM -> "SMS"
135
- HmsScan.WIFI_CONNECT_INFO_FORM -> "WIFI"
136
- HmsScan.CONTACT_DETAIL_FORM -> "CONTACT"
137
- HmsScan.EVENT_INFO_FORM -> "EVENT"
138
- HmsScan.LOCATION_COORDINATE_FORM -> "LOCATION"
139
- HmsScan.DRIVER_INFO_FORM -> "DRIVER"
140
- HmsScan.ISBN_NUMBER_FORM -> "ISBN"
141
- HmsScan.ARTICLE_NUMBER_FORM -> "ARTICLE"
142
- HmsScan.PURE_TEXT_FORM -> "TEXT"
143
- else -> "OTHER"
144
- }
135
+ private fun mapContentType(form: Int): String =
136
+ when (form) {
137
+ HmsScan.URL_FORM -> "URL"
138
+ HmsScan.EMAIL_CONTENT_FORM -> "EMAIL"
139
+ HmsScan.TEL_PHONE_NUMBER_FORM -> "PHONE"
140
+ HmsScan.SMS_FORM -> "SMS"
141
+ HmsScan.WIFI_CONNECT_INFO_FORM -> "WIFI"
142
+ HmsScan.CONTACT_DETAIL_FORM -> "CONTACT"
143
+ HmsScan.EVENT_INFO_FORM -> "EVENT"
144
+ HmsScan.LOCATION_COORDINATE_FORM -> "LOCATION"
145
+ HmsScan.DRIVER_INFO_FORM -> "DRIVER"
146
+ HmsScan.ISBN_NUMBER_FORM -> "ISBN"
147
+ HmsScan.ARTICLE_NUMBER_FORM -> "ARTICLE"
148
+ HmsScan.PURE_TEXT_FORM -> "TEXT"
149
+ else -> "OTHER"
150
+ }
145
151
  }
@@ -26,9 +26,10 @@ import com.huawei.hms.ml.scan.HmsScan
26
26
  * forwarded both from the View attach/detach callbacks and from the host
27
27
  * Activity's lifecycle via [LifecycleEventListener].
28
28
  */
29
- class HmsScanView(context: Context) :
30
- FrameLayout(context), LifecycleEventListener {
31
-
29
+ class HmsScanView(
30
+ context: Context,
31
+ ) : FrameLayout(context),
32
+ LifecycleEventListener {
32
33
  private val themedReactContext: ThemedReactContext = context as ThemedReactContext
33
34
 
34
35
  private var remoteView: RemoteView? = null
@@ -117,15 +118,17 @@ class HmsScanView(context: Context) :
117
118
  if (activity == null) {
118
119
  emitError(
119
120
  "E_CAMERA_INIT",
120
- "Cannot start camera: no host Activity available for RemoteView"
121
+ "Cannot start camera: no host Activity available for RemoteView",
121
122
  )
122
123
  return
123
124
  }
124
125
 
125
126
  try {
126
- val builder = RemoteView.Builder()
127
- .setContext(activity)
128
- .setContinuouslyScan(continuous)
127
+ val builder =
128
+ RemoteView
129
+ .Builder()
130
+ .setContext(activity)
131
+ .setContinuouslyScan(continuous)
129
132
 
130
133
  val types = HmsScanResultMapper.parseFormatsCsv(formatsCsv)
131
134
  if (types != null && types.isNotEmpty()) {
@@ -140,14 +143,14 @@ class HmsScanView(context: Context) :
140
143
 
141
144
  view.setOnResultCallback(OnResultCallback { result -> onScanResult(result) })
142
145
  view.setOnLightVisibleCallback(
143
- OnLightVisibleCallBack { visible -> onTorchVisible(visible) }
146
+ OnLightVisibleCallBack { visible -> onTorchVisible(visible) },
144
147
  )
145
148
 
146
149
  // onCreate must run after build() and before addView (per HMS docs/demo).
147
150
  view.onCreate(Bundle())
148
151
  addView(
149
152
  view,
150
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
153
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
151
154
  )
152
155
 
153
156
  remoteView = view
@@ -220,38 +223,63 @@ class HmsScanView(context: Context) :
220
223
  val json = HmsScanResultMapper.toJson(result)
221
224
  // toJson skips value-less hits; avoid emitting an empty "[]" event.
222
225
  if (json == "[]") return
223
- emitEvent("topScanResult", Arguments.createMap().apply {
224
- putString("resultsJson", json)
225
- })
226
+ emitEvent(
227
+ "topScanResult",
228
+ Arguments.createMap().apply {
229
+ putString("resultsJson", json)
230
+ },
231
+ )
226
232
  }
227
233
 
228
234
  private fun onTorchVisible(visible: Boolean) {
229
235
  // Keep our cached torch flag in sync with the actual hardware state.
230
236
  val on = remoteView?.lightStatus ?: torch
231
237
  torch = on
232
- emitEvent("topTorchStatus", Arguments.createMap().apply {
233
- putBoolean("available", visible)
234
- putBoolean("on", on)
235
- })
238
+ emitEvent(
239
+ "topTorchStatus",
240
+ Arguments.createMap().apply {
241
+ putBoolean("available", visible)
242
+ putBoolean("on", on)
243
+ },
244
+ )
236
245
  }
237
246
 
238
- private fun emitError(code: String, message: String) {
239
- emitEvent("topScanError", Arguments.createMap().apply {
240
- putString("code", code)
241
- putString("message", message)
242
- })
247
+ private fun emitError(
248
+ code: String,
249
+ message: String,
250
+ ) {
251
+ emitEvent(
252
+ "topScanError",
253
+ Arguments.createMap().apply {
254
+ putString("code", code)
255
+ putString("message", message)
256
+ },
257
+ )
243
258
  }
244
259
 
245
260
  // ── Fabric event dispatch ──────────────────────────────────────────────────
246
261
 
247
- private fun emitEvent(eventName: String, payload: WritableMap) {
262
+ private fun emitEvent(
263
+ eventName: String,
264
+ payload: WritableMap,
265
+ ) {
248
266
  val reactTag = id
249
267
  val dispatcher =
250
268
  UIManagerHelper.getEventDispatcherForReactTag(themedReactContext, reactTag)
251
269
  val surfaceId = themedReactContext.surfaceId
252
- dispatcher?.dispatchEvent(object : Event<Event<*>>(surfaceId, reactTag) {
253
- override fun getEventName(): String = eventName
254
- override fun getEventData(): WritableMap = payload
255
- })
270
+ dispatcher?.dispatchEvent(ScanEvent(surfaceId, reactTag, eventName, payload))
271
+ }
272
+
273
+ // RN 0.85 的 Event<T : Event<T>> 是自递归泛型(CRTP),必须用命名子类 Event<Self>;
274
+ // 匿名 object : Event<Event<*>> 不满足该 bound(0.85 起加严,旧版 bound 宽松能编)。
275
+ private class ScanEvent(
276
+ surfaceId: Int,
277
+ viewTag: Int,
278
+ private val name: String,
279
+ private val data: WritableMap,
280
+ ) : Event<ScanEvent>(surfaceId, viewTag) {
281
+ override fun getEventName(): String = name
282
+
283
+ override fun getEventData(): WritableMap = data
256
284
  }
257
285
  }