Trong quá trình phát triển ứng dụng HarmonyOS, khi triển khai các tính năng yêu cầu sử dụng camera như quét mã, camera tùy chỉnh, hoặc nhận dạng khuôn mặt, bạn có thể gặp phải tình trạng màn hình camera hiển thị đen. Vấn đề này chủ yếu do tài nguyên camera không được giải phóng đúng cách. Tình huống phổ biến xảy ra khi bạn sử dụng camera trên một trang, sau đó chuyển sang trang khác cũng sử dụng camera mà không giải phóng tài nguyên từ trang trước.
Để giải quyết, bạn cần đảm bảo giải phóng tài nguyên camera không chỉ khi trang bị hủy mà còn khi trang bị ẩn hoặc không còn hiển thị. Việc xử lý trong phương thức onPageHide sẽ giúp tránh xung đột tài nguyên khi trang mới cũng cần sử dụng camera.
async onPageHidden() {
// Ngừng và giải phóng luồng camera khi trang bị ẩn
this.isPermissionGranted = false;
this.isFlashOn = false;
this.isAutoLightOn = false;
try {
cameraManager.off('lightingFlash');
} catch (error) {
hilog.error(0x0001, this.LOG_TAG, `Không thể tắt lightingFlash. Mã lỗi: ${error.code}, thông báo: ${error.message}`);
}
await cameraManager.stop();
// Gọi API giải phóng luồng camera tùy chỉnh
cameraManager.release().then(() => {
hilog.info(0x0001, this.LOG_TAG, 'Giải phóng cameraManager thành công.');
}).catch((error: Error) => {
hilog.error(0x0001, this.LOG_TAG, `Không thể giải phóng cameraManager. Mã lỗi: ${error.code}, thông báo: ${error.message}`);
})
}
Dưới đây là ví dụ mã nguồn hoàn chỉnh, đã được tái cấu trúc và đổi tên biến để minh bạch hóa logic.
import { cameraManager, scanBarcode, scanCore, detectBarcode } from '@kit.ScanKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { Error } from '@kit.BasicServicesKit'
import { abilityAccessCtrl, common } from '@kit.AbilityKit'
import { display, Toast } from '@kit.ArkUI'
import { photoAccessHelper } from '@kit.MediaLibraryKit';
@Builder
export function CameraScanPageBuilder(name: string, param: object){
if(isLog(name, param)){
CameraScanPage()
}
}
function isLog(name: string, param: object){
console.log("CameraScanPageBuilder", " CameraScanPageBuilder init name: " + name);
return true;
}
@Entry
@Component
export struct CameraScanPage {
private LOG_TAG: string = '[CameraScanPage]';
@State isPermissionGranted: boolean = false // Trạng thái quyền truy cập camera
@State previewSurfaceId: string = '' // ID của component XComponent
@State hasScannedResult: boolean = false // Trạng thái đã quét được kết quả
@State isFlashOn: boolean = false // Trạng thái đèn flash
@State isAutoLightOn: boolean = false // Trạng thái cảm biến ánh sáng
@State previewHeight: number = 480 // Chiều cao luồng preview, đơn vị: vp
@State previewWidth: number = 300 // Chiều rộng luồng preview, đơn vị: vp
@State previewOffsetX: number = 0 // Độ lệch trục X của luồng preview, đơn vị: vp
@State previewOffsetY: number = 0 // Độ lệch trục Y của luồng preview, đơn vị: vp
@State currentZoom: number = 1 // Tỷ lệ zoom hiện tại
@State targetZoom: number = 1 // Tỷ lệ zoom đã cài đặt
@State screenScale: number = 1 // Tỷ lệ co giãn màn hình
@State pinchScale: number = 1 // Tỷ lệ co giãn bằng hai ngón tay
@State screenHeight: number = 0 // Chiều cao màn hình, đơn vị vp
@State screenWidth: number = 0 // Chiều rộng màn hình, đơn vị vp
@State scanData: Array<ScanResult> = [] // Kết quả quét
private xComponentController: XComponentController = new XComponentController()
async onPageVisible() {
// Bước 1: Yêu cầu quyền truy cập camera
await this.requestCameraAccess();
// Bước 2: Cấu hình kích thước layout preview
this.configurePreviewLayout();
// Bước 3: Khởi tạo các cài đặt quét
this.initializeScanConfig();
}
private initializeScanConfig(){
let options: scanBarcode.ScanOptions = {
scanTypes: [scanCore.ScanType.ALL],
enableMultiMode: true,
enableAlbum: true
}
cameraManager.init(options);
}
async onPageHidden() {
// Ngừng và giải phóng luồng camera khi trang bị ẩn
this.isPermissionGranted = false;
this.isFlashOn = false;
this.isAutoLightOn = false;
try {
cameraManager.off('lightingFlash');
} catch (error) {
hilog.error(0x0001, this.LOG_TAG, `Không thể tắt lightingFlash. Mã lỗi: ${error.code}, thông báo: ${error.message}`);
}
await cameraManager.stop();
// Gọi API giải phóng luồng camera tùy chỉnh
cameraManager.release().then(() => {
hilog.info(0x0001, this.LOG_TAG, 'Giải phóng cameraManager thành công.');
}).catch((error: Error) => {
hilog.error(0x0001, this.LOG_TAG, `Không thể giải phóng cameraManager. Mã lỗi: ${error.code}, thông báo: ${error.message}`);
})
}
/**
* Chọn ảnh từ album
*/
selectPhotoFromGallery = ()=>{
try {
let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
// Thiết lập bộ lọc MIME type
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
// Số lượng ảnh tối đa người dùng có thể chọn
photoSelectOptions.maxSelectNumber = 1;
// Khởi tạo trình chọn ảnh
let photoPicker = new photoAccessHelper.PhotoViewPicker();
// Mở thành phần album an toàn
photoPicker.select(photoSelectOptions, (err: Error, photoSelectResult: PhotoSelectResult) => {
if (err) {
console.error(this.LOG_TAG, "Lỗi khi chọn ảnh: " + JSON.stringify(err));
return;
}
// Callback khi người dùng xác nhận chọn ảnh
console.info(this.LOG_TAG, "Chọn ảnh thành công: " + JSON.stringify(photoSelectResult));
// Vì chỉ chọn 1 ảnh, lấy ảnh đầu tiên
this.decodeImageFromUri(photoSelectResult.photoUris[0]);
});
} catch (error) {
let err: Error = error as Error;
console.error(this.LOG_TAG, "Bắt lỗi khi chọn ảnh: " + JSON.stringify(err));
}
}
/**
* Giải mã dữ liệu mã vạch từ ảnh
*/
private decodeImageFromUri(uri: string){
if(uri){
let inputImg: detectBarcode.InputImage = {
uri: uri,
};
let setting: scanBarcode.ScanOptions = {
scanTypes: [
scanCore.ScanType.ALL
],
// Bật nhận dạng nhiều mã
enableMultiMode: true,
enableAlbum: true,
};
try {
// Gọi API giải mã ảnh
detectBarcode.decode(inputImg, setting).then((result: Array<ScanResult>) => {
console.info(this.LOG_TAG, "Kết quả giải mã: " + JSON.stringify(result))
// Nếu có kết quả
if(result.length > 0){
// Nếu có nhiều hơn 1 mã
this.scanData = result;
this.hasScannedResult = true;
if(result.length > 1){
}else{
this.displayScanResult(result[0]);
}
}else{
Toast.show({
message: "Không tìm thấy dữ liệu mã vạch!"
});
}
}).catch((error: Error) => {
console.error(this.LOG_TAG, "Lỗi khi giải mã: " + JSON.stringify(error))
});
} catch (error) {
console.error(this.LOG_TAG, "Bắt lỗi khi giải mã: " + JSON.stringify(error))
}
}else{
Toast.show({
message: "Dữ liệu ảnh không hợp lệ! " + uri
});
}
}
/**
* Yêu cầu quyền truy cập từ người dùng
* @returns
*/
async requestRequiredPermissions(): Promise {
hilog.info(0x0001, this.LOG_TAG, 'Bắt đầu yêu cầu quyền');
let context = getContext() as common.UIAbilityContext;
let atManager = abilityAccessCtrl.createAtManager();
let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
return grantStatus.authResults;
}
/**
* Yêu cầu quyền truy cập camera
*/
async requestCameraAccess() {
let grantStatus = await this.requestRequiredPermissions();
for (let i = 0; i < grantStatus.length; i++) {
if (grantStatus[i] === 0) {
console.log(this.LOG_TAG, "Yêu cầu quyền thành công.");
this.isPermissionGranted = true;
}
}
}
// Cấu hình layout preview cho màn hình dọc, ví dụ toàn màn hình
configurePreviewLayout() {
// Thiết bị gập không xác định hoặc ở trạng thái gập
if(display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_UNKNOWN || display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_FOLDED){
// Mặc định màn hình dọc
let displayClass = display.getDefaultDisplaySync();
this.screenHeight = px2vp(displayClass.height);
this.screenWidth = px2vp(displayClass.width);
}else{
// Thiết bị gập mở rộng hoặc bán mở
let displayClass = display.getDefaultDisplaySync();
let tempHeight = px2vp(displayClass.height);
let tempWidth = px2vp(displayClass.width);
console.info("debugDisplay", 'tempHeight: ' + tempHeight + " tempWidth: " + tempWidth);
this.screenHeight = tempHeight + px2vp(8);
this.screenWidth = ( tempWidth - px2vp(64) ) / 2;
}
console.info("debugDisplay", 'Kích thước màn hình cuối cùng: ' + this.screenHeight + " " + this.screenWidth);
let maxLen: number = Math.max(this.screenWidth, this.screenHeight);
let minLen: number = Math.min(this.screenWidth, this.screenHeight);
const RATIO: number = 16 / 9;
this.previewHeight = maxLen;
this.previewWidth = maxLen / RATIO;
this.previewOffsetX = (minLen - this.previewWidth) / 2;
}
// Hiển thị kết quả quét bằng toast
async displayScanResult(result: ScanResult) {
Toast.show({
message: JSON.stringify(result),
duration: 5000
});
}
/**
* Khởi động camera
*/
private initiateCameraPreview() {
this.hasScannedResult = false;
this.scanData = [];
let viewControl: cameraManager.ViewControl = {
width: this.previewWidth,
height: this.previewHeight,
surfaceId : this.previewSurfaceId
};
// Bước 4: Yêu cầu dịch vụ quét, sử dụng Promise để callback
try {
cameraManager.start(viewControl)
.then(async (result: Array<ScanResult>) => {
console.error(this.LOG_TAG, 'Kết quả: ' + JSON.stringify(result));
if (result.length) {
this.scanData = result;
this.hasScannedResult = true;
// Tạm dừng luồng camera sau khi có kết quả
try {
cameraManager.stop().then(() => {
console.info(this.LOG_TAG, 'Tạm dừng quét thành công.');
}).catch((error: Error) => {
console.error(this.LOG_TAG, 'Lỗi khi tạm dừng quét: ' + JSON.stringify(error));
});
} catch (error) {
console.error(this.LOG_TAG, 'Lỗi cameraManager.stop: ' + JSON.stringify(error));
}
}
}).catch((error: Error) => {
console.error(this.LOG_TAG, 'Lỗi cameraManager.start: ' + JSON.stringify(error));
});
} catch (err) {
console.error(this.LOG_TAG, 'Lỗi cameraManager.start: ' + JSON.stringify(err));
}
}
/**
* Đăng ký lắng nghe sự kiện đèn flash
*/
private setupFlashListener(){
cameraManager.on('lightingFlash', (error, isLightingFlash) => {
if (error) {
console.info(this.LOG_TAG, "Lỗi lightingFlash: " + JSON.stringify(error));
return;
}
if (isLightingFlash) {
this.isFlashOn = true;
} else {
if (!cameraManager?.getFlashLightStatus()) {
this.isFlashOn = false;
}
}
this.isAutoLightOn = isLightingFlash;
});
}
// Giao diện quét tùy chỉnh với nút quay lại và hướng dẫn
@Builder
HeaderSection() {
Column() {
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('Quay lại')
.onClick(async () => {
Navigation.pop();
})
}.padding({ left: 24, right: 24, top: 40 })
Column() {
Text('Quét mã QR/Barcode')
Text('Hướng camera về mã cần quét để tự động nhận dạng')
}.margin({ left: 24, right: 24, top: 24 })
}
.height(146)
.width('100%')
}
@Builder CameraPreviewView(){
XComponent({
id: 'componentId',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.onLoad(async () => {
// Lấy surfaceId của component XComponent
this.previewSurfaceId = this.xComponentController.getXComponentSurfaceId();
console.info(this.LOG_TAG, "Lấy surfaceId thành công: " + this.previewSurfaceId);
this.initiateCameraPreview();
this.setupFlashListener();
})
.width(this.previewWidth)
.height(this.previewHeight)
.position({ x: this.previewOffsetX, y: this.previewOffsetY })
}
@Builder MainScanView(){
Stack() {
Column() {
if (this.isPermissionGranted) {
this.CameraPreviewView()
}
}
.height('100%')
.width('100%')
.backgroundColor(Color.Red)
Column() {
this.HeaderSection()
Column() {
}
.layoutWeight(1)
.width('100%')
Column() {
Row() {
// Nút đèn flash, chỉ hiển thị khi camera đang chạy
Button('Đèn flash')
.onClick(() => {
if (cameraManager.getFlashLightStatus()) {
cameraManager.closeFlashLight();
setTimeout(() => {
this.isFlashOn = this.isAutoLightOn;
}, 200);
} else {
cameraManager.openFlashLight();
}
})
.visibility((this.isPermissionGranted && this.isFlashOn) ? Visibility.Visible : Visibility.None)
// Sau khi quét thành công, nhấn nút để quét lại
Button('Quét lại')
.onClick(() => {
try {
cameraManager.rescan();
} catch (error) {
console.error(this.LOG_TAG, 'Lỗi cameraManager.rescan: ' + JSON.stringify(error));
}
// Khởi động lại luồng camera để quét
this.initiateCameraPreview();
})
.visibility(this.hasScannedResult ? Visibility.Visible : Visibility.None)
// Chọn ảnh từ album để giải mã
Button('Album')
.onClick(() => {
this.selectPhotoFromGallery();
})
}
Row() {
// Cài đặt tỷ lệ zoom
Button('Tỷ lệ zoom, hiện tại: ' + this.targetZoom)
.onClick(() => {
if (!this.hasScannedResult) {
if (!this.currentZoom || this.currentZoom === this.targetZoom) {
this.targetZoom = cameraManager.getZoom();
} else {
this.currentZoom = this.currentZoom;
cameraManager.setZoom(this.currentZoom);
setTimeout(() => {
if (!this.hasScannedResult) {
this.targetZoom = cameraManager.getZoom();
}
}, 1000);
}
}
})
}
.margin({ top: 10, bottom: 10 })
Row() {
// Nhập tỷ lệ zoom mong muốn
TextInput({ placeholder: 'Nhập tỷ lệ zoom' })
.type(InputType.Number)
.borderWidth(1)
.backgroundColor(Color.White)
.onChange(value => {
this.currentZoom = Number(value);
})
}
}
.width('50%')
.height(180)
}
// Hiển thị vị trí mã quét. Nhấn vào để xem thông tin
ForEach(this.scanData, (item: ScanResult, index: number) => {
if (item.scanCodeRect) {
Image($r("app.media.icon_select_dian"))
.width(20)
.height(20)
.markAnchor({ x: 20, y: 20 })
.position({
x: (item.scanCodeRect.left + item?.scanCodeRect?.right) / 2 + this.previewOffsetX,
y: (item.scanCodeRect.top + item?.scanCodeRect?.bottom) / 2 + this.previewOffsetY
})
.onClick(() => {
this.displayScanResult(item);
})
}
})
}
.width('100%')
.height('100%')
.onClick((event: ClickEvent) => {
if (this.hasScannedResult) {
return;
}
// Lấy tọa độ chạm (x,y) và đặt điểm lấy nét
let x1 = vp2px(event.displayY) / (this.screenHeight + 0.0);
let y1 = 1.0 - (vp2px(event.displayX) / (this.screenWidth + 0.0));
cameraManager.setFocusPoint({ x: x1, y: y1 });
hilog.info(0x0001, this.LOG_TAG, `Thiết lập điểm lấy nét thành công x1: ${x1}, y1: ${y1}`);
// Đặt chế độ lấy nét tự động liên tục
setTimeout(() => {
cameraManager.resetFocus();
}, 200);
}).gesture(PinchGesture({ fingers: 2 })
.onActionStart((event: GestureEvent) => {
hilog.info(0x0001, this.LOG_TAG, 'Bắt đầu co giãn');
})
.onActionUpdate((event: GestureEvent) => {
if (event) {
this.screenScale = event.scale;
}
})
.onActionEnd((event: GestureEvent) => {
if (this.hasScannedResult) {
return;
}
// Lấy tỷ lệ co giãn và cài đặt zoom
try {
let zoom = cameraManager.getZoom();
this.pinchScale = this.screenScale * zoom;
cameraManager.setZoom(this.pinchScale);
hilog.info(0x0001, this.LOG_TAG, 'Kết thúc co giãn');
} catch (error) {
hilog.error(0x0001, this.LOG_TAG, `Lỗi khi setZoom. Mã lỗi: ${error.code}, thông báo: ${error.message}`);
}
}))
}
private navContext: NavDestinationContext | null = null;
build() {
NavDestination(){
this.MainScanView()
}
.width("100%")
.height("100%")
.hideTitleBar(true)
.onReady((navContext: NavDestinationContext)=>{
this.navContext = navContext;
})
.onShown(()=>{
this.onPageVisible();
})
.onHidden(()=>{
this.onPageHidden();
})
}
}