README.md
1# 折叠屏扫描二维码方案
2
3### 介绍
4
5本示例介绍使用自定义界面扫码能力在折叠屏设备中实现折叠态切换适配。自定义界面扫码使用系统能力customScan,其提供相机流的初始化、启动扫码、识别、停止扫码、释放相机流资源等能力。支持访问系统图库,选择照片进行识别。折叠屏折叠状态通过监听display的foldStatusChange事件实现。
6
7### 效果图预览
8
9<img src="./entry/src/main/resources/base/media/custom_scan.gif" width="200">
10
11**使用说明**
12
131. 用户授权相机扫码。
142. 对准二维码即可识别展示,支持多二维码同时出现时的识别,但因为识别速度较快,当相机视野内出现一个二维码时,就会立马识别。
153. 支持打开和关闭相机闪光灯。
164. 折叠态不同,相机流的尺寸也不同,因此折叠态变更时,扫码服务会重新初始化。
175. 支持图库照片识别,当识别多二维码照片时,随机选择一个展示结果。
186. 展示识别结果,如果是可访问的网页,直接打开网页展示,如果不是url,显示识别结果文本。
19
20### 实现思路
21
221. 相机权限需要用户授权。
23```typescript
24// 向用户申请授权
25let context = getContext() as common.UIAbilityContext;
26let atManager = abilityAccessCtrl.createAtManager();
27let grantStatusArr = await atManager.requestPermissionsFromUser(context, [ 'ohos.permission.CAMERA' ]);
28const grantStatus = grantStatusArr.authResults[0];
29```
30源码请参考[CustomScanViewModel.ets](./customscan/src/main/ets/viewmodel/CustomScanViewModel.ets)
31
322. 依赖XComponent展示相机流内容,在加载完相机流后启动相机扫码服务。
33```typescript
34// TODO:知识点:相机流显示依赖XComponent
35XComponent({
36 type: XComponentType.SURFACE,
37 controller: this.cameraSurfaceController
38})
39 .onLoad(() => {
40 // TODO:知识点:customScan依赖XComponent组件的surfaceId,对图像进行扫描
41 this.customScanVM.surfaceId = this.cameraSurfaceController.getXComponentSurfaceId();
42 // TODO:知识点:初始化XComponent组件的surface流的尺寸
43 this.updateCameraSurfaceSize(this.customScanVM.cameraCompWidth, this.customScanVM.cameraCompHeight);
44 // TODO:知识点:XComponent加载完成后,启动相机进行扫码
45 this.customScanVM.initCustomScan();
46 })
47 .clip(true)
48```
49源码请参考[CustomScanCameraComp.ets](./customscan/src/main/ets/components/CustomScanCameraComp.ets)
50
513. 二维码识别通过customScan系统能力在启动扫描之后,通过异步任务监控相机图像,对识别到的内容直接返回customScanCallback处理。
52```typescript
53try {
54 const viewControl: customScan.ViewControl = {
55 width: this.cameraCompWidth,
56 height: this.cameraCompHeight,
57 surfaceId: this.surfaceId
58 };
59 // TODO:知识点 请求扫码结果,通过Promise触发回调
60 customScan.start(viewControl).then((result) => {
61 // 处理扫码结果
62 this.customScanCallback(result);
63 }).catch((error: BusinessError) => {
64 logger.error('start failed error: ' + JSON.stringify(error));
65 })
66} catch (error) {
67 logger.error('start fail, error: ' + JSON.stringify(error));
68}
69```
70源码请参考[CustomScanViewModel.ets](./customscan/src/main/ets/viewmodel/CustomScanViewModel.ets)
71
724. 在customScanCallback回调中,处理相机返回的扫码结果,用于在屏幕上标记二维码位置。如果扫码结果为空,重启扫码。
73```typescript
74customScanCallback(result: scanBarcode.ScanResult[]): void {
75 if (!this.isScanned) {
76 this.scanResult.code = 0;
77 this.scanResult.data = result || [];
78 let resultLength: number = result ? result.length : 0;
79 if (resultLength) {
80 // 停止扫描
81 this.stopCustomScan();
82 // 标记扫描状态,触发UI刷新
83 this.isScanned = true;
84 this.scanResult.size = resultLength;
85 } else {
86 // 重新扫码
87 this.reCustomScan()
88 }
89 }
90}
91```
92源码请参考[CustomScanViewModel.ets](./customscan/src/main/ets/viewmodel/CustomScanViewModel.ets)
93
945. 如果扫描结果只有一个二维码,那么在指定位置上标记图片,并弹窗展示二维码内容
95```typescript
96Image($rawfile('scan_selected.svg'))
97 // TODO: 知识点: 在扫描结果返回的水平坐标和纵坐标位置上展示图片
98 .selected(true, this.singleCodeX, this.singleCodeY)
99 .scale({ x: this.singleCodeScale, y: this.singleCodeScale })
100 .opacity(this.singleCodeOpacity)
101 .onAppear(() => {
102 this.singleCodeBreathe();
103 })
104```
105源码请参考[CommonCodeLayout.ets](./customscan/src/main/ets/components/CommonCodeLayout.ets)
106
1076. 如果扫描结果只有多个二维码,那么需要在这些二维码的位置上都标记图片,等待用户点击
108```typescript
109Row() {
110 Image($rawfile('scan_selected2.svg'))
111 .width(40)
112 .height(40)
113 .visibility((this.isMultiSelected && this.multiSelectedIndex !== index) ? Visibility.None : Visibility.Visible)
114 .scale({ x: this.multiCodeScale, y: this.multiCodeScale })
115 .opacity(this.multiCodeOpacity)
116 .onAppear(() => {
117 // 展示动画,因为共用状态变量,只需要第一次执行
118 if (index === 0) {
119 this.multiAppear();
120 }
121 })
122 .onClick(() => {
123 // 点击打开二维码信息弹窗
124 this.openMultiCode(arr, index);
125 })
126}
127// TODO: 知识点: 预览流有固定比例,XComponent只能展示部分,返回的扫码结果和当前展示存在一定偏移量
128.position({
129 x: this.getOffset('x', arr),
130 y: this.getOffset('y', arr)
131})
132```
133源码请参考[CommonCodeLayout.ets](./customscan/src/main/ets/components/CommonCodeLayout.ets)
134
1357. 选中二维码后,读取二维码内容,然后跳转路由展示
136```typescript
137async showScanResult(scanResult: scanBarcode.ScanResult): Promise<void> {
138 // 码源信息
139 const originalValue: string = scanResult.originalValue;
140 // 二维码识别结果展示
141 this.subPageStack.pushPathByName(CommonConstants.SUB_PAGE_DETECT_BARCODE, originalValue, true);
142}
143```
144源码请参考[CommonCodeLayout.ets](./customscan/src/main/ets/components/CommonCodeLayout.ets)
145
1468. 通过系统picker拉起系统相册选择需要进行二维码识别的照片,通过系统二维码识别库detectBarcode进行二维码识别
147```typescript
148async detectFromPhotoPicker(): Promise<string> {
149 const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
150 photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
151 photoSelectOptions.maxSelectNumber = 1;
152 const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
153 const photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoViewPicker.select(photoSelectOptions);
154 const uris: string[] = photoSelectResult.photoUris;
155 if (uris.length === 0) {
156 return '';
157 }
158
159 // 识别结果
160 let retVal = CommonConstants.DETECT_NO_RESULT;
161 const inputImage: detectBarcode.InputImage = { uri: uris[0] };
162 try {
163 // 定义识码参数options
164 let options: scanBarcode.ScanOptions = {
165 scanTypes: [scanCore.ScanType.QR_CODE],
166 enableMultiMode: true,
167 enableAlbum: true,
168 }
169 // 调用图片识码接口
170 const decodeResult: scanBarcode.ScanResult[] = await detectBarcode.decode(inputImage, options);
171 if (decodeResult.length > 0) {
172 retVal = decodeResult[0].originalValue;
173 }
174 logger.error('[customscan]', `Failed to get ScanResult by promise with options.`);
175 } catch (error) {
176 logger.error('[customscan]', `Failed to detectBarcode. Code: ${error.code}, message: ${error.message}`);
177 }
178
179 // 停止扫描
180 this.stopCustomScan();
181 return retVal;
182}
183```
184源码请参考[CustomScanViewModel.ets](./customscan/src/main/ets/viewmodel/CustomScanViewModel.ets)
185
1869. 折叠屏设备上,依赖display的屏幕状态事件,监听屏幕折叠状态变更,通过对折叠状态的分析,更新XComponent尺寸并重新启动扫码服务。
187```typescript
188// 监听折叠屏状态变更,更新折叠态,修改窗口显示方向
189display.on('foldStatusChange', async (curFoldStatus: display.FoldStatus) => {
190 // 无视FOLD_STATUS_UNKNOWN状态
191 if (curFoldStatus === display.FoldStatus.FOLD_STATUS_UNKNOWN) {
192 return;
193 }
194 // FOLD_STATUS_HALF_FOLDED状态当作FOLD_STATUS_EXPANDED一致处理
195 if (curFoldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) {
196 curFoldStatus = display.FoldStatus.FOLD_STATUS_EXPANDED;
197 }
198 // 同一个状态重复触发不做处理
199 if (this.curFoldStatus === curFoldStatus) {
200 return;
201 }
202
203 // 缓存当前折叠状态
204 this.curFoldStatus = curFoldStatus;
205
206 // 当前没有相机流资源,只更新相机流宽高设置
207 if (!this.surfaceId) {
208 this.updateCameraCompSize();
209 return;
210 }
211
212 // 关闭闪光灯
213 this.tryCloseFlashLight();
214 setTimeout(() => {
215 // 重新启动扫码
216 this.restartCustomScan();
217 }, 10)
218})
219```
220源码请参考[CustomScanViewModel.ets](./customscan/src/main/ets/viewmodel/CustomScanViewModel.ets)
221
222### 高性能知识点
223
224不涉及
225
226### 工程结构&模块类型
227
228 ```
229 customscan // har类型
230 |---common
231 | |---constants
232 | | |---BreakpointConstants.ets // 设备大小枚举常量
233 | | |---CommonConstants.ets // 通用常量
234 | |---utils
235 | | |---FunctionUtil.ets // 功能函数工具
236 | | |---GlobalUtil // global工具
237 |---components
238 | |---CommonCodeLayout.ets // 自定义组件-二维码位置标记
239 | |---CommonTipsDialog.ets // 自定义组件-未开通相机权限引导弹框
240 | |---MaskLayer.ets // 自定义组件-二维码位置标记蒙层
241 | |---ScanLine.ets // 自定义组件-二维码扫码的扫描线
242 | |---ScanTitle.ets // 自定义组件-二维码扫码的标题
243 | |---CustomScanCameraComp.ets // 自定义组件-二维码扫描相机流组件
244 | |---CustomScanCtrlComp.ets // 自定义组件-二维码扫描控制菜单组件
245 |---model
246 | |---PermissionModel.ets // 模型层-权限控制管理器
247 | |---WindowModel.ets // 模型层-窗口管理器
248 |---pages
249 | |---CustomScanPage.ets // 展示层-二维码扫描页面
250 | |---DetectBarcodePage.ets // 展示层-二维码扫描结果页面
251 |---viewmodel
252 | |---CustomScanViewModel.ets // 控制层-二维码扫描控制器
253 ```
254
255### 参考资料
256
257- [属性动画](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-animatorproperty.md)
258- [程序访问控制管理](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-ability-kit/js-apis-abilityAccessCtrl.md)
259
260### 相关权限
261
262[ohos.permission.CAMERA](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/security/AccessToken/permissions-for-all-user.md)
263[ohos.permission.INTERNET](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/security/AccessToken/permissions-for-all.md)
264
265### 依赖
266
267不涉及
268
269### 约束和限制
270
2711. 本示例仅支持标准系统上运行。
2722. 本示例已适配API version 12版本SDK。
2733. 本示例需要使用DevEco Studio 5.0.0 Release及以上版本才可编译运行。
274
275### 下载
276
277如需单独下载本工程,执行如下命令:
278```shell
279git init
280git config core.sparsecheckout true
281echo /code/BasicFeature/Media/CustomScan/ > .git/info/sparse-checkout
282git remote add origin https://gitee.com/openharmony/applications_app_samples.git
283git pull origin master
284```