OpenHarmony應用實現(xiàn)二維碼掃碼識別
概念介紹
二維碼的應用場景非常廣泛,在購物應用中,消費者可以直接掃描商品二維碼,瀏覽并購買產品,如圖是購物應用的掃描二維碼的頁面。
本文就以橘子購物示例應用為例,來講解OpenHarmony應用二維碼開發(fā)相關的技術點。
我們先看下二維碼相關的幾個概念。
- 二維碼生成
OpenHarmony應用框架提供了QRCode組件,用于顯示單個二維碼的組件。該組件只能用于顯示二維碼,無法顯示條碼與解析碼內容。
- 二維碼解析
OpenHarmony提供了功能強大的三方庫 @ohos/zxing,是一個解析/生成一維碼/二維碼的庫。詳細內容可以參考@ohos/zxing。
二維碼解析時,通常有兩種方式,使用相機拍攝獲取圖片或打開相冊選取圖片,然后圖片解析合適的圖片格式,進行二維碼解析。
橘子購物示例應用掃描二維碼的示例圖:
OpenHarmony應用實現(xiàn)二維碼掃碼識別-開源基礎軟件社區(qū)
配置文件
了解了二維碼相關的概念后,我們看下橘子購物示例應用的oh-package.json5配置文件。
在橘子購物示例應用中,實現(xiàn)首頁二維碼掃描的頁面的文件位置為:entry/src/main/ets/pages/ScanPage.ets。文件內容如下:
import { QRCodeScanComponent } from "@ohos/scan-component"
@Entry
@Component
struct Scan {
build() {
Column() {
QRCodeScanComponent()
}
}
}
內容非常簡單,主要是導入的自定義組件QRCodeScanComponent,這個組件的代碼來自:二維碼掃描示例應用,后文我們這樣分析如何開發(fā)這個二維碼掃描應用。
從這一行,可以了解到OpenHarmony應用如何引用ohpm本地三方庫。
"@ohos/scan-component": "file:../libs/ohos-qr-code-scan-1.0.1.har",
oh-package.json5配置文件片段如下:
{
"license": "ISC",
"devDependencies": {},
"name": "product",
"description": "example description",
"repository": {},
"version": "1.0.0",
"dependencies": {
"@ohos/http": "file:../libs/ohos-http-1.0.0.tgz",
"@ohos/video-component": "file:../libs/ohos-video-component-1.0.5.tgz",
"@ohos/details-page-component": "file:../feature/detailPage",
"@ohos/notification": "file:../libs/ohos-notification-1.0.0.tgz",
"@ohos/scan-component": "file:../libs/ohos-qr-code-scan-1.0.1.har",
"@ohos/updatedialog": "file:../libs/ohos-updatedialog-1.0.0.tgz",
"@ohos/enter-animation": "file:../libs/ohos-enter-animation-1.0.1.tgz",
"@ohos/share-component": "file:../libs/ohos-sharecomponent-1.0.1.tgz",
"@ohos/emitter": "file:../feature/emitter",
"@ohos/navigation-component": "file:../feature/navigationHome"
}
}
開發(fā)步驟
我們來看二維碼掃描功能是如何開發(fā)的。
導入ohpm三方庫
在開發(fā)前,我們需要導入ohpm組件庫:@ohos/zxing??梢允褂妹钚蟹绞綄雘hpm install @ohos/zxing,也可以直接在文件entry\oh-package.json5中配置,如文件片段所示。
可以看出,二維碼掃描的核心代碼存放在Feature目錄,是一個獨立的module模塊,方便復用:
“@ohos/feature-qr-code-scan”: “file:…/Feature”。
文件entry\oh-package.json5片段:
"dependencies": {
"@ohos/feature-qr-code-scan": "file:../Feature",
"@ohos/zxing": "^2.0.0"
}
相機服務
CameraService.ets文件相機服務構造函數中,會創(chuàng)建一個圖片接收器。
該圖片接收器可以監(jiān)聽’imageArrival’事件,當相機拍照時會觸發(fā)該事件。在監(jiān)聽事件的回調函數里,實現(xiàn)對拍照的圖片進行處理。
CameraService.ets文件相機服務構造函數:
constructor(imgReceiver?: image.ImageReceiver) {
if (imgReceiver === undefined) {
this.imageReceiver = image.createImageReceiver(QRCodeScanConst.IMG_DEFAULT_SIZE.WIDTH,
QRCodeScanConst.IMG_DEFAULT_SIZE.HEIGHT, image.ImageFormat.JPEG, QRCodeScanConst.MAX_IMAGE_CAPACITY)
} else {
this.imageReceiver = image.createImageReceiver(imgReceiver.size.width, imgReceiver.size.height,
imgReceiver.format, imgReceiver.capacity)
}
}
在CameraService.ets文件創(chuàng)建相機函數中,主要包含如下幾個步驟:
- 獲取支持的相機
根據context獲取CameraManager,然后獲取支持的相機(攝像頭)。如果沒有支持的相機,則然后。
如有支持的相機,則默認使用相機列表中的第一個。實際應用中,對于二維碼掃描,需要使用后置相機攝像頭。
- 獲取相機輸入輸出流
首先,根據指定的相機,創(chuàng)建相機輸入流this.cameraInput。
然后,獲取相機的cameraOutputCapability參數,接著創(chuàng)建兩個輸出流:
- 預覽輸出流
創(chuàng)建相機預覽輸出流this.previewOutput,使用的surfaceId來自XComponent組件。預覽輸出流,對應相機拍照前的圖片預覽。
- 相片輸出流
創(chuàng)建相片輸出流this.photoOutput,使用的receivingSurfaceId來自上文創(chuàng)建的圖片接收器。相片輸出流,用于保存到相片。
- 配置相機會話
配置相機會話,也比較簡單,添加輸入流和輸出流即可,見代碼及其注釋。
CameraService.ets文件創(chuàng)建相機函數:
/**
* 創(chuàng)建相機
*/
async createCamera(surfaceId: string) {
Logger.info("createCamera start")
// 根據context獲取CameraManager
let cameraManager = camera.getCameraManager(AppStorage.Get('context'))
// 獲取Camera對象數組
let cameras = cameraManager.getSupportedCameras()
// 沒有相機就停止
if (cameras.length === 0) {
Logger.error("createCamera: cameras length is 0.")
return
}
// 拿到相機列表中的第一個默認相機id, 根據id獲取相機輸入流
this.cameraInput = cameraManager.createCameraInput(cameras[0])
this.cameraInput.open()
// 獲取cameraOutputCapability參數
let cameraOutputCapability = cameraManager.getSupportedOutputCapability(cameras[0])
// 獲取相機輸出流
this.previewOutput = cameraManager.createPreviewOutput(cameraOutputCapability.previewProfiles[0], surfaceId)
// 獲取一個可以創(chuàng)建相片輸出流的id
let receivingSurfaceId = await this.imageReceiver.getReceivingSurfaceId()
// 創(chuàng)建相片輸出流
this.photoOutput = cameraManager.createPhotoOutput(cameraOutputCapability.photoProfiles[0], receivingSurfaceId)
// 獲取捕獲會話的實例
this.captureSession = cameraManager.createCaptureSession()
// 開始會話配置
this.captureSession.beginConfig()
// 使用相機輸入流---添加一個攝像頭輸入流
this.captureSession.addInput(this.cameraInput)
// 使用相機輸出流---添加一個攝像頭輸出
this.captureSession.addOutput(this.previewOutput)
// 使用相片輸出流---添加相機照片的輸出
this.captureSession.addOutput(this.photoOutput)
// 結束并提交配置
await this.captureSession.commitConfig()
// 開始捕獲會話
await this.captureSession.start()
Logger.info("createCamera end")
}
CameraService.ets文件拍照函數中,指定相片參數設置,然后調用capture()函數完成拍照。
拍照后會觸發(fā)圖片接收器的’imageArrival’事件。拍照函數在使用相機掃描二維碼的時候調用。
該圖片接收器可以監(jiān)聽’imageArrival’事件,當相機拍照時會觸發(fā)該事件。在監(jiān)聽事件的回調函數里,實現(xiàn)對拍照的圖片進行處理。
CameraService.ets文件拍照函數:
takePicture() {
let photoSetting = {
rotation: camera.ImageRotation.ROTATION_0,
quality: camera.QualityLevel.QUALITY_LEVEL_MEDIUM,
mirror: false
}
this.photoOutput.capture(photoSetting)
}
二維碼解析實現(xiàn)代碼
二維碼解析類文件為:QRCodeParser.ets,支持拍照識別二維碼,還支持從相冊選擇二維碼圖片進行識別。
我們首先看下如何解析從相機獲取的二維碼圖片,對應函數為:parseQRCodeImageFromCamera,該類指定一個時間隨機的圖片文件名,圖片歸檔格式,然后繼續(xù)調用函數parseQRCodeImageWithNameFromCamera。
/**
* 解析從相機獲取的二維碼圖片
*
* @param cameraService
* @param canvasContext
*/
parseQRCodeImageFromCamera(cameraService: CameraService,
imageComponentType?: image.ComponentType): void {
Logger.info("parseQRCodeImageFromCamera start")
let fileName = this.getRandomFileName(QRCodeScanConst.IMG_FILE_PREFIX, QRCodeScanConst.IMG_SUFFIX_JPG)
this.parseQRCodeImageWithNameFromCamera(cameraService, fileName, imageComponentType);
Logger.info("parseQRCodeImageFromCamera end")
}
在函數parseQRCodeImageWithNameFromCamera中,注冊圖片接收器監(jiān)聽’imageArrival’事件,在監(jiān)聽函數里,對二維碼圖片進行解析識別。
當相機對二維碼拍照后,二維碼圖片會被保存到指定的目錄下,返回文件URI。保存圖片的函數createPublicDirFileAsset的實現(xiàn),可以自行查閱源碼。
根據返回的圖片URI,調用函數parseImageQRCode對二維碼進行解析。函數parseImageQRCode后文會介紹。
如果解析失敗,彈窗提示解析失敗。如果解析成功,會被解析結果保存到AppStorage。
保存到AppStorage的二維碼解析結果會被@watch裝飾器的變量監(jiān)視,當監(jiān)視到有二維碼識別結果后,會在界面展示,后文會介紹。
QRCodeParser.ets文件parseQRCodeImageWithNameFromCamera函數代碼:
/**
* 解析從相機獲取的二維碼圖片,指定文件名稱
*
* @param cameraService
* @param canvasContext
*/
parseQRCodeImageWithNameFromCamera(cameraService: CameraService,
fileDisplayName: string,
imageComponentType?: image.ComponentType): void {
Logger.info("parseQRCodeImageWithNameFromCamera...")
cameraService.imageReceiver.on('imageArrival', async () => {
Logger.info("parseQRCodeImageWithNameFromCamera imageArrival start")
// 從接收器獲取下一個圖像,并返回結果
let targetImage: image.Image = await cameraService.imageReceiver.readNextImage()
// 默認按JPEG格式處理
let imgComponentType = imageComponentType === undefined ? image.ComponentType.JPEG : imageComponentType
let imageComponent = await targetImage.getComponent(imgComponentType)
// 將image的ArrayBuffer寫入指定文件中,返回文件uri
let imageUri = await this.createPublicDirFileAsset(fileDisplayName, mediaLibrary.MediaType.IMAGE,
mediaLibrary.DirectoryType.DIR_IMAGE, imageComponent.byteBuffer);
// 釋放已讀取的image資源,以便處理下一個資源
await targetImage.release()
// 解析二維碼
let qrCodeParseRlt = await this.parseImageQRCode(imageUri);
if (!qrCodeParseRlt.isSucess) {
Logger.error("parseQRCodeImageWithNameFromCamera qrCodeParseRlt is null")
prompt.showToast({
message: $r('app.string.qrCodeNotRecognized')
})
return;
}
// 拼接解析結果
AppStorage.SetOrCreate(QRCodeScanConst.QR_CODE_PARSE_RESULT, qrCodeParseRlt.decodeResult);
Logger.info("parseQRCodeImageWithNameFromCamera imageArrival end")
})
}
二維碼解析類文件為:QRCodeParser.ets,支持拍照識別二維碼,還支持從相冊選擇二維碼圖片進行識別。
我們接著,再看下如何解析從相冊里挑選的二維碼圖片。
參數imageSrc為選定圖片的URI地址。
getImageSource()代碼可以自行查詢,實現(xiàn)根據圖片URI返回圖片的寬、高,以及圖片的pixelMap數據。然后,把像素數據寫入ArrayBuffer,供zxing二維碼識別程序使用。
函數RGBLuminanceSource、BinaryBitmap、BinaryBitmap等都是zxing的類。通過調用MultiFormatReader的decode函數對二維碼圖像進行解析。
如果解析成功,會返回成功的標記和解析的結果。
如果解析失敗,會在catch語句塊里進行處理,會返回失敗的標記和解析失敗的原因。
QRCodeParser.ets文件parseImageQRCode函數代碼:
/**
* 解析圖片二維碼信息
* @param canvasContext
* @param imageSrc
*/
async parseImageQRCode(imageSrc: string): Promise<DecodeResultAttribute> {
Logger.info(`parseImageQRCode start`);
// 獲取圖片的寬高
let imageSource = await this.getImageSource(imageSrc);
let imageWidth = imageSource.width;
let imageHeight = imageSource.height;
// 獲取PixelMap圖片數據
let pixMapData = imageSource.pixelMap;
let pixelBytesNumber = pixMapData.getPixelBytesNumber();
let arrayBuffer: ArrayBuffer = new ArrayBuffer(pixelBytesNumber);
// 讀取圖像像素數據,結果寫入ArrayBuffer里
await pixMapData.readPixelsToBuffer(arrayBuffer);
let int32Array = new Int32Array(arrayBuffer);
let luminanceSource = new RGBLuminanceSource(int32Array, imageWidth, imageHeight);
let binaryBitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
let mltiFormatReader = new MultiFormatReader();
let hints = new Map();
hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.QR_CODE]);
mltiFormatReader.setHints(hints);
try {
// 解析二維碼
let decodeResult = mltiFormatReader.decode(binaryBitmap);
let decodeText = decodeResult.getText();
Logger.info(`parseImageQRCode end ${decodeText}`);
return { isSucess: true, decodeResult: decodeText };
} catch (err) {
let error = `The error is ${err}`;
Logger.info(`parseImageQRCode end`);
return { isSucess: false, decodeResult: error };
}
}
相機掃描識別二維碼
在文件QRCodeScanComponent.ets中實現(xiàn)了二維碼掃描自定義組件。我們看下該文件中如何實現(xiàn)相機掃描二維碼的。
在二維碼掃描組件的aboutToAppear()函數調用的watchCameraPermission()函數,用于使用相機掃描二維碼進行識別。
在watchCameraPermission()函數中,使用setInterval函數每100ms判斷下是否具有相機權限,當有相機權限的時候,才能使用相機掃描二維碼。
當具備相機權限時 ,使用setInterval函數每4000ms輪詢判斷下是否識別到二維碼圖片,如果識別到則取消執(zhí)行輪詢。
如果沒有識別到二維碼,則繼續(xù)調用函數takePicture()拍照。調用該函數后,會觸發(fā)圖片接收器的監(jiān)聽事件’imageArrival’,對這個事件的監(jiān)聽分析,見上文。
文件QRCodeScanComponent.ets中,相機拍照識別二維碼的代碼片段:
aboutToAppear() {
// 監(jiān)聽相機權限
this.watchCameraPermission()
// 設置掃描動畫
this.setQRCodeScanAnimation()
// 解析二維碼圖片信息
this.qrCodeParser.parseQRCodeImageFromCamera(this.cameraService);
}
......
// 監(jiān)聽相機權限變化
watchCameraPermission() {
let interval = setInterval(() => {
this.hasCameraPermission = AppStorage.Get(QRCodeScanConst.HAS_CAMERA_PERMISSION)
if (this.hasCameraPermission) {
let qrCodeScanInterval = setInterval(() => {
if (this.qrCodeParseResult.length > 0 || this.isQRCodeScanStopped) {
clearInterval(qrCodeScanInterval)
}
// 拍照
this.cameraService.takePicture()
}, 4000)
clearInterval(interval)
}
}, 100)
}
識別相冊二維碼圖片
在文件QRCodeScanComponent.ets中實現(xiàn)了二維碼掃描自定義組件。我們看下該文件中如何識別相冊二維碼圖片。
首先,設置this.isQRCodeScanStopped為true,這個會關閉相機拍照識別二維碼。
然后,通過startAbilityForResult啟動相冊應用,供用戶選擇二維碼圖片。
如果選擇圖片失敗,則彈窗報錯。
如果選擇圖片成功,則調用二維碼解碼函數parseImageQRCode完成對圖片二維碼的識別。
如果識別二維碼成功,則彈窗展示二維碼結果。
如果識別識別,則toast展示:未識別到二維碼。
文件QRCodeScanComponent.ets中,相冊選擇二維碼圖片進行識別代碼片段:
Image($r('app.media.scan_photo'))
.width(30)
.height(30)
.id('scanPhoto')
.onClick(async () => {
// 打開相冊獲取圖片
this.isQRCodeScanStopped = true
let context = AppStorage.Get('context') as common.UIAbilityContext
await context.startAbilityForResult({
parameters: { uri: 'singleselect' },
bundleName: 'com.ohos.photos',
abilityName: 'com.ohos.photos.MainAbility',
}).then(data => {
// 獲取want數據
let want = data['want'];
if (want) {
// param代表want參數中的paramters
let param = want['parameters'];
if (param) {
// 被選中的圖片路徑media/image/8
let selectedUri = param['select-item-list'];
setTimeout(async () => {
if (!selectedUri) {
prompt.showToast({
message: $r('app.string.queryImageFailed'),
duration: 1000
})
return;
}
// 獲取解析數據
let qrCodeParseRlt = await this.qrCodeParser.parseImageQRCode(selectedUri[0]);
if (qrCodeParseRlt.isSucess) {
prompt.showDialog({
title: $r('app.string.qrcodeResult'),
message: qrCodeParseRlt.decodeResult
})
} else {
prompt.showToast({
message: $r('app.string.qrCodeNotRecognized')
})
}
}, 50)
}
}
})
})
二維碼掃描組件界面
在文件QRCodeScanComponent.ets中實現(xiàn)了二維碼掃描自定義組件。我們看下二維碼掃描組件的頁面布局。
整個頁面使用Stack進行堆疊布局。
如果有相機權限,會XComponent組件,用于展示相機的預覽輸出流。XComponent組件的onLoad函數里會創(chuàng)建相機,onDestroy函數里會釋放相機。
Image($r('app.media.scan_border'))圖片就是二維碼掃描框,引導用戶把二維碼放到框內進行掃描識別。
Divider是個分割線,該分割線使能了動畫效果,在識別二維碼的過程中,分割線從二維碼識別框里從上到下移動。掃描動畫實現(xiàn)代碼如下:
// 掃描掃描動畫
setQRCodeScanAnimation() {
setInterval(() => {
animateTo({
duration: 1000, // 動畫時間
tempo: 0.5, // 動畫速率
curve: Curve.EaseInOut,
delay: 200, // 動畫延遲時間
iterations: -1, // 動畫是否重復播放
playMode: PlayMode.Normal,
}, () => {
this.animationOrdinate = 390 // 掃描動畫結束Y坐標
})
}, 2000)
}
Text($r('app.string.putTheQRCodeToScan'))引導用戶把二維碼放到框內進行掃描識別。
Image($r('app.media.scan_back'))返回退出應用。
Image($r('app.media.scan_photo'))從相冊里挑選二維碼圖片進行識別。
build() {
Column() {
Stack() {
if (this.hasCameraPermission) {
XComponent({
id: 'componentId',
type: 'surface',
controller: this.xComponentController
})
.onLoad(() => {
// 適配可能需要獲取設備信息
this.xComponentController.setXComponentSurfaceSize({
surfaceWidth: QRCodeScanConst.IMG_DEFAULT_SIZE.WIDTH,
surfaceHeight: QRCodeScanConst.IMG_DEFAULT_SIZE.HEIGHT
})
this.surFaceId = this.xComponentController.getXComponentSurfaceId()
this.cameraService.createCamera(this.surFaceId)
})
.onDestroy(() => {
this.cameraService.releaseCamera()
})
.height('100%')
.width('100%')
}
Column() {
Column() {
Image($r('app.media.scan_border'))
......
Divider()
.strokeWidth(1)
.height(4)
.width('100%')
.color(Color.White)
.width('100%')
.position({ x: 0, y: 0 })
.translate({ x: 0, y: this.animationOrdinate })
}
......
Text($r('app.string.putTheQRCodeToScan'))
......
}
......
Row() {
Image($r('app.media.scan_back'))
......
Row({ space: 16 }) {
Image($r('app.media.scan_photo'))
......
}
運行測試效果
可以下載橘子購物示例應用代碼,使用DevEco Studio編譯構建,使用Simulator模擬器或者真實設備進行運行體驗??梢泽w驗下使用相機對二維碼圖片進行識別,還可以嘗試下識別相冊中的二維碼圖片。
git init
git config core.sparsecheckout true
echo code/Solutions/Shopping/OrangeShopping/ > .git/info/sparse-checkout
git remote add origin https://gitee.com/openharmony/applications_app_samples.git
git pull origin master
注意事項
當前二維碼示例應用識別相冊的二維碼,彈出識別結果后,程序會崩潰,已經提單跟蹤。示例程序待改進。
使用相機功能直接拍攝二維碼的功能,一直沒有成功運行,需要進一步優(yōu)化。