@unif/react-native-hms-scan 0.1.0 → 0.2.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/README.md +13 -114
- package/ReactNativeHmsScan.podspec +14 -6
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +38 -28
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +64 -58
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +54 -26
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +21 -12
- package/android/src/main/java/com/unif/reactnativehmsscan/ReactNativeHmsScanPackage.kt +17 -21
- package/ios/HmsScanModule.mm +26 -40
- package/ios/HmsScanResultMapper.mm +97 -83
- package/ios/HmsScanView.mm +23 -40
- package/lib/module/index.js +3 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +34 -7
- package/scripts/prepare-scankit-xcframework.sh +83 -0
- package/src/index.tsx +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# @unif/react-native-hms-scan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@unif/react-native-hms-scan)
|
|
4
|
+
[](https://github.com/unif-design/react-native-hms-scan/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/unif-design/react-native-hms-scan/blob/main/LICENSE)
|
|
6
|
+
[](https://unif-design.github.io/react-native-hms-scan/)
|
|
4
7
|
|
|
5
|
-
|
|
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
|
-
>
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
`<
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
27
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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(
|
|
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 =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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? =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
|
30
|
-
|
|
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 =
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
224
|
-
|
|
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(
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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(
|
|
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(
|
|
253
|
-
|
|
254
|
-
|
|
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
|
}
|