diff --git a/README.en.md b/README.en.md index 94a46c51b65153e8421b6204a5dfd18e55475214..a5dc45362a8cb0dbdeaf671f783f5863e3d51595 100644 --- a/README.en.md +++ b/README.en.md @@ -2,11 +2,11 @@ ### Overview -Based on the Camera Kit, this sample implements a range of core camera functionalities such as basic preview, preview image adjustments (switching between the front and rear cameras, flash light, focus, zoom, etc.), advanced preview functionalities (grid line, level, timeout pause, etc.), dual-channel preview, photographing (such as motion photo and delayed shooting), and video recording. It serves as a comprehensive reference and practice guidance for developing a custom camera service. +Based on the Camera Kit, this sample implements a range of core camera functionalities such as basic preview, preview image adjustments (switching between the front and rear cameras, flash light, focus, zoom, etc.), advanced preview functionalities (grid line, level, timeout pause, face detection, etc.), dual-channel preview, photographing (such as motion photo and delayed shooting), and video recording. It serves as a comprehensive reference and practice guidance for developing a custom camera service. ### Preview -![](./screenshots/devices/camera_en.png) +![](./screenshots/devices/camera_en.png)   ![](./screenshots/devices/camera_people_en.png) How to use: @@ -29,6 +29,7 @@ How to use: │ │ └──cameraManagers │ │ ├──CamaraManager.ets // Camera session management class. │ │ ├──ImageReceiverManager.ets // ImageReceiver preview stream. +│ │ ├──MetadataManager.ets // Metadata output stream. │ │ ├──OutputManager.ets // Output stream management abstract. │ │ ├──PhotoManager.ets // Photo stream management class. │ │ ├──VideoManager.ets // Video stream management class. @@ -37,25 +38,30 @@ How to use: ├──commons/src/main/ets/ │ └──utils │ └──Logger.ets // Log class. -├──entry/src/main/ets/ -│ ├──entryability -│ │ └──EntryAbility.ets // Entry point class. +├──entry/src/main/ets/ │ ├──constants │ │ └──Constants.ets // Constant file. -│ ├──pages +│ ├──entryability +│ │ └──EntryAbility.ets // Entry point class. +│ ├──models +│ │ └──CameraManagerModel.ets // Camera manager data class. +│ ├──pages │ │ └──Index.ets // Entry preview page. -│ ├──views -│ │ ├──ModeButtonsView.ets // Photo mode switch button view. -│ │ ├──OperateButtonsView.ets // Operation button view. -│ │ ├──SettingButtonsView.ets // Setting button view. -│ │ └──ZoomButtonsView.ets // Zoom control button view. -│ ├──viewModels +│ ├──utils +│ │ ├──CommonUtil.ets // Common utility function module. +│ │ ├──PermissionManager.ets // Permission management class. +│ │ ├──RefreshableTimer.ets // Timer management class. +│ │ └──WindowUtil.ets // Window utility class. +│ ├──viewModels │ │ └──PreviewViewModel.ets // Preview-related state management. -│ └──utils -│ ├──CommonUtil.ets // Common utility function module. -│ ├──PermissionManager.ets // Permission management class. -│ ├──RefreshableTimer.ets // Timer management class. -│ └──WindowUtil.ets // Window utility class. +│ └──views +│ ├──FuncButtonsView.ets // Camera Function Button View. +│ ├──ModeButtonsView.ets // Photo mode switch button view. +│ ├──OperateButtonsView.ets // Operation button view. +│ ├──PreviewImageView.ets // Photo Preview View. +│ ├──PreviewScreenView.ets // Preview screen view. +│ ├──SettingButtonsView.ets // Setting button view. +│ └──ZoomButtonsView.ets // Zoom control button view. └──entry/src/main/resources // Static resources. ``` diff --git a/README.md b/README.md index 0e423c84bf94c590e2f5503d53e92ecea3c6c366..306b92cc1b7a1fd5a0f1f4f9b12d5f750243642e 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ### 介绍 -本示例基于Camera Kit相机服务,使用ArkTS API实现基础预览、预览画面调整(前后置镜头切换、闪光灯、对焦、调焦、设置曝光中心点等)、预览进阶功能(网格线、水平仪、超时暂停等)、双路预览(获取预览帧数据)、拍照(动图拍摄、延迟拍摄等)、录像等核心功能。为开发者提供自定义相机开发的完整参考与实践指导。 +本示例基于Camera Kit相机服务,使用ArkTS API实现基础预览、预览画面调整(前后置镜头切换、闪光灯、对焦、调焦、设置曝光中心点等)、预览进阶功能(网格线、水平仪、人脸检测、超时暂停等)、双路预览(获取预览帧数据)、拍照(动图拍摄、延迟拍摄等)、录像等核心功能。为开发者提供自定义相机开发的完整参考与实践指导。 ### 效果预览 -![](./screenshots/devices/camera.png) +![](./screenshots/devices/camera.png)   ![](./screenshots/devices/camera_people.png) 使用说明: 1. 打开应用,授权后展示预览界面。 @@ -28,6 +28,7 @@ │ │ └──cameraManagers │ │ ├──CamaraManager.ets // 相机会话管理类 │ │ ├──ImageReceiverManager.ets // ImageReceiver预览流管理类 +│ │ ├──MetadataManager.ets // 元数据输出流管理类 │ │ ├──OutputManager.ets // 输出流管理类抽象接口 │ │ ├──PhotoManager.ets // 拍照流管理类 │ │ ├──VideoManager.ets // 视频流管理类 @@ -37,24 +38,29 @@ │ └──utils │ └──Logger.ets // 日志类 ├──entry/src/main/ets/ -│ ├──entryability -│ │ └──EntryAbility.ets // 程序入口类 │ ├──constants │ │ └──Constants.ets // 常量文件 +│ ├──entryability +│ │ └──EntryAbility.ets // 程序入口类 +│ ├──models +│ │ └──CameraManagerModel.ets // 相机管理数据类 │ ├──pages │ │ └──Index.ets // 入口预览页面 -│ ├──views -│ │ ├──ModeButtonsView.ets // 拍照模式切换按钮视图 -│ │ ├──OperateButtonsView.ets // 操作按钮视图 -│ │ ├──SettingButtonsView.ets // 设置按钮视图 -│ │ └──ZoomButtonsView.ets // 设置焦距按钮视图 +│ ├──utils +│ │ ├──CommonUtil.ets // 通用工具函数模块 +│ │ ├──PermissionManager.ets // 权限管理类 +│ │ ├──RefreshableTimer.ets // 定时器管理类 +│ │ └──WindowUtil.ets // 窗口工具类 │ ├──viewModels │ │ └──PreviewViewModel.ets // 预览相关的状态管理类 -│ └──utils -│ ├──CommonUtil.ets // 通用工具函数模块 -│ ├──PermissionManager.ets // 权限管理类 -│ ├──RefreshableTimer.ets // 定时器管理类 -│ └──WindowUtil.ets // 窗口工具类 +│ └──views +│ ├──FuncButtonsView.ets // 拍照功能按钮视图 +│ ├──ModeButtonsView.ets // 拍照模式切换按钮视图 +│ ├──OperateButtonsView.ets // 操作按钮视图 +│ ├──PreviewImageView.ets // 拍照结果预览视图 +│ ├──PreviewScreenView.ets // 预览画面视图 +│ ├──SettingButtonsView.ets // 设置按钮视图 +│ └──ZoomButtonsView.ets // 设置焦距按钮视图 └──entry/src/main/resources // 应用静态资源目录 ``` diff --git a/camera/Index.ets b/camera/Index.ets index ccfbb991d25157b984dd7911b8e1bcb8dffb967b..c8eba2c9439e42abdab6e3b659c54a9ee2ecd56e 100644 --- a/camera/Index.ets +++ b/camera/Index.ets @@ -18,5 +18,6 @@ export { PreviewManager } from './src/main/ets/cameramanagers/PreviewManager'; export { PhotoManager } from './src/main/ets/cameramanagers/PhotoManager'; export { VideoManager, AVRecorderState } from './src/main/ets/cameramanagers/VideoManager'; export { ImageReceiverManager } from './src/main/ets/cameramanagers/ImageReceiverManager'; +export { MetadataManager } from './src/main/ets/cameramanagers/MetadataManager'; export { GridLine } from './src/main/ets/components/GridLine'; export { LevelIndicator } from './src/main/ets/components/LevelIndicator'; diff --git a/camera/src/main/ets/cameramanagers/CameraManager.ets b/camera/src/main/ets/cameramanagers/CameraManager.ets index 693d0115bdd56a8cdf639645332cd381c6f948bb..7f8c1709948bfe864dba08020046ba23177913a2 100644 --- a/camera/src/main/ets/cameramanagers/CameraManager.ets +++ b/camera/src/main/ets/cameramanagers/CameraManager.ets @@ -16,10 +16,13 @@ import { camera } from '@kit.CameraKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { Logger } from 'commons'; -import OutputManager, { CreateOutputConfig } from './OutputManager'; +import { OutputManager, CreateOutputConfig } from './OutputManager'; const TAG = 'CameraManager'; +/** + * Camera capability management class, manages various configurations and output streams of the camera. + */ export class CameraManager { private cameraManager?: camera.CameraManager; session?: camera.PhotoSession | camera.VideoSession; @@ -38,6 +41,7 @@ export class CameraManager { } } + // Monitor camera status. addCameraStatusListener() { this.cameraManager?.on('cameraStatus', (err: BusinessError, statusInfo: camera.CameraStatusInfo) => { if (err && err.message) { @@ -80,6 +84,7 @@ export class CameraManager { surfaceId: xComponentSurfaceId }; // [EndExclude session] + // [Start addOutput] for (const outputManager of this.outputManagers) { if (outputManager.isActive) { const output = await outputManager.createOutput(config); @@ -88,6 +93,7 @@ export class CameraManager { } await session?.commitConfig(); await session?.start(); + // [End addOutput] // [End session] this.session = session as (camera.PhotoSession | camera.VideoSession); this.setFocusMode(camera.FocusMode.FOCUS_MODE_AUTO); @@ -96,7 +102,7 @@ export class CameraManager { Logger.error(TAG, `Failed to start camera session. Cause ${JSON.stringify(e)}`); } } - + // Stop and reconfigure the output stream. async refreshOutput(oldOutput: camera.CameraOutput, newOutput: camera.CameraOutput) { try { await this.session?.stop(); diff --git a/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets b/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets index 556b7c344fdef60d631ca5e93a07343e5a676ddd..7328f2f40ba86f41041c9092d89672924c64395e 100644 --- a/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets +++ b/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets @@ -18,11 +18,15 @@ import { camera } from '@kit.CameraKit'; import { display } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; import { Logger } from 'commons'; -import OutputManager, { CreateOutputConfig } from './OutputManager'; +import { OutputManager, CreateOutputConfig } from './OutputManager'; import CameraConstant from '../constants/CameraConstants'; const TAG = 'ImageReceiverManager'; +/** + * Dual-stream preview output - image receiver management class, responsible for the creation, + * configuration, and release of this output stream. + */ export class ImageReceiverManager implements OutputManager { output?: camera.PreviewOutput; isActive: boolean = true; diff --git a/camera/src/main/ets/cameramanagers/MetadataManager.ets b/camera/src/main/ets/cameramanagers/MetadataManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c5e9ef1ff93b93ef103646765c4b6c35cf76abc2 --- /dev/null +++ b/camera/src/main/ets/cameramanagers/MetadataManager.ets @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 ("the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BusinessError } from '@kit.BasicServicesKit'; +import { camera } from '@kit.CameraKit'; +import { Logger } from 'commons'; +import { OutputManager, CreateOutputConfig } from './OutputManager'; + +const TAG_LOG = 'MetadataManager'; + +/** + * Metadata output stream management class, responsible for managing the creation, + * configuration, and release of the output stream. + */ +export class MetadataManager implements OutputManager { + output?: camera.MetadataOutput; + isActive: boolean = true; + onMetadataObjectsAvailable: (faceBoxArr: camera.Rect[]) => void; + + constructor(onMetadataObjectsAvailable: (faceBoxArr: camera.Rect[]) => void) { + this.onMetadataObjectsAvailable = onMetadataObjectsAvailable; + } + + // [Start MetadataOutput] + async createOutput(config: CreateOutputConfig): Promise { + const cameraOutputCap = config.cameraManager?.getSupportedOutputCapability(config.device, config.sceneMode); + if (!cameraOutputCap) { + Logger.error(TAG_LOG, 'Failed to get supported output capability.'); + return; + } + let metadataObjectTypes: Array = cameraOutputCap!.supportedMetadataObjectTypes; + try { + this.output = config.cameraManager?.createMetadataOutput(metadataObjectTypes); + if (this.output) { + this.addOutputListener(this.output); + } + } catch (error) { + Logger.error(TAG_LOG, `Failed to createMetadataOutput, error code: ${error.code}`); + } + return this.output; + } + // [End MetadataOutput] + + addOutputListener(output: camera.MetadataOutput): void { + this.addMetadataObjectsAvailableListener(output); + this.addMetadataErrorListener(output); + } + + // [Start metadataObjectsAvailable] + addMetadataObjectsAvailableListener(metadataOutput: camera.MetadataOutput): void { + metadataOutput.on('metadataObjectsAvailable', + (err: BusinessError, metadataObjectArr: Array) => { + if (err && err.code !== 0) { + Logger.error(TAG_LOG, `Metadata output on metadataObjectsAvailable error code: ${err.code}`); + return; + } + let boxRectArr: camera.Rect[] = []; + metadataObjectArr.forEach((obj: camera.MetadataObject)=>{ + boxRectArr.push(obj.boundingBox); + }); + this.onMetadataObjectsAvailable(boxRectArr); + }); + } + // [End metadataObjectsAvailable] + + addMetadataErrorListener(metadataOutput: camera.MetadataOutput): void { + metadataOutput.on('error', (metadataOutputError: BusinessError) => { + Logger.error(TAG_LOG, `Metadata output error code: ${metadataOutputError.code}`); + }); + } + + async release() { + try { + await this.output?.release(); + } catch (exception) { + Logger.error(TAG_LOG, `release failed, code is ${exception.code}, message is ${exception.message}`); + } + this.output = undefined; + } +} \ No newline at end of file diff --git a/camera/src/main/ets/cameramanagers/OutputManager.ets b/camera/src/main/ets/cameramanagers/OutputManager.ets index cf636eefe9ff43a033af25be5ac5b9717507d6b4..fe75ce680c2e3ad188d351a541b0b87f02ac77e3 100644 --- a/camera/src/main/ets/cameramanagers/OutputManager.ets +++ b/camera/src/main/ets/cameramanagers/OutputManager.ets @@ -15,6 +15,7 @@ import { camera } from '@kit.CameraKit'; +// Standard Interface for Output Stream Configuration. export interface CreateOutputConfig { cameraManager?: camera.CameraManager; device: camera.CameraDevice; @@ -24,7 +25,7 @@ export interface CreateOutputConfig { } // [Start OutputManager] -export default interface OutputManager { +export interface OutputManager { output?: camera.CameraOutput; isActive: boolean; createOutput: (config: CreateOutputConfig) => Promise; diff --git a/camera/src/main/ets/cameramanagers/PhotoManager.ets b/camera/src/main/ets/cameramanagers/PhotoManager.ets index b8a64d822efac6d86ed2385376a8b79688e56b89..b0d559d706350a3a46148c3f5c9d086e0778e12f 100644 --- a/camera/src/main/ets/cameramanagers/PhotoManager.ets +++ b/camera/src/main/ets/cameramanagers/PhotoManager.ets @@ -22,11 +22,15 @@ import { image } from '@kit.ImageKit'; import { colorSpaceManager } from '@kit.ArkGraphics2D'; import { geoLocationManager } from '@kit.LocationKit'; import { Logger } from 'commons'; -import OutputManager, { CreateOutputConfig } from './OutputManager'; +import { OutputManager, CreateOutputConfig } from './OutputManager'; import CameraConstant from '../constants/CameraConstants'; const TAG_LOG = 'PhotoManager'; +/** + * Photo output stream management class, responsible for managing the creation, + * configuration, and release of the output stream + */ export class PhotoManager implements OutputManager { output?: camera.PhotoOutput; isActive: boolean = true; diff --git a/camera/src/main/ets/cameramanagers/PreviewManager.ets b/camera/src/main/ets/cameramanagers/PreviewManager.ets index d11877b13b86730c451318102032cc94496e91ea..9c9d5f4a00c96e50fcc3b8a500eb30b6f259e9ee 100644 --- a/camera/src/main/ets/cameramanagers/PreviewManager.ets +++ b/camera/src/main/ets/cameramanagers/PreviewManager.ets @@ -16,11 +16,15 @@ import { camera } from '@kit.CameraKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { Logger } from 'commons'; -import OutputManager, { CreateOutputConfig } from './OutputManager'; +import { OutputManager, CreateOutputConfig } from './OutputManager'; import CameraConstant from '../constants/CameraConstants'; const TAG_LOG = 'PreviewManager' +/** + * Preview output stream management class, responsible for managing the creation, + * configuration, and release of the output stream. + */ export class PreviewManager implements OutputManager { output?: camera.PreviewOutput; isActive: boolean = true; diff --git a/camera/src/main/ets/cameramanagers/VideoManager.ets b/camera/src/main/ets/cameramanagers/VideoManager.ets index cc36fb9def8790e5cbda671eaa2dc7a2e06c561f..5615a79f576e3473b5d9baeaf21b650e3babcb1e 100644 --- a/camera/src/main/ets/cameramanagers/VideoManager.ets +++ b/camera/src/main/ets/cameramanagers/VideoManager.ets @@ -22,7 +22,7 @@ import { Decimal } from '@kit.ArkTS'; import { image } from '@kit.ImageKit'; import { colorSpaceManager } from '@kit.ArkGraphics2D'; import { Logger } from 'commons'; -import OutputManager, { CreateOutputConfig } from './OutputManager'; +import { OutputManager, CreateOutputConfig } from './OutputManager'; import CameraConstant from '../constants/CameraConstants'; const TAG_LOG = 'video'; @@ -42,6 +42,10 @@ export enum AVRecorderState { ERROR = 'error' } +/** + * video output stream management class, responsible for managing the creation, + * configuration, and release of the output stream + */ export class VideoManager implements OutputManager { private avRecorder: media.AVRecorder | undefined = undefined; private avConfig: media.AVRecorderConfig | undefined = undefined; diff --git a/camera/src/main/ets/components/GridLine.ets b/camera/src/main/ets/components/GridLine.ets index dfdc02e402cdfe4a53766510db66ed23018d57cd..58ad0781d717a568d496ca7dd7f3816c08160361 100644 --- a/camera/src/main/ets/components/GridLine.ets +++ b/camera/src/main/ets/components/GridLine.ets @@ -14,6 +14,9 @@ */ @Component +/** + * Universal Camera Preview Grid Component. + */ export struct GridLine { private settings: RenderingContextSettings = new RenderingContextSettings(true); private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); diff --git a/camera/src/main/ets/components/LevelIndicator.ets b/camera/src/main/ets/components/LevelIndicator.ets index da5302a37312d6f5162f073a41f4ccb70dd7ef48..c7c951266fbf7df127c8efddc5203841edade117 100644 --- a/camera/src/main/ets/components/LevelIndicator.ets +++ b/camera/src/main/ets/components/LevelIndicator.ets @@ -22,6 +22,9 @@ const TAG = 'LevelIndicator'; // [Start LevelIndicator] @Component +/** + * Universal Camera Preview Level Component. + */ export struct LevelIndicator { @Prop acc: sensor.AccelerometerResponse; diff --git a/entry/src/main/ets/models/CameraManagerModel.ets b/entry/src/main/ets/models/CameraManagerModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..2854c9320fddd951bb1e069fd9a731f2c82ed8c3 --- /dev/null +++ b/entry/src/main/ets/models/CameraManagerModel.ets @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 ("the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CameraManager, ImageReceiverManager, MetadataManager, + PhotoManager, PreviewManager, VideoManager } from "camera"; + +/** + * Camera manager data class. + */ +export class CameraManagerModel { + previewManager: PreviewManager; + photoManager: PhotoManager; + videoManager: VideoManager; + imageReceiverManager: ImageReceiverManager; + metadataManager: MetadataManager; + cameraManager: CameraManager; + + constructor(context: Context, previewManager: PreviewManager, photoManager: PhotoManager, videoManager: VideoManager, + imageReceiverManager: ImageReceiverManager, metadataManager: MetadataManager,) { + this.previewManager = previewManager; + this.photoManager = photoManager; + this.videoManager = videoManager; + this.imageReceiverManager = imageReceiverManager; + this.metadataManager = metadataManager; + this.cameraManager = new CameraManager(context, [previewManager, + photoManager, videoManager, imageReceiverManager, metadataManager]); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 8b3f6d214d58f1e93b0c260227ed23c76224191b..8f0ae7bb7dd4737789676c1d402d777dd60e3a23 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -15,95 +15,46 @@ import { sensor } from '@kit.SensorServiceKit'; import { common } from '@kit.AbilityKit'; -import { display } from '@kit.ArkUI'; -import { curves } from '@kit.ArkUI'; -import { BusinessError } from '@kit.BasicServicesKit'; -import { - CameraManager, - GridLine, - ImageReceiverManager, - LevelIndicator, - PhotoManager, - PreviewManager, - VideoManager -} from 'camera'; -import CameraConstant from '../constants/Constants'; -import { calCameraPoint, getClampedChildPosition, limitNumberInRange, showToast } from '../utils/CommonUtil'; +import { display, window } from '@kit.ArkUI'; +import { camera } from '@kit.CameraKit'; +import { Logger } from 'commons'; import RefreshableTimer from '../utils/RefreshableTimer'; -import PermissionManager from '../utils/PermissionManager'; import ZoomButtonsView from '../views/ZoomButtonsView'; import ModeButtonsView from '../views/ModeButtonsView'; import SettingButtonsView from '../views/SettingButtonsView'; import OperateButtonsView from '../views/OperateButtonsView'; +import { PreviewScreenView } from '../views/PreviewScreenView'; +import { FuncButtonView } from '../views/FuncButtomView'; +import { PreviewImageView } from '../views/PreviewImageView'; import PreviewViewModel from '../viewmodels/PreviewViewModel'; -import { Logger } from 'commons'; -@Extend(SymbolGlyph) -function funcButtonStyle() { - .fontSize(22) - .fontColor([Color.White]) - .borderRadius('50%') - .padding(12) - .backgroundColor('#664D4D4D') -} - -const TAG = 'Index' +const TAG = 'Index'; @Entry @Component struct Index { - private context: Context = this.getUIContext().getHostContext()!; - private applicationContext = this.context.getApplicationContext(); - private windowClass = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync(); - @State videoManager: VideoManager = new VideoManager(this.context); - @State isSinglePhoto: boolean = false; - @State isLivePhoto: boolean = false; - private photoManager: PhotoManager = new PhotoManager(this.context, true, this.isSinglePhoto); - private previewManager: PreviewManager = new PreviewManager(() => { - this.onPreviewStart() - }); - private imageReceiverManager: ImageReceiverManager = new ImageReceiverManager(px => { - this.onImageReceiver(px); - }); - private cameraManager: CameraManager = new CameraManager(this.context, [this.previewManager, - this.photoManager, this.videoManager, this.imageReceiverManager]); @State previewVM: PreviewViewModel = new PreviewViewModel(this.getUIContext()); - @State isGridLineVisible: boolean = false; - @State isLevelIndicatorVisible: boolean = false; - @State isPreviewImageVisible: boolean = false; - @State isFocusBoxVisible: boolean = false; - @State focusBoxPosition: Edges = { top: 0, left: 0 }; - private focusBoxSize: Size = { width: 80, height: 80 }; - private focusBoxTimer: RefreshableTimer = new RefreshableTimer(() => { - this.isFocusBoxVisible = false; - }, 3 * 1000); - private exposureFontSize: number = 24; @State isSleeping: boolean = false; - private sleepTimer?: RefreshableTimer; - private zoomRange: number[] = []; - @State zooms: number[] = [1, 5, 10]; - @State currentZoom: number = 1; - @State isZoomPinching: boolean = false; - private originZoomBeforePinch: number = 1; // record zoom after pinch, sale base it. - @State isStabilizationEnabled: boolean = false; - @State previewImage: PixelMap | ResourceStr = ''; - private PreviewImageHeight: number = 80; - @State photoDelayTime: number = 0; - @State photoRemainder: number = 0; - @State isDelayTakePhoto: boolean = false; - @State acc: sensor.AccelerometerResponse = { x: 0, y: 0, z: 0 } as sensor.AccelerometerResponse; - private setPreviewSize: () => void = () => { - this.previewVM.setPreviewSize(); - } - @State isShowBlack: boolean = false; - @StorageLink('captureClick') @Watch('onCaptureClick') captureClickFlag: number = 0; - @State flashBlackOpacity: number = 1; + @State sleepTimer: RefreshableTimer | undefined = undefined; + private context: Context = this.getUIContext().getHostContext()!; + private applicationContext = this.context.getApplicationContext(); + private windowClass: window.Window | undefined = undefined; async aboutToAppear() { + // Add state listener when the page initializes. this.addGravityEventListener(); this.initSleepTimer(); this.registerApplicationStateChange(); this.addOrientationChangeEventListener(); + + // Get the current window. + try { + this.windowClass = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync(); + } catch (exception) { + Logger.error(TAG, `getMainWindowSync failed, code is ${exception.code}, message is ${exception.message}`); + } + + // Monitor device fold state changes. try { display.on('foldStatusChange', () => { this.onFoldStatusChange() @@ -118,10 +69,11 @@ struct Index { } // [Start addGravityEventListener] + // Add a gravity sensor listener callback function. addGravityEventListener() { try { sensor.on(sensor.SensorId.GRAVITY, (data) => { - this.acc = data; + this.previewVM.acc = data; }, { interval: 100 * 1000 * 1000 }); // 100ms } catch (exception) { Logger.error(TAG, `addGravityEventListener failed, code is ${exception.code}, message is ${exception.message}`); @@ -130,24 +82,12 @@ struct Index { // [End addGravityEventListener] - addOrientationChangeEventListener() { - this.windowClass.on('windowSizeChange', this.setPreviewSize); - } - - removeOrientationChangeEventListener() { - this.windowClass.off('windowSizeChange', this.setPreviewSize); - } - - onImageReceiver(pixelMap: PixelMap) { - this.previewImage = pixelMap; - } - // [Start initSleepTimer] initSleepTimer() { this.sleepTimer = new RefreshableTimer(() => { this.previewVM.openPreviewBlur(); this.isSleeping = true; - this.cameraManager.release(); + this.previewVM.cameraManagerRelease(); }, 30 * 1000); this.sleepTimer.start(); const observer = this.getUIContext().getUIObserver(); @@ -158,212 +98,62 @@ struct Index { // [End initSleepTimer] - async onFoldStatusChange() { - await this.cameraManager.release(); - await this.startCamera(); - this.syncButtonSettings(); - } - // [Start registerApplicationStateChange] registerApplicationStateChange() { this.applicationContext.on('applicationStateChange', { onApplicationForeground: async () => { await this.startCamera(); // [StartExclude registerApplicationStateChange] - this.syncButtonSettings(); + this.previewVM.syncButtonSettings(); // [EndExclude registerApplicationStateChange] }, onApplicationBackground: () => { // [StartExclude registerApplicationStateChange] this.previewVM.openPreviewBlur(); // [EndExclude registerApplicationStateChange] - this.cameraManager.release(); + this.previewVM.cameraManagerRelease(); } }) } + // open camera and start session. async startCamera() { const cameraPosition = this.previewVM.getCameraPosition(); const sceneMode = this.previewVM.getSceneMode(); - await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + await this.previewVM.cameraManagerStart(cameraPosition, sceneMode); } // [End registerApplicationStateChange] - exitApp() { - this.applicationContext.killAllProcesses().catch((err: BusinessError) => { - Logger.error('showToast', `showToast failed, code is ${err.code}, message is ${err.message}`); - }); - } - - onPreviewStart() { - this.previewVM.closePreviewBlur(); - } - - initZooms() { - const zoomRange = this.cameraManager.getZoomRange(); - const minZoom = zoomRange[0]; - this.zoomRange = zoomRange; - if (minZoom < this.zooms[0]) { - this.zooms.unshift(minZoom); - } - } - - initRates() { - const frameRates = this.previewManager.getSupportedFrameRates(); - if (frameRates && frameRates[0]) { - const minRate = frameRates[0].min; - const maxRate = frameRates[0].max; - this.previewVM.rates = [minRate, maxRate]; - this.previewVM.currentRate = maxRate; - this.previewManager.setFrameRate(maxRate, maxRate); - } - ; - } - - syncButtonSettings() { - this.previewManager.setFrameRate(this.previewVM.currentRate, this.previewVM.currentRate); - this.photoManager.enableMovingPhoto(this.isLivePhoto); - this.photoManager.setPhotoOutputCallback(this.isSinglePhoto); - } - - flashBlackAnim() { - this.flashBlackOpacity = 1; - this.isShowBlack = true; - animateToImmediately({ - curve: curves.interpolatingSpring(1, 1, 410, 38), - delay: 50, - onFinish: () => { - this.isShowBlack = false; - this.flashBlackOpacity = 1; - } - }, () => { - this.flashBlackOpacity = 0; - }) + // Rotation state listener. + addOrientationChangeEventListener() { + this.windowClass?.on('windowSizeChange', () => { this.previewVM.setPreviewSize(); }); } - onCaptureClick(): void { - this.flashBlackAnim(); + removeOrientationChangeEventListener() { + this.windowClass?.off('windowSizeChange', () => { this.previewVM.setPreviewSize(); }); } - @Builder - preview() { - // [Start Stack] - Stack({ - alignContent: Alignment.Center - }) { - // [Start XComponent] - // [Start XComponent_gesture] - XComponent({ - type: XComponentType.SURFACE, - controller: this.previewVM.xComponentController - }) - // [StartExclude Stack] - // [StartExclude XComponent_gesture] - .onLoad(async () => { - // [StartExclude XComponent] - await PermissionManager.request(CameraConstant.PERMISSIONS, this.context) - .catch(() => { - this.exitApp() - }); - // [EndExclude XComponent] - this.previewVM.surfaceId = this.previewVM.xComponentController.getXComponentSurfaceId(); - this.previewVM.setPreviewSize(); - this.previewVM.xComponentController.setXComponentSurfaceRotation({ lock: true }); - // [StartExclude XComponent] - await this.startCamera(); - this.initZooms(); - this.initRates(); - // [EndExclude XComponent] - }) - // [StartExclude XComponent_gesture] - // [End XComponent] - .gesture( - PinchGesture({ fingers: 2 }) - .onActionStart(() => { - this.originZoomBeforePinch = this.currentZoom; - this.isZoomPinching = true; - this.sleepTimer?.refresh(); - }) - .onActionUpdate((event: GestureEvent) => { - if (this.previewVM.isVideoMode() && this.isStabilizationEnabled) { - return; - } - const targetZoom = this.originZoomBeforePinch * event.scale; - this.currentZoom = limitNumberInRange(targetZoom, this.zoomRange); - this.cameraManager.setZoomRatio(this.currentZoom); - }) - .onActionEnd(() => { - this.isZoomPinching = false; - }) - ) - // [End XComponent_gesture] - .onClick(event => { - this.isFocusBoxVisible = true; - const previewSize = this.previewVM.previewSize; - const cameraPoint = calCameraPoint( - this.getUIContext().vp2px(event.x), - this.getUIContext().vp2px(event.y), - previewSize.width, - previewSize.height - ); - this.cameraManager.setFocusPoint(cameraPoint); - this.cameraManager.setMeteringPoint(cameraPoint); - this.focusBoxPosition = getClampedChildPosition(this.focusBoxSize, { - width: this.getUIContext().px2vp(previewSize.width), - height: this.getUIContext().px2vp(previewSize.height) - }, event); - this.focusBoxTimer.refresh(); - }) - // [EndExclude Stack] - if (this.isGridLineVisible) { - GridLine() - } - // [StartExclude Stack] - if (this.isLevelIndicatorVisible) { - LevelIndicator({ - acc: this.acc - }) - } - // focus box - if (this.isFocusBoxVisible) { - Image($r('app.media.focus_box')) - .width(80) - .height(80) - .position(this.focusBoxPosition) - SymbolGlyph($r('sys.symbol.sun_max')) - .fontSize(this.exposureFontSize) - .fontColor([Color.White]) - .position(this.getExposurePosition()) - } - - if (this.isDelayTakePhoto) { - Text(`${this.photoRemainder}S`) - .fontSize(44) - .fontWeight(FontWeight.Regular) - .fontColor(Color.White) - } - // [EndExclude Stack] - - if (this.isShowBlack) { - Column() - .id('black') - .width('100%') - .height('100%') - .backgroundColor(Color.Black) - .opacity(this.flashBlackOpacity) - } - } - // [End Stack] - .alignRules({ - middle: { anchor: '__container__', align: HorizontalAlign.Center } + // Fold state listener callback. + async onFoldStatusChange() { + await this.previewVM.cameraManagerRelease(); + await this.startCamera(); + this.previewVM.syncButtonSettings(); + } + + // [Start onMetadataObjectsAvailable] + // Face detection information listener callback. + onMetadataObjectsAvailable(faceBoxArr: camera.Rect[]) { + faceBoxArr.forEach((value) => { + // Convert normalized coordinates to actual coordinates. + value.topLeftX *= this.previewVM.getPreviewWidth(); + value.topLeftY *= this.previewVM.getPreviewHeight(); + value.width *= this.previewVM.getPreviewWidth(); + value.height *= this.previewVM.getPreviewHeight(); }) - .width(this.previewVM.getPreviewWidth()) - .height(this.previewVM.getPreviewHeight()) - .margin({ top: this.previewVM.getPreviewTop() }) - .blur(this.previewVM.blurRadius) - .rotate(this.previewVM.blurRotation) + this.previewVM.faceBoundingBoxArr = faceBoxArr; } + // [End onMetadataObjectsAvailable] // [Start wakeupMask] @Builder @@ -383,161 +173,60 @@ struct Index { this.isSleeping = false; this.sleepTimer?.refresh(); await this.startCamera(); - this.syncButtonSettings(); + this.previewVM.syncButtonSettings(); }) } // [End wakeupMask] - @Builder - gridLineButton() { - SymbolGlyph( - this.isGridLineVisible - ? $r('sys.symbol.camera_assistive_grid') - : $r('sys.symbol.camera_assistive_grid_slash') - ) - .funcButtonStyle() - .onClick(() => { - this.isGridLineVisible = !this.isGridLineVisible; - const message = this.isGridLineVisible ? $r('app.string.grid_line_open') : $r('app.string.grid_line_close'); - showToast(this.getUIContext(), message); - }) - } - - @Builder - levelButton() { - SymbolGlyph($r('sys.symbol.horizontal_level')) - .funcButtonStyle() - .onClick(() => { - this.isLevelIndicatorVisible = !this.isLevelIndicatorVisible; - const message = this.isLevelIndicatorVisible ? $r('app.string.level_open') : $r('app.string.level_close'); - showToast(this.getUIContext(), message); - }) - } - - @Builder - previewImageButton() { - SymbolGlyph(this.isPreviewImageVisible ? $r('sys.symbol.eye') : $r('sys.symbol.eye_slash')) - .funcButtonStyle() - .onClick(() => { - this.isPreviewImageVisible = !this.isPreviewImageVisible; - const message = - this.isPreviewImageVisible ? $r('app.string.preview_image_open') : $r('app.string.preview_image_close'); - showToast(this.getUIContext(), message); - }) - } - - getPreviewImageWidth() { - let displayDefault: display.Display | null = null; - try { - displayDefault = display.getDefaultDisplaySync(); - } catch (exception) { - Logger.error(TAG, `getDefaultDisplaySync failed, code is ${exception.code}, message is ${exception.message}`); - } - const rotation = (displayDefault?.rotation ?? 0) * 90; - const ratio = this.previewVM.getPreviewRatio(); - const displayRatio = rotation === 90 || rotation === 270 ? 1 / ratio : ratio; - return this.PreviewImageHeight / displayRatio; - } - - getExposurePosition(): Edges { - const focusBoxLeft = this.focusBoxPosition.left as number; - const focusBoxTop = this.focusBoxPosition.top as number; - const exposureWidth = this.exposureFontSize; - const exposureHeight = this.exposureFontSize; - const focusBoxWidth = this.focusBoxSize.width; - const focusBoxHeight = this.focusBoxSize.height; - const previewWidth = this.getUIContext().px2vp(this.previewVM.previewSize.width); - const GAP = 10; - const top = focusBoxTop - exposureHeight / 2 + focusBoxHeight / 2; - const left = focusBoxLeft > previewWidth / 2 - ? focusBoxLeft - GAP - exposureWidth - : focusBoxLeft + focusBoxWidth + GAP; - return { top, left }; - } - - @Builder - previewImageView() { - Image(this.previewImage) - .width(this.getPreviewImageWidth()) - .height(this.PreviewImageHeight) - .alignRules({ - bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, - left: { anchor: '__container__', align: HorizontalAlign.Start } - }) - .margin({ - bottom: 10, - left: 10 - }) - } - - @Builder - funcButtonsView() { - Column({ space: 24 }) { - this.gridLineButton() - this.levelButton() - this.previewImageButton() - } - .alignRules({ - top: { anchor: 'settingButtonsView', align: VerticalAlign.Bottom }, - right: { anchor: 'settingButtonsView', align: HorizontalAlign.End } - }) - .margin({ - top: 40, - right: 10 - }) - } - build() { RelativeContainer() { - this.preview() - if (this.isPreviewImageVisible) { - this.previewImageView() + if (this.previewVM.isPreviewManagerExist()) { + // Display camera preview and related preview auxiliary components. + PreviewScreenView({ + previewVM: this.previewVM, + sleepTimer: this.sleepTimer + }); + } + + if (this.previewVM.isPreviewImageVisible) { + // Show a thumbnail of the last photo taken. + PreviewImageView({ + previewVM: this.previewVM + }); } - this.funcButtonsView() + + // Camera Shooting Assist Function Button. + FuncButtonView({ + previewVM: this.previewVM + }); + + // Camera shooting settings button. SettingButtonsView({ - previewVM: this.previewVM, - cameraManager: this.cameraManager, - previewManager: this.previewManager, - photoManager: this.photoManager, - videoManager: this.videoManager, - photoDelayTime: this.photoDelayTime, - isSinglePhoto: this.isSinglePhoto, - isLivePhoto: this.isLivePhoto, - isStabilizationEnabled: this.isStabilizationEnabled + previewVM: this.previewVM }) - if (!this.photoRemainder) { + + // Camera focal length setting button + if (!this.previewVM.photoRemainder) { if (!this.previewVM.isFront && - !(this.isStabilizationEnabled && this.previewVM.isVideoMode())) { + !(this.previewVM.isStabilizationEnabled && this.previewVM.isVideoMode())) { ZoomButtonsView({ - cameraManager: this.cameraManager, - zoomRange: this.zoomRange, - zooms: this.zooms, - currentZoom: this.currentZoom + previewVM: this.previewVM }) } + + // Camera shooting mode setting button. ModeButtonsView({ - previewVM: this.previewVM, - photoManager: this.photoManager, - videoManager: this.videoManager, - cameraManager: this.cameraManager, - syncButtonSettings: () => { - this.syncButtonSettings(); - } + previewVM: this.previewVM }) + + // Camera Shooting Operation Buttons. OperateButtonsView({ - previewVM: this.previewVM, - cameraManager: this.cameraManager, - photoManager: this.photoManager, - videoManager: this.videoManager, - isDelayTakePhoto: this.isDelayTakePhoto, - photoDelayTime: this.photoDelayTime, - photoRemainder: this.photoRemainder, - syncButtonSettings: () => { - this.syncButtonSettings(); - } + previewVM: this.previewVM }) } + + // Camera Sleep Prompt. if (this.isSleeping) { this.wakeupMask() } diff --git a/entry/src/main/ets/utils/CommonUtil.ets b/entry/src/main/ets/utils/CommonUtil.ets index dc1a899d345721c0acbae23979a93d0e78d1c96f..ae858af78e54d7cb5941163447f5d1262035838c 100644 --- a/entry/src/main/ets/utils/CommonUtil.ets +++ b/entry/src/main/ets/utils/CommonUtil.ets @@ -19,6 +19,13 @@ import { display } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; import { Logger } from 'commons'; +const FACE_BOX_LINE_RATIO: number = 0.3; + +export interface LinePoint { + start: camera.Point, + increment: camera.Point +} + export function limitNumberInRange(src: number, range: number[]) { if (range.length < 2) { return src; @@ -77,9 +84,40 @@ export function getClampedChildPosition(childSize: Size, parentSize: Size, point } return { left, top }; } - // [End getClampedChildPosition] +// [Start calFaceBoxLinePoint] +// Calculate the coordinates of the face detection box. +export function calFaceBoxLinePoint(faceBoxRect: camera.Rect): LinePoint[] { + // The length of the sides of the box. + let lineLength: number = Math.min(faceBoxRect.width, faceBoxRect.height) * FACE_BOX_LINE_RATIO; + let linePoints: LinePoint[] = []; + + // The coordinates of the four vertices of the detection box. + let startPoints: camera.Point[] = [ + { x: faceBoxRect.topLeftX, y: faceBoxRect.topLeftY }, + { x: faceBoxRect.topLeftX + faceBoxRect.width, y: faceBoxRect.topLeftY }, + { x: faceBoxRect.topLeftX, y: faceBoxRect.topLeftY + faceBoxRect.height }, + { x: faceBoxRect.topLeftX + faceBoxRect.width, y: faceBoxRect.topLeftY + faceBoxRect.height }]; + + // Calculate the relative coordinates of each edge. + startPoints.forEach((startPoint: camera.Point) => { + let HorizontalLine: LinePoint = { + start: startPoint, + increment: { x: startPoint.x > faceBoxRect.topLeftX ? -lineLength : lineLength, y: 0 } + }; + + let verticalLine: LinePoint = { + start: startPoint, + increment: { x: 0, y: startPoint.y > faceBoxRect.topLeftY ? -lineLength : lineLength } + }; + + linePoints.push(HorizontalLine, verticalLine); + }); + return linePoints; +} +// [End calFaceBoxLinePoint] + export function showToast( UIContext: UIContext, message: ResourceStr = '', diff --git a/entry/src/main/ets/viewmodels/PreviewViewModel.ets b/entry/src/main/ets/viewmodels/PreviewViewModel.ets index 87dcf0f760c6bd7d7b926c6a1d74a13fd701a141..1e96aea1b479863bec4d96cef0c79b4db4d70ade 100644 --- a/entry/src/main/ets/viewmodels/PreviewViewModel.ets +++ b/entry/src/main/ets/viewmodels/PreviewViewModel.ets @@ -15,9 +15,14 @@ import { curves, display } from '@kit.ArkUI'; import { camera } from '@kit.CameraKit'; +import { image } from '@kit.ImageKit'; +import { media } from '@kit.MediaKit'; +import { sensor } from '@kit.SensorServiceKit'; +import { Logger } from 'commons'; +import { ImageReceiverManager, MetadataManager, PhotoManager, PreviewManager, VideoManager } from 'camera'; import WindowUtil from '../utils/WindowUtil'; import CameraConstant from '../constants/Constants'; -import { Logger } from 'commons'; +import { CameraManagerModel } from '../models/CameraManagerModel'; export enum CameraMode { PHOTO, @@ -29,8 +34,11 @@ const TAG = 'PreviewViewModel'; /** * States and methods related to preview. */ +@Observed class PreviewViewModel { + private context: Context | undefined; private uiContext: UIContext; + private cameraManagerModel: CameraManagerModel; // [Start isFront] isFront: boolean = false; // [StartExclude isFront] @@ -42,9 +50,177 @@ class PreviewViewModel { currentRate: number = 0; blurRadius: number = 0; blurRotation: RotateOptions = { y: 0.5, angle: 0 }; + faceBoundingBoxArr: camera.Rect[] = []; + // Accelerometer data. + acc: sensor.AccelerometerResponse = { x: 0, y: 0, z: 0 } as sensor.AccelerometerResponse; + + // Timer photo settings params. + isDelayTakePhoto: boolean = false; + photoDelayTime: number = 0; + photoRemainder: number = 0; + + isStabilizationEnabled: boolean = false; + // Focal length params. + zoomRange: number[] = []; + zooms: number[] = [1, 5, 10]; + currentZoom: number = 1; + + isGridLineVisible: boolean = false; + isLevelIndicatorVisible: boolean = false; + isSinglePhoto: boolean = false; + isLivePhoto: boolean = false; + isPreviewImageVisible: boolean = false; + previewImage: PixelMap | ResourceStr = ''; constructor(uiContext: UIContext) { this.uiContext = uiContext; + let context = this.uiContext.getHostContext()!; + this.context = context; + + // Create camera management variables. + let previewManager = new PreviewManager(() => { + this.closePreviewBlur(); + }); + let photoManager = new PhotoManager(this.context, true, this.isSinglePhoto); + let videoManager = new VideoManager(this.context); + let imageReceiverManager = new ImageReceiverManager(px => { + this.onImageReceiver(px); + }); + let metadataManager = new MetadataManager(faceBoxArr => { + this.onMetadataObjectsAvailable(faceBoxArr); + }); + this.cameraManagerModel = new CameraManagerModel(context, previewManager, + photoManager, videoManager, imageReceiverManager, metadataManager) + } + + onPreviewStart() { + this.closePreviewBlur(); + } + + onImageReceiver(pixelMap: PixelMap) { + this.previewImage = pixelMap; + } + + // [Start onMetadataObjectsAvailable] + onMetadataObjectsAvailable(faceBoxArr: camera.Rect[]) { + faceBoxArr.forEach((value) => { + // Convert normalized coordinates to actual coordinates. + value.topLeftX *= this.getPreviewWidth(); + value.topLeftY *= this.getPreviewHeight(); + value.width *= this.getPreviewWidth(); + value.height *= this.getPreviewHeight(); + }) + this.faceBoundingBoxArr = faceBoxArr; + } + // [End onMetadataObjectsAvailable] + + async cameraManagerStart(cameraPosition: camera.CameraPosition, sceneMode: camera.SceneMode) { + await this.cameraManagerModel.cameraManager.start(this.surfaceId, cameraPosition, sceneMode, this.getProfile); + } + + async cameraManagerRelease() { + await this.cameraManagerModel.cameraManager.release(); + } + + getCameraZoomRange(): number[] { + return this.cameraManagerModel.cameraManager.getZoomRange(); + } + + setCameraZoomRatio() { + this.cameraManagerModel.cameraManager.setZoomRatio(this.currentZoom); + } + + setCameraFocusPoint(cameraPoint: camera.Point) { + this.cameraManagerModel.cameraManager.setFocusPoint(cameraPoint); + } + + setCameraMeteringPoint(cameraPoint: camera.Point) { + this.cameraManagerModel.cameraManager.setMeteringPoint(cameraPoint); + } + + setCameraFlashMode(mode: camera.FlashMode) { + this.cameraManagerModel.cameraManager.setFlashMode(mode); + } + + setCameraVideoStabilizationMode(mode: camera.VideoStabilizationMode) { + this.cameraManagerModel.cameraManager.setVideoStabilizationMode(mode); + } + + setCameraSmoothZoom(zoom: number) { + this.cameraManagerModel.cameraManager.setSmoothZoom(zoom); + } + + syncButtonSettings() { + this.cameraManagerModel.previewManager.setFrameRate(this.currentRate, this.currentRate); + this.cameraManagerModel.photoManager.enableMovingPhoto(this.isLivePhoto); + this.cameraManagerModel.photoManager.setPhotoOutputCallback(this.isSinglePhoto); + } + + isPreviewManagerExist(): boolean { + if (this.cameraManagerModel.previewManager) { + return true; + } + return false; + } + + setPhotoIsActive() { + this.cameraManagerModel.photoManager.setIsActive(this.isPhotoMode() ? true : false); + } + + setVideoIsActive() { + this.cameraManagerModel.videoManager.setIsActive(this.isPhotoMode() ? false : true); + } + + setPhotoCallback(cb: (pixelMap: image.PixelMap, url: string) => void) { + this.cameraManagerModel.photoManager.setCallback(cb); + } + + setVideoCallback(cb: (pixelMap: image.PixelMap, url: string) => void) { + this.cameraManagerModel.videoManager.setVideoCallback(cb); + } + + async photoCapture() { + this.cameraManagerModel.photoManager.capture(this.isFront); + } + + async videoStart() { + this.cameraManagerModel.videoManager.start(this.isFront); + } + + async videoPause() { + this.cameraManagerModel.videoManager.pause(); + } + + async videoResume() { + this.cameraManagerModel.videoManager.resume(); + } + + async videoStop() { + await this.cameraManagerModel.videoManager.stop(); + } + + isVideoRecording(): boolean { + return this.cameraManagerModel.videoManager.isRecording(); + } + + checkVideoState(state: media.AVRecorderState): boolean { + return this.cameraManagerModel.videoManager.state === state; + } + + getPreviewSupportedFrameRates(): camera.FrameRateRange[] | undefined { + return this.cameraManagerModel.previewManager.getSupportedFrameRates(); + } + + setPreviewFrameRate(minRate: number, maxRate: number) { + this.cameraManagerModel.previewManager.setFrameRate(minRate, maxRate); + } + + enableMovingPhoto() { + this.cameraManagerModel.photoManager.enableMovingPhoto(this.isLivePhoto); + } + + setPhotoOutputCallback() { + this.cameraManagerModel.photoManager.setPhotoOutputCallback(this.isSinglePhoto); } // [EndExclude isFront] @@ -69,6 +245,7 @@ class PreviewViewModel { } // [Start getProfile] + // Get the camera's width, height, and format params. getProfile: (cameraOrientation: number) => camera.Profile = cameraOrientation => { const displaySize: Size = WindowUtil.getMaxDisplaySize(this.getPreviewRatio()); let displayDefault: display.Display | null = null; @@ -77,6 +254,7 @@ class PreviewViewModel { } catch (exception) { Logger.error(TAG, `getDefaultDisplaySync failed, code is ${exception.code}, message is ${exception.message}`); } + // Get actual rotation angle. const displayRotation = (displayDefault?.rotation ?? 0) * 90; const isRevert = (cameraOrientation + displayRotation) % 180 !== 0; return { @@ -127,6 +305,7 @@ class PreviewViewModel { return this.cameraMode === mode; } + // open blur animation openPreviewBlur() { animateToImmediately({ duration: 200, @@ -136,6 +315,7 @@ class PreviewViewModel { }); } + // Blur animation when rotating is enabled. rotatePreviewBlur() { animateToImmediately({ delay: 50, @@ -149,6 +329,7 @@ class PreviewViewModel { }); } + // Enable blur animation at the end of rotation. rotatePreviewBlurSecond() { this.blurRotation = { y: 0.5, angle: 270 }; animateToImmediately({ @@ -162,6 +343,7 @@ class PreviewViewModel { }); } + // close blur animation. closePreviewBlur() { animateToImmediately({ duration: 200, diff --git a/entry/src/main/ets/views/FuncButtomView.ets b/entry/src/main/ets/views/FuncButtomView.ets new file mode 100644 index 0000000000000000000000000000000000000000..24e8f05c0724b0c6d9e2271b6ee3acaf4512f7f0 --- /dev/null +++ b/entry/src/main/ets/views/FuncButtomView.ets @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 ("the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { showToast } from '../utils/CommonUtil'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +@Extend(SymbolGlyph) +function funcButtonStyle() { + .fontSize(22) + .fontColor([Color.White]) + .borderRadius('50%') + .padding(12) + .backgroundColor('#664D4D4D') +} +/** + * Camera preview accessibility features, including controls and displays for grid lines, + * a level, and dual-preview images + */ +@Component +export struct FuncButtonView { + @ObjectLink previewVM: PreviewViewModel; + + @Builder + gridLineButton() { + SymbolGlyph( + this.previewVM.isGridLineVisible + ? $r('sys.symbol.camera_assistive_grid') + : $r('sys.symbol.camera_assistive_grid_slash') + ) + .funcButtonStyle() + .onClick(() => { + this.previewVM.isGridLineVisible = !this.previewVM.isGridLineVisible; + const message = this.previewVM.isGridLineVisible ? $r('app.string.grid_line_open') : $r('app.string.grid_line_close'); + showToast(this.getUIContext(), message); + }) + } + + @Builder + levelButton() { + SymbolGlyph($r('sys.symbol.horizontal_level')) + .funcButtonStyle() + .onClick(() => { + this.previewVM.isLevelIndicatorVisible = !this.previewVM.isLevelIndicatorVisible; + const message = this.previewVM.isLevelIndicatorVisible ? $r('app.string.level_open') : $r('app.string.level_close'); + showToast(this.getUIContext(), message); + }) + } + + @Builder + previewImageButton() { + SymbolGlyph(this.previewVM.isPreviewImageVisible ? $r('sys.symbol.eye') : $r('sys.symbol.eye_slash')) + .funcButtonStyle() + .onClick(() => { + this.previewVM.isPreviewImageVisible = !this.previewVM.isPreviewImageVisible; + const message = + this.previewVM.isPreviewImageVisible ? $r('app.string.preview_image_open') : $r('app.string.preview_image_close'); + showToast(this.getUIContext(), message); + }) + } + + + build() { + Column({ space: 24 }) { + this.gridLineButton() + this.levelButton() + this.previewImageButton() + } + .alignRules({ + top: { anchor: 'settingButtonsView', align: VerticalAlign.Bottom }, + right: { anchor: 'settingButtonsView', align: HorizontalAlign.End } + }) + .margin({ + top: 40, + right: 10 + }) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/views/ModeButtonsView.ets b/entry/src/main/ets/views/ModeButtonsView.ets index 0a8bd9352ccf756425e974a4cc71cfefcf4a114e..7886c58ff0f8c875bad496da5b5cabc2a9d4bae2 100644 --- a/entry/src/main/ets/views/ModeButtonsView.ets +++ b/entry/src/main/ets/views/ModeButtonsView.ets @@ -13,7 +13,6 @@ * limitations under the License. */ -import { CameraManager, PhotoManager, VideoManager } from 'camera'; import PreviewViewModel, { CameraMode } from '../viewmodels/PreviewViewModel'; export interface CameraModeButton { @@ -22,8 +21,12 @@ export interface CameraModeButton { onClick?: () => void; } +/** + * Camera photo and video mode switch component. + */ @Component struct ModeButtonsView { + @ObjectLink previewVM: PreviewViewModel; private cameraModeButtons: CameraModeButton[] = [ { title: $r('app.string.photo'), @@ -33,12 +36,7 @@ struct ModeButtonsView { title: $r('app.string.video'), mode: CameraMode.VIDEO } - ] - @Link previewVM: PreviewViewModel; // Do not use @prop, otherwise deep copying, some underlying data will be lost. - @Require cameraManager: CameraManager; - @Require photoManager: PhotoManager; - @Require videoManager: VideoManager; - @Require syncButtonSettings: () => void; + ]; build() { Row() { @@ -54,16 +52,17 @@ struct ModeButtonsView { if (this.previewVM.isCurrentCameraMode(modeBtn.mode)) { return; } + // Recreate preview stream when switching modes. this.previewVM.openPreviewBlur(); this.previewVM.cameraMode = modeBtn.mode; this.previewVM.setPreviewSize(); const sceneMode = this.previewVM.getSceneMode(); const cameraPosition = this.previewVM.getCameraPosition(); - await this.cameraManager.release(); - this.photoManager.setIsActive(this.previewVM.isPhotoMode() ? true : false); - this.videoManager.setIsActive(this.previewVM.isPhotoMode() ? false : true); - await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); - this.syncButtonSettings(); + await this.previewVM.cameraManagerRelease(); + this.previewVM.setPhotoIsActive(); + this.previewVM.setVideoIsActive(); + await this.previewVM.cameraManagerStart(cameraPosition, sceneMode); + this.previewVM.syncButtonSettings(); } }) }, (modeBtn: CameraModeButton) => modeBtn.mode.toString()) diff --git a/entry/src/main/ets/views/OperateButtonsView.ets b/entry/src/main/ets/views/OperateButtonsView.ets index 7de83e3bfa7f117ea8554d6cfd0b0e3338d40f5c..17a25e4d7c6b4cbca505878623ba698f6c73d6de 100644 --- a/entry/src/main/ets/views/OperateButtonsView.ets +++ b/entry/src/main/ets/views/OperateButtonsView.ets @@ -16,35 +16,32 @@ import { image } from '@kit.ImageKit'; import { common } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; -import { AVRecorderState, CameraManager, PhotoManager, VideoManager } from 'camera'; -import PreviewViewModel from '../viewmodels/PreviewViewModel'; +import { AVRecorderState } from 'camera'; import { Logger } from 'commons'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; const TAG = 'OperateButtonsView'; +/** + * Shooting operation button components, including taking photos, starting, pausing, + * and stopping video recording, as well as switching cameras. + */ @Component struct OperateButtonsView { - @Link isDelayTakePhoto: boolean; - @Link previewVM: PreviewViewModel; - @Require cameraManager: CameraManager; - @Link videoManager: VideoManager; // Do not use @prop, otherwise deep copying, some underlying data will be lost. - @Require photoManager: PhotoManager; - @Prop @Require photoDelayTime: number; - @Link photoRemainder: number; - private photoDelayTimer: number = 0; @State thumbnail: image.PixelMap | string = ''; @State thumbnailUrl: string = ''; - @Require syncButtonSettings: () => void; + @ObjectLink previewVM: PreviewViewModel; + @StorageLink('captureClick') captureClickFlag: number = 0; + private photoDelayTimer: number = 0; private context = this.getUIContext().getHostContext() as common.UIAbilityContext; private setThumbnail: (pixelMap: image.PixelMap, url: string) => void = (pixelMap: image.PixelMap, url: string) => { this.thumbnail = pixelMap this.thumbnailUrl = url } - @StorageLink('captureClick') captureClickFlag: number = 0; aboutToAppear(): void { - this.photoManager.setCallback(this.setThumbnail); - this.videoManager.setVideoCallback(this.setThumbnail); + this.previewVM.setPhotoCallback(this.setThumbnail); + this.previewVM.setVideoCallback(this.setThumbnail); } @Builder @@ -68,20 +65,22 @@ struct OperateButtonsView { }) .justifyContent(FlexAlign.Center) .onClick(() => { - if (this.photoDelayTime) { - this.isDelayTakePhoto = true; - this.photoRemainder = this.photoDelayTime; + if (this.previewVM.photoDelayTime) { + // Shooting logic in timer photo mode. + this.previewVM.isDelayTakePhoto = true; + this.previewVM.photoRemainder = this.previewVM.photoDelayTime; this.photoDelayTimer = setInterval(() => { - this.photoRemainder--; - if (this.photoRemainder === 0) { - this.photoManager.capture(this.previewVM.isFront); + this.previewVM.photoRemainder--; + if (this.previewVM.photoRemainder === 0) { + this.previewVM.photoCapture(); this.captureClickFlag++; - this.isDelayTakePhoto = false; + this.previewVM.isDelayTakePhoto = false; clearTimeout(this.photoDelayTimer); } }, 1000) } else { - this.photoManager.capture(this.previewVM.isFront); + // Shooting logic in normal photo mode. + this.previewVM.photoCapture(); this.captureClickFlag++; } }) @@ -109,7 +108,7 @@ struct OperateButtonsView { }) .justifyContent(FlexAlign.Center) .onClick(() => { - this.videoManager.start(this.previewVM.isFront); + this.previewVM.videoStart(); }) } @@ -131,13 +130,16 @@ struct OperateButtonsView { }) .justifyContent(FlexAlign.Center) .onClick(async () => { - if (this.videoManager.state === AVRecorderState.STARTED || this.videoManager.state === AVRecorderState.PAUSED) { - await this.videoManager.stop(); - await this.cameraManager.release(); + // Check if the current status allows recording. + if (this.previewVM.checkVideoState(AVRecorderState.STARTED) || + this.previewVM.checkVideoState(AVRecorderState.PAUSED)) { + await this.previewVM.videoStop(); + // Release and reopen the camera. + await this.previewVM.cameraManagerRelease(); const cameraPosition = this.previewVM.getCameraPosition(); const sceneMode = this.previewVM.getSceneMode(); - await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); - this.syncButtonSettings(); + await this.previewVM.cameraManagerStart(cameraPosition, sceneMode); + this.previewVM.syncButtonSettings(); } }) } @@ -154,7 +156,7 @@ struct OperateButtonsView { .borderRadius('50%') .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true) .onClick(async () => { - this.videoManager.pause(); + this.previewVM.videoPause(); }) } @@ -169,7 +171,7 @@ struct OperateButtonsView { .borderColor(Color.White) .borderRadius('50%') .onClick(async () => { - this.videoManager.resume(); + this.previewVM.videoResume(); }) } @@ -208,9 +210,9 @@ struct OperateButtonsView { .borderRadius('50%') .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true) .onClick(async () => { - this.isDelayTakePhoto = false; + this.previewVM.isDelayTakePhoto = false; clearTimeout(this.photoDelayTimer); - this.photoRemainder = 0; + this.previewVM.photoRemainder = 0; }) } @@ -228,10 +230,10 @@ struct OperateButtonsView { this.previewVM.isFront = !this.previewVM.isFront; const cameraPosition = this.previewVM.getCameraPosition(); const sceneMode = this.previewVM.getSceneMode(); - await this.cameraManager.release(); - await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + await this.previewVM.cameraManagerRelease(); + await this.previewVM.cameraManagerStart(cameraPosition, sceneMode); // [StartExclude toggleCameraPositionButton] - this.syncButtonSettings(); + this.previewVM.syncButtonSettings(); // [EndExclude toggleCameraPositionButton] }) } @@ -241,21 +243,23 @@ struct OperateButtonsView { build() { Row() { this.thumbnailButton() + // Different buttons are shown in photo and video modes. if (this.previewVM.isPhotoMode()) { this.photoButton() } else { - if (this.videoManager.isRecording()) { + if (this.previewVM.isVideoRecording()) { this.videoStopButton() } else { this.videoStartButton() } } - if (!this.videoManager.isRecording()) { + // Different components are displayed for different states in video mode. + if (!this.previewVM.isVideoRecording()) { this.toggleCameraPositionButton() } - if (this.previewVM.isVideoMode() && this.videoManager.state === AVRecorderState.STARTED) { + if (this.previewVM.isVideoMode() && this.previewVM.checkVideoState(AVRecorderState.STARTED)) { this.videoPauseButton() - } else if (this.previewVM.isVideoMode() && this.videoManager.state === AVRecorderState.PAUSED) { + } else if (this.previewVM.isVideoMode() && this.previewVM.checkVideoState(AVRecorderState.PAUSED)) { this.videoResumeButton() } } diff --git a/entry/src/main/ets/views/PreviewImageView.ets b/entry/src/main/ets/views/PreviewImageView.ets new file mode 100644 index 0000000000000000000000000000000000000000..3d732a98aca42245f6783b437a44005741be23be --- /dev/null +++ b/entry/src/main/ets/views/PreviewImageView.ets @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 ("the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { display } from '@kit.ArkUI'; +import { Logger } from 'commons'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +const TAG = 'PreviewImageView'; + +/** + * Dual preview image component, displaying the second preview stream. + */ +@Component +export struct PreviewImageView { + @ObjectLink previewVM: PreviewViewModel; + private PreviewImageHeight: number = 80; + + // Determine the preview image size based on the current device size and rotation state. + getPreviewImageWidth() { + let displayDefault: display.Display | null = null; + try { + displayDefault = display.getDefaultDisplaySync(); + } catch (exception) { + Logger.error(TAG, `getDefaultDisplaySync failed, code is ${exception.code}, message is ${exception.message}`); + } + const rotation = (displayDefault?.rotation ?? 0) * 90; + const ratio = this.previewVM.getPreviewRatio(); + const displayRatio = rotation === 90 || rotation === 270 ? 1 / ratio : ratio; + return this.PreviewImageHeight / displayRatio; + } + + build() { + Image(this.previewVM.previewImage) + .width(this.getPreviewImageWidth()) + .height(this.PreviewImageHeight) + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, + left: { anchor: '__container__', align: HorizontalAlign.Start } + }) + .margin({ + bottom: 10, + left: 10 + }) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/views/PreviewScreenView.ets b/entry/src/main/ets/views/PreviewScreenView.ets new file mode 100644 index 0000000000000000000000000000000000000000..913996fbb7e935a3e769528a60393a04eefd262d --- /dev/null +++ b/entry/src/main/ets/views/PreviewScreenView.ets @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 ("the License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { camera } from '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { curves } from '@kit.ArkUI'; +import { Logger } from 'commons'; +import { GridLine, LevelIndicator } from 'camera'; +import CameraConstant from '../constants/Constants'; +import PermissionManager from '../utils/PermissionManager'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; +import RefreshableTimer from '../utils/RefreshableTimer'; +import { calCameraPoint, calFaceBoxLinePoint, getClampedChildPosition, limitNumberInRange, + LinePoint } from '../utils/CommonUtil'; + +/** + * Preview stream display component, + * showing the preview screen as well as focus frames and face detection frames + */ +@Component +export struct PreviewScreenView { + @State isFocusBoxVisible: boolean = false; + @State isZoomPinching: boolean = false; + @State focusBoxPosition: Edges = { top: 0, left: 0 }; + @State flashBlackOpacity: number = 1; + @State isShowBlack: boolean = false; + @Link sleepTimer: RefreshableTimer | undefined; + @ObjectLink previewVM: PreviewViewModel; + @StorageLink('captureClick') @Watch('onCaptureClick') captureClickFlag: number = 0; + private context: Context = this.getUIContext().getHostContext()!; + private applicationContext = this.context.getApplicationContext(); + private focusBoxTimer: RefreshableTimer = new RefreshableTimer(() => { + this.isFocusBoxVisible = false; + }, 3 * 1000); + private originZoomBeforePinch: number = 1; // record zoom after pinch, sale base it. + private focusBoxSize: Size = { width: 80, height: 80 }; + private exposureFontSize: number = 24; + + exitApp() { + this.applicationContext.killAllProcesses().catch((err: BusinessError) => { + Logger.error('showToast', `showToast failed, code is ${err.code}, message is ${err.message}`); + }); + } + + // Initialize the range of focal lengths that the current camera can be set to. + initZooms() { + const zoomRange = this.previewVM.getCameraZoomRange(); + const minZoom = zoomRange[0]; + this.previewVM.zoomRange = zoomRange; + if (minZoom < this.previewVM.zooms[0]) { + this.previewVM.zooms.unshift(minZoom); + } + } + + // Initialize the range of preview stream frame rates that can be set for the current camera. + initRates() { + const frameRates = this.previewVM.getPreviewSupportedFrameRates(); + if (frameRates && frameRates[0]) { + const minRate = frameRates[0].min; + const maxRate = frameRates[0].max; + this.previewVM.rates = [minRate, maxRate]; + this.previewVM.currentRate = maxRate; + this.previewVM.setPreviewFrameRate(maxRate, maxRate); + } + } + + // Get the exposure position through the focus frame position. + getExposurePosition(): Edges { + const focusBoxLeft = this.focusBoxPosition.left as number; + const focusBoxTop = this.focusBoxPosition.top as number; + const exposureWidth = this.exposureFontSize; + const exposureHeight = this.exposureFontSize; + const focusBoxWidth = this.focusBoxSize.width; + const focusBoxHeight = this.focusBoxSize.height; + const previewWidth = this.getUIContext().px2vp(this.previewVM.previewSize.width); + const GAP = 10; + const top = focusBoxTop - exposureHeight / 2 + focusBoxHeight / 2; + const left = focusBoxLeft > previewWidth / 2 + ? focusBoxLeft - GAP - exposureWidth + : focusBoxLeft + focusBoxWidth + GAP; + return { top, left }; + } + + // The screen flashes black when taking a photo. + flashBlackAnim() { + this.flashBlackOpacity = 1; + this.isShowBlack = true; + animateToImmediately({ + curve: curves.interpolatingSpring(1, 1, 410, 38), + delay: 50, + onFinish: () => { + this.isShowBlack = false; + this.flashBlackOpacity = 1; + } + }, () => { + this.flashBlackOpacity = 0; + }) + } + + onCaptureClick(): void { + this.flashBlackAnim(); + } + + // [Start faceBox] + @Builder + // Face detection box component. + faceBox(faceBoxRect: camera.Rect) { + ForEach(calFaceBoxLinePoint(faceBoxRect), (linePoint: LinePoint) => { + Line() + .startPoint([0, 0]) + .endPoint([linePoint.increment.x, linePoint.increment.y]) + .stroke(Color.White) + .position({ x: linePoint.start.x, y: linePoint.start.y }) + }, (linePoint: LinePoint) => JSON.stringify(linePoint)); + } + + // [End faceBox] + + build() { + // [Start Stack] + Stack({ + alignContent: Alignment.Center + }) { + // [Start XComponent] + // [Start XComponent_gesture] + XComponent({ + type: XComponentType.SURFACE, + controller: this.previewVM.xComponentController + }) + // [StartExclude Stack] + // [StartExclude XComponent_gesture] + .onLoad(async () => { + // [StartExclude XComponent] + await PermissionManager.request(CameraConstant.PERMISSIONS, this.context) + .catch(() => { + this.exitApp() + }); + // [EndExclude XComponent] + this.previewVM.surfaceId = this.previewVM.xComponentController.getXComponentSurfaceId(); + this.previewVM.setPreviewSize(); + this.previewVM.xComponentController.setXComponentSurfaceRotation({ lock: true }); + // [StartExclude XComponent] + await this.previewVM.cameraManagerStart(this.previewVM.getCameraPosition(), this.previewVM.getSceneMode()); + this.initZooms(); + this.initRates(); + // [EndExclude XComponent] + }) + // [StartExclude XComponent_gesture] + // [End XComponent] + .gesture( + // Adjust focus with two fingers pinchGesture. + PinchGesture({ fingers: 2 }) + .onActionStart(() => { + this.originZoomBeforePinch = this.previewVM.currentZoom; + this.isZoomPinching = true; + this.sleepTimer?.refresh(); + }) + .onActionUpdate((event: GestureEvent) => { + if (this.previewVM.isVideoMode() && this.previewVM.isStabilizationEnabled) { + return; + } + const targetZoom = this.originZoomBeforePinch * event.scale; + this.previewVM.currentZoom = limitNumberInRange(targetZoom, this.previewVM.zoomRange); + this.previewVM.setCameraZoomRatio(); + }) + .onActionEnd(() => { + this.isZoomPinching = false; + }) + ) + // [End XComponent_gesture] + // Click to set the focus position. + .onClick(event => { + this.isFocusBoxVisible = true; + const previewSize = this.previewVM.previewSize; + const cameraPoint = calCameraPoint( + this.getUIContext().vp2px(event.x), + this.getUIContext().vp2px(event.y), + previewSize.width, + previewSize.height + ); + this.previewVM.setCameraFocusPoint(cameraPoint); + this.previewVM.setCameraMeteringPoint(cameraPoint); + this.focusBoxPosition = getClampedChildPosition(this.focusBoxSize, { + width: this.getUIContext().px2vp(previewSize.width), + height: this.getUIContext().px2vp(previewSize.height) + }, event); + this.focusBoxTimer.refresh(); + }) + // [EndExclude Stack] + if (this.previewVM.isGridLineVisible) { + GridLine() + } + // [StartExclude Stack] + if (this.previewVM.isLevelIndicatorVisible) { + LevelIndicator({ + acc: this.previewVM.acc + }) + } + // focus box + if (this.isFocusBoxVisible) { + Image($r('app.media.focus_box')) + .width(80) + .height(80) + .position(this.focusBoxPosition) + SymbolGlyph($r('sys.symbol.sun_max')) + .fontSize(this.exposureFontSize) + .fontColor([Color.White]) + .position(this.getExposurePosition()) + } + + if (!this.isFocusBoxVisible) { + ForEach(this.previewVM.faceBoundingBoxArr, (value: camera.Rect) => { + this.faceBox(value); + }, (value: camera.Rect) => JSON.stringify(value)); + } + + if (this.previewVM.isDelayTakePhoto) { + Text(`${this.previewVM.photoRemainder}S`) + .fontSize(44) + .fontWeight(FontWeight.Regular) + .fontColor(Color.White) + } + // [EndExclude Stack] + + if (this.isShowBlack) { + Column() + .id('black') + .width('100%') + .height('100%') + .backgroundColor(Color.Black) + .opacity(this.flashBlackOpacity) + } + } + // [End Stack] + .alignRules({ + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + .width(this.previewVM.getPreviewWidth()) + .height(this.previewVM.getPreviewHeight()) + .margin({ top: this.previewVM.getPreviewTop() }) + .blur(this.previewVM.blurRadius) + .rotate(this.previewVM.blurRotation) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/views/SettingButtonsView.ets b/entry/src/main/ets/views/SettingButtonsView.ets index 559f433380af10bdf8a2bcf6588118f2cd839efd..75296d03c8dbd3d508dcd34a5b75f5f86a9318d4 100644 --- a/entry/src/main/ets/views/SettingButtonsView.ets +++ b/entry/src/main/ets/views/SettingButtonsView.ets @@ -14,7 +14,7 @@ */ import { camera } from '@kit.CameraKit'; -import { AVRecorderState, CameraManager, PhotoManager, PreviewManager, VideoManager } from 'camera'; +import { AVRecorderState } from 'camera'; import { showToast } from '../utils/CommonUtil'; import PreviewViewModel from '../viewmodels/PreviewViewModel'; @@ -25,8 +25,13 @@ interface FlashItem { toast: ResourceStr; } +/** + * Set up button components, configure the flash, preview frame rates, and other features. + */ @Component struct SettingButtonsView { + @State flashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE; + @ObjectLink previewVM: PreviewViewModel; private flashItems: FlashItem[] = [ { mode: camera.FlashMode.FLASH_MODE_CLOSE, @@ -63,16 +68,6 @@ struct SettingButtonsView { camera.FlashMode.FLASH_MODE_CLOSE, camera.FlashMode.FLASH_MODE_ALWAYS_OPEN ]; - @State flashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE; - @Link isLivePhoto: boolean; - @Require cameraManager: CameraManager; - @Require previewManager: PreviewManager; - @Require photoManager: PhotoManager; - @Link videoManager: VideoManager; - @Link photoDelayTime: number; - @Link isStabilizationEnabled: boolean; - @Link isSinglePhoto: boolean; - @Link previewVM: PreviewViewModel; getFlashItem(mode: camera.FlashMode) { return this.flashItems.find(item => item.mode === mode); @@ -89,7 +84,7 @@ struct SettingButtonsView { value: flashItem.title, action: () => { this.flashMode = mode!; - this.cameraManager.setFlashMode(mode); + this.previewVM.setCameraFlashMode(mode); showToast(this.getUIContext(), flashItem.toast); } }; @@ -99,15 +94,15 @@ struct SettingButtonsView { @Builder videoTimerBuilder() { - if (this.videoManager.isRecording()) { + if (this.previewVM.isVideoRecording()) { Row({ space: 5 }) { - SymbolGlyph(this.videoManager.state === AVRecorderState.STARTED ? $r('sys.symbol.record_circle') : + SymbolGlyph(this.previewVM.checkVideoState(AVRecorderState.STARTED)? $r('sys.symbol.record_circle') : $r('sys.symbol.pause')) .fontSize(22) - .fontColor(this.videoManager.state === AVRecorderState.STARTED ? [Color.Red, 'rgba(255,0,0,0)'] : + .fontColor(this.previewVM.checkVideoState(AVRecorderState.STARTED)? [Color.Red, 'rgba(255,0,0,0)'] : [Color.White]) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) - Text(this.videoManager.state === AVRecorderState.STARTED ? $r('app.string.recording') : $r('app.string.paused')) + Text(this.previewVM.checkVideoState(AVRecorderState.STARTED)? $r('app.string.recording') : $r('app.string.paused')) .fontColor(Color.White) .fontSize(12) } @@ -116,13 +111,13 @@ struct SettingButtonsView { @Builder livePhotoButton() { - SymbolGlyph(this.isLivePhoto + SymbolGlyph(this.previewVM.isLivePhoto ? $r('sys.symbol.livephoto') : $r('sys.symbol.livephoto_slash')) .onClick(() => { - this.isLivePhoto = !this.isLivePhoto; - this.photoManager.enableMovingPhoto(this.isLivePhoto); - const message = this.isLivePhoto ? $r('app.string.moving_open') : $r('app.string.moving_close'); + this.previewVM.isLivePhoto = !this.previewVM.isLivePhoto; + this.previewVM.enableMovingPhoto(); + const message = this.previewVM.isLivePhoto ? $r('app.string.moving_open') : $r('app.string.moving_close'); showToast(this.getUIContext(), message); }) .fontSize(22) @@ -139,7 +134,7 @@ struct SettingButtonsView { const menuElement: MenuElement = { value: rate + 'fps', action: () => { - this.previewManager.setFrameRate(rate, rate); + this.previewVM.setPreviewFrameRate(rate, rate); this.previewVM.currentRate = rate; showToast(this.getUIContext(), $r('app.string.preview_rate', rate + 'fps')); } @@ -151,8 +146,8 @@ struct SettingButtonsView { @Builder delayPhotoButton(photoDelayTimeElements: MenuElement[]) { - if (this.photoDelayTime) { - Text(`${this.photoDelayTime}s`) + if (this.previewVM.photoDelayTime) { + Text(`${this.previewVM.photoDelayTime}s`) .fontColor(Color.White) .fontSize(16) .bindMenu(photoDelayTimeElements) @@ -176,7 +171,7 @@ struct SettingButtonsView { const menuElement: MenuElement = { value: text, action: () => { - this.photoDelayTime = time!; + this.previewVM.photoDelayTime = time!; const message = time ? $r('app.string.delay', text) : $r('app.string.delay_close'); showToast(this.getUIContext(), message); } @@ -187,13 +182,13 @@ struct SettingButtonsView { @Builder stabilizationButton() { - SymbolGlyph(this.isStabilizationEnabled + SymbolGlyph(this.previewVM.isStabilizationEnabled ? $r('sys.symbol.motion_stabilization') : $r('sys.symbol.motion_stabilization_slash')) .onClick(() => { - this.isStabilizationEnabled = !this.isStabilizationEnabled; - this.cameraManager.setVideoStabilizationMode(camera.VideoStabilizationMode.AUTO); - const message = this.isStabilizationEnabled ? $r('app.string.stabilization_enable') : $r('app.string.stabilization_disabled'); + this.previewVM.isStabilizationEnabled = !this.previewVM.isStabilizationEnabled; + this.previewVM.setCameraVideoStabilizationMode(camera.VideoStabilizationMode.AUTO); + const message = this.previewVM.isStabilizationEnabled ? $r('app.string.stabilization_enable') : $r('app.string.stabilization_disabled'); showToast(this.getUIContext(), message); }) .fontSize(22) @@ -202,17 +197,17 @@ struct SettingButtonsView { @Builder togglePhotoModeButton() { - SymbolGlyph(this.isSinglePhoto + SymbolGlyph(this.previewVM.isSinglePhoto ? $r('sys.symbol.picture') : $r('sys.symbol.picture_on_square_1')) .onClick(() => { - this.isSinglePhoto = !this.isSinglePhoto; - this.photoManager.setPhotoOutputCallback(this.isSinglePhoto); - if (this.isSinglePhoto) { - this.isLivePhoto = false; + this.previewVM.isSinglePhoto = !this.previewVM.isSinglePhoto; + this.previewVM.setPhotoOutputCallback(); + if (this.previewVM.isSinglePhoto) { + this.previewVM.isLivePhoto = false; } - this.photoManager.enableMovingPhoto(this.isLivePhoto); - const message = this.isSinglePhoto ? $r('app.string.photo_single') : $r('app.string.photo_double'); + this.previewVM.enableMovingPhoto(); + const message = this.previewVM.isSinglePhoto ? $r('app.string.photo_single') : $r('app.string.photo_double'); showToast(this.getUIContext(), message); }) .fontSize(22) @@ -225,12 +220,12 @@ struct SettingButtonsView { this.rateButton() this.flashButton(this.photoFlashModes) this.delayPhotoButton(this.getPhotoDelayTimeElements()) - if (!this.isSinglePhoto) { + if (!this.previewVM.isSinglePhoto) { this.livePhotoButton() } this.togglePhotoModeButton() } else { - if (this.videoManager.isRecording()) { + if (this.previewVM.isVideoRecording()) { this.videoTimerBuilder() } else { this.rateButton() diff --git a/entry/src/main/ets/views/ZoomButtonsView.ets b/entry/src/main/ets/views/ZoomButtonsView.ets index 1c943b7e522fa118aaa33b763e5d9d43d16908da..d998a2a8ab0174712510243711f9f7559a88f7c0 100644 --- a/entry/src/main/ets/views/ZoomButtonsView.ets +++ b/entry/src/main/ets/views/ZoomButtonsView.ets @@ -13,36 +13,36 @@ * limitations under the License. */ -import { CameraManager } from 'camera'; import { findRangeIndex, toFixed } from '../utils/CommonUtil'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; +/** + * Focal length setting button component, used to set wide-angle and different zoom levels. + */ @Component struct ZoomButtonsView { - @Prop @Require zooms: number[]; - @Prop @Require zoomRange: number[] = []; - @Link currentZoom: number; - @Require cameraManager: CameraManager; + @ObjectLink previewVM: PreviewViewModel; getZoomButtonText(zoom: number, index: number): string { - const minZoom = this.zoomRange[0]; - const currentZoomIndex: number = findRangeIndex(this.currentZoom, this.zooms); - if (index === 0 && (this.currentZoom === minZoom || currentZoomIndex !== index)) { + const minZoom = this.previewVM.zoomRange[0]; + const currentZoomIndex: number = findRangeIndex(this.previewVM.currentZoom, this.previewVM.zooms); + if (index === 0 && (this.previewVM.currentZoom === minZoom || currentZoomIndex !== index)) { return 'w'; } - if (this.currentZoom === zoom || currentZoomIndex !== index) { + if (this.previewVM.currentZoom === zoom || currentZoomIndex !== index) { return `${zoom}x`; } - return `${toFixed(this.currentZoom, 1)}x`; + return `${toFixed(this.previewVM.currentZoom, 1)}x`; } getZoomButtonBorderWidth(index: number): number { - const currentZoomIndex: number = findRangeIndex(this.currentZoom, this.zooms); + const currentZoomIndex: number = findRangeIndex(this.previewVM.currentZoom, this.previewVM.zooms); return currentZoomIndex === index ? 1.5 : 0; } build() { Row({ space: 15 }) { - ForEach(this.zooms, (zoom: number, index: number) => { + ForEach(this.previewVM.zooms, (zoom: number, index: number) => { Text(this.getZoomButtonText(zoom, index)) .width(36) .height(36) @@ -54,8 +54,8 @@ struct ZoomButtonsView { .borderRadius('50%') .textAlign(TextAlign.Center) .onClick(() => { - this.cameraManager.setSmoothZoom(zoom); - this.currentZoom = zoom; + this.previewVM.setCameraSmoothZoom(zoom); + this.previewVM.currentZoom = zoom; }) }, (zoom: number) => zoom.toString()) } diff --git a/screenshots/devices/camera_people.png b/screenshots/devices/camera_people.png new file mode 100644 index 0000000000000000000000000000000000000000..eabe80d720cab72477eeefefd23a0e33d3f6ca5f Binary files /dev/null and b/screenshots/devices/camera_people.png differ diff --git a/screenshots/devices/camera_people_en.png b/screenshots/devices/camera_people_en.png new file mode 100644 index 0000000000000000000000000000000000000000..5fef6fa7a25c7fc1678b1880305302367a882125 Binary files /dev/null and b/screenshots/devices/camera_people_en.png differ