diff --git a/README.en.md b/README.en.md index fe3d863a426d9f197dd471a0d2fba961123793fe..e5da618f5feb41670905110d6286bfc784463501 100644 --- a/README.en.md +++ b/README.en.md @@ -24,6 +24,7 @@ The application allows users to click on the circular button at the bottom to ta │ └──utils │ ├──CameraShooter.ets // Photographing │ ├──GravityUtil.ets // Gravity tools +│ ├──PreviewUtil.ets // Preview tools │ └──VideoRecorder.ets // Video recording └──entry/src/main/resource // Static resources ``` diff --git a/README.md b/README.md index 4af074af3deb4a3dd3c67bab3cd5bb0d32c162cf..c0a8907a0076d5d704f791586641f812f4332e83 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ │ └──utils │ ├──CameraShooter.ets // 拍照 │ ├──GravityUtil.ets // 重力工具类 +│ ├──PreviewUtil.ets // 预览工具类 │ └──VideoRecorder.ets // 录像 └──entry/src/main/resource // 应用静态资源目录 ``` diff --git a/entry/src/main/ets/constants/CameraConstants.ets b/entry/src/main/ets/constants/CameraConstants.ets index 41fd44c4110c4693c7202ea41e3a40cf13c34e6e..6ae775307943c19ec762c7ddcacd8b7060bb5133 100644 --- a/entry/src/main/ets/constants/CameraConstants.ets +++ b/entry/src/main/ets/constants/CameraConstants.ets @@ -49,7 +49,7 @@ export class CameraConstants { /** * Parameter Configuration button margin top */ - public static readonly MARGIN_TOP: number = 30; + public static readonly MARGIN_TOP: number = 40; /** * camera switch icon size */ @@ -110,4 +110,24 @@ export class CameraConstants { * count down time font size */ public static readonly COUNT_DOWN_FONT_SIZE: number = 100; + /** + * camera shoot ratio + */ + public static readonly PHOTO_RATIO: number = 4 / 3; + /** + * video record ratio + */ + public static readonly VIDEO_RATIO: number = 16 / 9; + /** + * control panel bottom margin + */ + public static readonly CONTROL_MARGIN_BOTTOM: number = 80; + /** + * mode panel bottom margin + */ + public static readonly MODE_MARGIN_BOTTOM: number = 20; + /** + * zoom panel bottom margin + */ + public static readonly ZOOM_MARGIN_BOTTOM: number = 40; } \ 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 0a75bb616dd4ca302033ff30cc656c56cd8758d1..ee5257a782866848233ebf1f223aa7f28a65619c 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -16,7 +16,7 @@ import { dataSharePredicates } from '@kit.ArkData'; import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; import { camera } from '@kit.CameraKit'; -import { MovingPhotoViewController, photoAccessHelper } from '@kit.MediaLibraryKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { CameraConstants } from '../constants/CameraConstants'; import { filePreview } from '@kit.PreviewKit'; import { @@ -42,9 +42,10 @@ import { setPhotoZoom } from '../utils/CameraShooter'; import display from '@ohos.display'; -import { curves } from '@kit.ArkUI'; +import { curves, window } from '@kit.ArkUI'; import { sensor } from '@kit.SensorServiceKit'; import { Context } from '@ohos.arkui.UIContext'; +import { getCurrentWindowSize, getPreviewSize } from '../utils/PreviewUtil'; let cameraPosition = 0; let surfaceId = ''; @@ -53,8 +54,9 @@ let isVideo = false; let qualityLevel: number = 0; let storage = new LocalStorage(); let videoUri: string; -let foldAbleStatus: number = 0; let currentFov: number = 1; +const DEFAULT_TOP_OFFSET = + CameraConstants.IMAGE_SIZE + CameraConstants.MARGIN * 2 + CameraConstants.MARGIN_TOP + CameraConstants.MARGIN_HEIGHT; class CountDownTimerModifier implements ContentModifier { applyContent(): WrappedBuilder<[TextTimerConfiguration]> { @@ -66,6 +68,7 @@ class CountDownTimerModifier implements ContentModifier function buildTextTimer(config: TextTimerConfiguration) { Text(Math.ceil(config.count / 1000 - config.elapsedTime / 100).toString()) .width(CameraConstants.FULL_SCREEN) + .height(CameraConstants.FULL_SCREEN) .fontSize(CameraConstants.COUNT_DOWN_FONT_SIZE) .fontColor(Color.White) .textAlign(TextAlign.Center) @@ -86,8 +89,6 @@ struct XComponentPage { @State isFront: boolean = false; // Is recording now @State recording: boolean = false; - @State isFoldAble: boolean = display.isFoldable(); - @State foldAbleStatus: number = this.isFoldAble ? display.getFoldStatus() : FoldStatus.FOLD_STATUS_UNKNOWN; @StorageLink('photoUri') photoUri: string | Resource | PixelMap = ''; // Indicates whether the current preview type is an image @State currentPic: boolean = true; @@ -103,6 +104,11 @@ struct XComponentPage { countDownTimeController: TextTimerController = new TextTimerController(); countDownTimeModifier: CountDownTimerModifier = new CountDownTimerModifier(); countDownInterval: number = 0; + // Window object from EntryAbility + windowClass = AppStorage.get('window'); + // Fold status for camera auto switching + foldStatus: display.FoldStatus = display.getFoldStatus(); + // Icon rotate angle @State rotation: number = 0; // Show zoom info on screen @State isShowZoom: boolean = false; @@ -110,6 +116,10 @@ struct XComponentPage { @State isCountingDown: boolean = false; // Global context @State context: Context = this.getUIContext().getHostContext()!; + // Count down view size + @State countDownSize: Size = { width: 0, height: 0 }; + // Count down view offset relative to the screen + @State countDownOffset: Position = { x: 0, y: 0 }; onPageShow(): void { filePreview.closePreview(this.context); @@ -117,6 +127,11 @@ struct XComponentPage { async aboutToAppear() { sensor.on(sensor.SensorId.GRAVITY, (data: sensor.GravityResponse) => { + // Disable icon rotation when display rotation is not zero + if (display.getDefaultDisplaySync().rotation !== 0) { + this.rotation = 0; + return; + } let degree: number = -1; degree = this.getCalDegree(data.x, data.y, data.z); if (degree >= 0 && (degree <= 30 || degree >= 330)) { @@ -131,37 +146,30 @@ struct XComponentPage { // Use ROTATION_270 when degree range is [240, 300] this.rotation = camera.ImageRotation.ROTATION_90; } - }) + }); + this.updatePreview(getPreviewSize(isVideo)); abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, this.permissions).then(() => { setTimeout(async () => { - if (this.isFoldAble) { - foldAbleStatus = display.getFoldStatus(); - display.on('foldStatusChange', (foldStatus: display.FoldStatus) => { - if (foldStatus === 3) { - return; - } - foldAbleStatus = foldStatus; - this.foldAbleStatus = foldStatus; - cameraPosition = cameraPosition === 0 ? 0 : 1; - setTimeout(() => { - this.mXComponentController.setXComponentSurfaceRect({ - surfaceWidth: this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 1400 : - display.getDefaultDisplaySync().width, - surfaceHeight: this.isFoldAble && foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 1400 : - display.getDefaultDisplaySync().width * (this.isPhoto ? 4 / 3 : 16 / 9) - }) - cameraShooting(isVideo, cameraPosition, surfaceId, this.context, foldAbleStatus); - }, 500) - }) - } - zoomRatioRange = await cameraShooting(isVideo, cameraPosition, surfaceId, this.context, foldAbleStatus); + // Update preview when window rectangle changed + this.windowClass?.on('windowRectChange', () => { + const currFoldStatus = display.getFoldStatus(); + if (currFoldStatus !== this.foldStatus) { + // Update camera and preview when fold status changed + this.foldStatus = currFoldStatus; + cameraShooting(isVideo, cameraPosition, surfaceId, this.context, this.updatePreview); + } else { + // Update preview only + this.updatePreview(getPreviewSize(isVideo)); + } + }); + zoomRatioRange = await cameraShooting(isVideo, cameraPosition, surfaceId, this.context, this.updatePreview); }, 200); }); - } aboutToDisappear(): void { clearInterval(this.countDownInterval); + this.windowClass?.off('windowRectChange'); sensor.off(sensor.SensorId.GRAVITY); } @@ -176,7 +184,50 @@ struct XComponentPage { } build() { - Column() { + RelativeContainer() { + // Top count down view + TextTimer({ isCountDown: true, count: this.countDownTime, controller: this.countDownTimeController }) + .contentModifier(this.countDownTimeModifier) + .visibility(this.isCountingDown ? Visibility.Visible : Visibility.Hidden) + .size(this.countDownSize) + .offset(this.countDownOffset) + .zIndex(1) + .onClick(() => { + clearInterval(this.countDownInterval); + this.isCountingDown = false; + this.countDownTimeController.reset(); + }) + + // Main xComponent view + XComponent({ + type: XComponentType.SURFACE, + controller: this.mXComponentController + }) + .gesture( + PinchGesture({ fingers: 2 }) + .onActionUpdate((event: GestureEvent) => { + if (event && !this.isStabilization && !this.isCountingDown) { + this.zoom = currentFov * event.scale; + this.isShowZoom = true; + if (this.zoom > (this.isPhoto ? zoomRatioRange[1] : 15)) { + this.zoom = this.isPhoto ? zoomRatioRange[1] : 15; + } else if (this.zoom < zoomRatioRange[0]) { + this.zoom = zoomRatioRange[0]; + } + this.isPhoto ? setPhotoZoom(this.zoom) : setVideoZoom(this.zoom) + } + }) + .onActionEnd(() => { + this.isPhoto ? currentFov = getPhotoZoom() : currentFov = getVideoZoom(); + this.isShowZoom = false; + }) + ) + .onLoad(async () => { + this.mXComponentController.setXComponentSurfaceRotation({ lock: true }); + surfaceId = this.mXComponentController.getXComponentSurfaceId(); + }) + + // Option panel Row() { Image(this.isStabilization ? $r('app.media.stabilization_on') : $r('app.media.stabilization_off')) .height(CameraConstants.IMAGE_SIZE) @@ -186,8 +237,8 @@ struct XComponentPage { .animation({ curve: curves.springMotion() }) .onClick(() => { this.isStabilization = !this.isStabilization; - videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, foldAbleStatus); isVideo = true; + videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context); }); Image(this.isMovingPhoto ? $r('app.media.live_photo_on') : $r('app.media.live_photo_off')) .height(CameraConstants.IMAGE_SIZE) @@ -265,8 +316,7 @@ struct XComponentPage { action: (): void => { qualityLevel = 0; stopRecordPreview(); - videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, - foldAbleStatus); + videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context); } }, { @@ -274,8 +324,7 @@ struct XComponentPage { action: (): void => { qualityLevel = 1; stopRecordPreview(); - videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, - foldAbleStatus); + videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context); } } ] @@ -316,246 +365,230 @@ struct XComponentPage { ] ); } + .id('optionPanel') .visibility(this.recording || this.isCountingDown ? Visibility.Hidden : Visibility.Visible) .width(CameraConstants.FULL_SCREEN) + .alignRules({ + top: { anchor: '__container__', align: VerticalAlign.Top } + }) .margin({ top: CameraConstants.MARGIN_TOP, bottom: CameraConstants.MARGIN_HEIGHT }) - .justifyContent(FlexAlign.SpaceAround); + .justifyContent(FlexAlign.SpaceAround) + + // Top zoom info view + Text(this.zoom === zoomRatioRange[0] ? 'wide angle' : this.zoom.toFixed(1) + 'x') + .fontColor(Color.White) + .visibility(this.isShowZoom && !this.isFront && !this.isCountingDown ? Visibility.Visible : Visibility.Hidden) + .alignRules({ + bottom: { anchor: 'zoomPanel', align: VerticalAlign.Top }, + middle: { anchor: 'zoomPanel', align: HorizontalAlign.Center } + }) + .margin({ bottom: CameraConstants.ZOOM_MARGIN_BOTTOM }) + .zIndex(1) - Stack() { - TextTimer({ controller: this.countDownTimeController, isCountDown: true, count: this.countDownTime }) - .contentModifier(this.countDownTimeModifier) - .visibility(this.isCountingDown ? Visibility.Visible : Visibility.Hidden) - .position({ - top: this.isPhoto || this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 200 : 285 + // Zoom panel + Row() { + Image(0 < this.zoom && this.zoom < 1 ? $r('app.media.W_0') : $r('app.media.W')) + .height(CameraConstants.ZOOM_SIZE) + .rotate({ angle: this.rotation }) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + this.zoom = zoomRatioRange[0]; + this.setZoom(); }) - .zIndex(1) + Image($r('app.media.small_2x')) + .height(CameraConstants.SMALL_DOT) + .opacity(CameraConstants.OPACITY) + .margin({ left: CameraConstants.MARGIN_LEFT }) + Image(1 <= this.zoom && this.zoom < 5 ? $r('app.media.1x_0') : $r('app.media.1x')) + .margin({ left: CameraConstants.MARGIN_LEFT }) + .height(CameraConstants.ZOOM_SIZE) + .rotate({ angle: this.rotation }) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + this.zoom = 1; + this.setZoom(); + }) + Image($r('app.media.small_2x')) + .margin({ left: CameraConstants.MARGIN_LEFT }) + .height(CameraConstants.SMALL_DOT) + .opacity(CameraConstants.OPACITY) + Image(5 <= this.zoom && this.zoom < 10 ? $r('app.media.5x_0') : $r('app.media.5x')) + .margin({ left: CameraConstants.MARGIN_LEFT }) + .height(CameraConstants.ZOOM_SIZE) + .rotate({ angle: this.rotation }) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + this.zoom = 5; + this.setZoom(); + }) + Image($r('app.media.small_2x')) + .margin({ left: CameraConstants.MARGIN_LEFT }) + .height(CameraConstants.SMALL_DOT) + .opacity(CameraConstants.OPACITY) + Image(this.zoom >= 10 ? $r('app.media.10x_0') : $r('app.media.10x')) + .margin({ left: CameraConstants.MARGIN_LEFT }) + .height(CameraConstants.ZOOM_SIZE) + .rotate({ angle: this.rotation }) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + this.zoom = 10; + this.setZoom(); + }) + } + .id('zoomPanel') + .visibility(this.isFront || this.isStabilization || this.isCountingDown ? Visibility.Hidden : Visibility.Visible) + .alignRules({ + bottom: { anchor: 'modePanel', align: VerticalAlign.Top }, + middle: { anchor: 'modePanel', align: HorizontalAlign.Center } + }) + .margin({ bottom: CameraConstants.ZOOM_MARGIN_BOTTOM }) - XComponent({ - type: XComponentType.SURFACE, - controller: this.mXComponentController - }) - .gesture( - PinchGesture({ fingers: 2 }) - .onActionUpdate((event: GestureEvent) => { - if (event && !this.isStabilization && !this.isCountingDown) { - this.zoom = currentFov * event.scale; - this.isShowZoom = true; - if (this.zoom > (this.isPhoto ? zoomRatioRange[1] : 15)) { - this.zoom = this.isPhoto ? zoomRatioRange[1] : 15; - } else if (this.zoom < zoomRatioRange[0]) { - this.zoom = zoomRatioRange[0]; - } - this.isPhoto ? setPhotoZoom(this.zoom) : setVideoZoom(this.zoom) - } - }) - .onActionEnd(() => { - this.isPhoto ? currentFov = getPhotoZoom() : currentFov = getVideoZoom(); - this.isShowZoom = false; - }) - ) - .onLoad(async () => { - this.mXComponentController.setXComponentSurfaceRect({ - surfaceWidth: this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 1400 : - display.getDefaultDisplaySync().width, - surfaceHeight: this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 1400 : - display.getDefaultDisplaySync().width * 4 / 3 - }); - surfaceId = this.mXComponentController.getXComponentSurfaceId(); + // Mode panel + Row() { + Text($r('app.string.Photo')) + .fontColor(this.isPhoto ? Color.White : Color.Gray) + .rotate({ angle: this.rotation === camera.ImageRotation.ROTATION_180 ? this.rotation : 0 }) + .animation({ curve: curves.springMotion() }) + .onClick(async () => { + if (!this.isPhoto) { + this.isPhoto = true; + isVideo = false; + stopRecordPreview(); + this.Initialize(); + cameraShooting(isVideo, cameraPosition, surfaceId, this.context, this.updatePreview); + } }) - .height(this.isPhoto || this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 500 : 670); - Row() { - Image(0 < this.zoom && this.zoom < 1 ? $r('app.media.W_0') : $r('app.media.W')) - .height(CameraConstants.ZOOM_SIZE) - .rotate({ angle: this.rotation }) - .animation({ curve: curves.springMotion() }) - .onClick(() => { - this.zoom = zoomRatioRange[0]; - this.setZoom(); - }); - Image($r('app.media.small_2x')) - .height(CameraConstants.SMALL_DOT) - .opacity(CameraConstants.OPACITY) - .margin({ left: CameraConstants.MARGIN_LEFT }) - Image(1 <= this.zoom && this.zoom < 5 ? $r('app.media.1x_0') : $r('app.media.1x')) - .margin({ left: CameraConstants.MARGIN_LEFT }) - .height(CameraConstants.ZOOM_SIZE) - .rotate({ angle: this.rotation }) - .animation({ curve: curves.springMotion() }) + Text($r('app.string.Video')) + .fontColor(this.isPhoto ? Color.Gray : Color.White) + .rotate({ angle: this.rotation === camera.ImageRotation.ROTATION_180 ? this.rotation : 0 }) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + if (this.isPhoto) { + this.isPhoto = false; + isVideo = true; + releaseCamera(); + // 1080P + this.Initialize(); + videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, + this.updatePreview); + } + }) + } + .id('modePanel') + .visibility(this.recording || this.isCountingDown ? Visibility.Hidden : Visibility.Visible) + .justifyContent(FlexAlign.SpaceAround) + .width(CameraConstants.CENTER_WIDTH) + .height(CameraConstants.CAPTURE_SIZE) + .alignRules({ + bottom: { anchor: 'controlPanel', align: VerticalAlign.Top }, + middle: { anchor: 'controlPanel', align: HorizontalAlign.Center } + }) + .margin({ bottom: CameraConstants.MODE_MARGIN_BOTTOM }) + + // Control panel + Row() { + Image(this.photoUri) + .borderWidth(this.photoUri === '' ? 0 : 1) + .visibility(this.recording ? Visibility.Hidden : Visibility.Visible) + .borderColor(Color.White) + .height(CameraConstants.CAMERA_SWITCH_SIZE) + .width(CameraConstants.CAMERA_SWITCH_SIZE) + .borderRadius(CameraConstants.CAMERA_SWITCH_SIZE / 2) + .rotate({ angle: this.rotation }) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + if (this.photoUri !== '') { + this.currentPic ? previewPhoto(this.context) : previewVideo(this.context, videoUri); + } + }) + Stack() { + Image($r('app.media.capture')) + .height(CameraConstants.CAPTURE_SIZE) + .visibility(this.isPhoto ? Visibility.Visible : Visibility.Hidden) .onClick(() => { - this.zoom = 1; - this.setZoom(); + if (this.countDownTime !== 0) { + this.isCountingDown = true; + this.countDownTimeController.start(); + } + this.countDownInterval = setTimeout(() => { + this.isCountingDown = false; + this.countDownTimeController.reset(); + capture(this.isFront); + this.currentPic = true; + }, this.countDownTime); }) - Image($r('app.media.small_2x')) - .margin({ left: CameraConstants.MARGIN_LEFT }) - .height(CameraConstants.SMALL_DOT) - .opacity(CameraConstants.OPACITY) - Image(5 <= this.zoom && this.zoom < 10 ? $r('app.media.5x_0') : $r('app.media.5x')) - .margin({ left: CameraConstants.MARGIN_LEFT }) - .height(CameraConstants.ZOOM_SIZE) - .rotate({ angle: this.rotation }) - .animation({ curve: curves.springMotion() }) - .onClick(() => { - this.zoom = 5; - this.setZoom(); + Image($r('app.media.record')) + .height(CameraConstants.CAPTURE_SIZE) + .visibility(this.isPhoto ? Visibility.Hidden : this.recording ? Visibility.Hidden : Visibility.Visible) + .onClick(async () => { + if (this.countDownTime !== 0) { + this.isCountingDown = true; + this.countDownTimeController.start(); + } + this.countDownInterval = setTimeout(() => { + this.isCountingDown = false; + this.countDownTimeController.reset(); + startRecord(); + this.textTimerController.start(); + this.recording = true; + }, this.countDownTime); }) - Image($r('app.media.small_2x')) - .margin({ left: CameraConstants.MARGIN_LEFT }) - .height(CameraConstants.SMALL_DOT) - .opacity(CameraConstants.OPACITY) - Image(this.zoom >= 10 ? $r('app.media.10x_0') : $r('app.media.10x')) - .margin({ left: CameraConstants.MARGIN_LEFT }) - .height(CameraConstants.ZOOM_SIZE) - .rotate({ angle: this.rotation }) - .animation({ curve: curves.springMotion() }) - .onClick(() => { - this.zoom = 10; - this.setZoom(); + Image($r('app.media.recording')) + .visibility(this.isPhoto ? Visibility.Hidden : this.recording ? Visibility.Visible : Visibility.Hidden) + .height(CameraConstants.CAPTURE_SIZE) + .onClick(async () => { + this.textTimerController.reset(); + this.recording = false; + this.currentPic = false; + this.zoom = 1; + currentFov = 1; + this.flashPic = $r('app.media.ic_camera_public_flash_off'); + videoUri = await stopRecord(); + stopRecordPreview(); + videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context); + setTimeout(() => { + this.getThumbnail(); + }, 500); }) } - .visibility(this.isFront || this.isStabilization || this.isCountingDown ? Visibility.Hidden : Visibility.Visible) - .margin({ top: this.isFoldAble ? CameraConstants.PREVIEW_HEIGHT_BUTTON : 450 }) - Text(this.zoom === zoomRatioRange[0] ? 'wide angle' : this.zoom.toFixed(1) + 'x') - .fontColor(Color.White) - .margin({ top: 250 }) - .visibility(this.isShowZoom && !this.isFront && !this.isCountingDown ? Visibility.Visible : Visibility.Hidden) - - Column() { - Row() { - Text($r('app.string.Photo')) - .fontColor(this.isPhoto ? Color.White : Color.Gray) - .rotate({ angle: this.rotation === camera.ImageRotation.ROTATION_180 ? this.rotation : 0 }) - .animation({ curve: curves.springMotion() }) - .onClick(async () => { - if (!this.isPhoto) { - this.isPhoto = true; - isVideo = false; - stopRecordPreview(); - this.Initialize(); - cameraShooting(isVideo, cameraPosition, surfaceId, this.context, foldAbleStatus); - } - }) - Text($r('app.string.Video')) - .fontColor(this.isPhoto ? Color.Gray : Color.White) - .rotate({ angle: this.rotation === camera.ImageRotation.ROTATION_180 ? this.rotation : 0 }) - .animation({ curve: curves.springMotion() }) - .onClick(() => { - if (this.isPhoto) { - this.isPhoto = false; - releaseCamera(); - // 1080P - this.Initialize(); - videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, - foldAbleStatus); - isVideo = true; - } - }) - } - .visibility(this.recording || this.isCountingDown ? Visibility.Hidden : Visibility.Visible) - .justifyContent(FlexAlign.SpaceAround) - .width(CameraConstants.CENTER_WIDTH) - .height(CameraConstants.CAPTURE_SIZE) - .margin({ bottom: CameraConstants.MARGIN_HEIGHT }) - - Row() { - Image(this.photoUri) - .borderWidth(this.photoUri === '' ? 0 : 1) - .visibility(this.recording ? Visibility.Hidden : Visibility.Visible) - .borderColor(Color.White) - .height(CameraConstants.CAMERA_SWITCH_SIZE) - .width(CameraConstants.CAMERA_SWITCH_SIZE) - .borderRadius(CameraConstants.CAMERA_SWITCH_SIZE / 2) - .rotate({ angle: this.rotation }) - .animation({ curve: curves.springMotion() }) - .onClick(() => { - if (this.photoUri !== '') { - this.currentPic ? previewPhoto(this.context) : previewVideo(this.context, videoUri); - } - }) - Stack() { - Image($r('app.media.capture')) - .height(CameraConstants.CAPTURE_SIZE) - .visibility(this.isPhoto ? Visibility.Visible : Visibility.Hidden) - .onClick(() => { - if (this.countDownTime !== 0) { - this.isCountingDown = true; - this.countDownTimeController.start(); - } - this.countDownInterval = setTimeout(() => { - this.isCountingDown = false; - this.countDownTimeController.reset(); - capture(this.isFront); - this.currentPic = true; - }, this.countDownTime); - }) - Image($r('app.media.record')) - .height(CameraConstants.CAPTURE_SIZE) - .visibility(this.isPhoto ? Visibility.Hidden : this.recording ? Visibility.Hidden : Visibility.Visible) - .onClick(async () => { - if (this.countDownTime !== 0) { - this.isCountingDown = true; - this.countDownTimeController.start(); - } - this.countDownInterval = setTimeout(() => { - this.isCountingDown = false; - this.countDownTimeController.reset(); - startRecord(); - this.textTimerController.start(); - this.recording = true; - }, this.countDownTime); - }) - Image($r('app.media.recording')) - .visibility(this.isPhoto ? Visibility.Hidden : this.recording ? Visibility.Visible : Visibility.Hidden) - .height(CameraConstants.CAPTURE_SIZE) - .onClick(async () => { - this.textTimerController.reset(); - this.recording = false; - this.currentPic = false; - this.zoom = 1; - currentFov = 1; - this.flashPic = $r('app.media.ic_camera_public_flash_off'); - videoUri = await stopRecord(); - stopRecordPreview(); - videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, - foldAbleStatus); - setTimeout(() => { - this.getThumbnail(); - }, 500); - }) + Image($r('app.media.switch_camera')) + .height(CameraConstants.CAMERA_SWITCH_SIZE) + .visibility(this.recording ? Visibility.Hidden : Visibility.Visible) + .rotate({ angle: this.rotation }) + .animation({ curve: curves.springMotion() }) + .onClick(async () => { + cameraPosition = cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK ? + camera.CameraPosition.CAMERA_POSITION_UNSPECIFIED : camera.CameraPosition.CAMERA_POSITION_BACK; + if (this.isPhoto) { + cameraShooting(isVideo, cameraPosition, surfaceId, this.context, this.updatePreview); + } else { + stopRecordPreview(); + videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, + this.updatePreview); } - - Image($r('app.media.switch_camera')) - .height(CameraConstants.CAMERA_SWITCH_SIZE) - .visibility(this.recording ? Visibility.Hidden : Visibility.Visible) - .rotate({ angle: this.rotation }) - .animation({ curve: curves.springMotion() }) - .onClick(async () => { - cameraPosition = cameraPosition === 1 ? 0 : 1 - - if (this.isPhoto) { - cameraShooting(isVideo, cameraPosition, surfaceId, this.context, foldAbleStatus); - } else { - stopRecordPreview(); - videoRecording(this.isStabilization, cameraPosition, qualityLevel, surfaceId, this.context, - foldAbleStatus); - } - this.Initialize(); - this.isFront = cameraPosition !== 0; - }) - } - .visibility(this.isCountingDown ? Visibility.Hidden : Visibility.Visible) - .width(CameraConstants.FULL_SCREEN) - .justifyContent(FlexAlign.SpaceAround) - - TextTimer({ controller: this.textTimerController }) - .format(CameraConstants.TIME_FORMAT) - .fontColor(Color.White) - .fontSize(CameraConstants.MARGIN_TOP) - .visibility(this.recording ? Visibility.Visible : Visibility.Hidden) - } - .margin({ top: 500 }) + this.Initialize(); + this.isFront = cameraPosition !== 0; + }) } - .alignContent(Alignment.Top) + .id('controlPanel') + .visibility(this.isCountingDown ? Visibility.Hidden : Visibility.Visible) + .width(CameraConstants.FULL_SCREEN) + .justifyContent(FlexAlign.SpaceAround) + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Bottom } + }) + .margin({ bottom: CameraConstants.CONTROL_MARGIN_BOTTOM }) + + TextTimer({ controller: this.textTimerController }) + .format(CameraConstants.TIME_FORMAT) + .fontColor(Color.White) + .fontSize(CameraConstants.MARGIN_TOP) + .visibility(this.recording ? Visibility.Visible : Visibility.Hidden) + .alignRules({ + top: { anchor: 'controlPanel', align: VerticalAlign.Bottom }, + middle: { anchor: 'controlPanel', align: HorizontalAlign.Center } + }) } .height(CameraConstants.FULL_SCREEN) .backgroundColor(Color.Black) @@ -567,12 +600,6 @@ struct XComponentPage { this.isStabilization = false; this.flashPic = $r('app.media.ic_camera_public_flash_off'); this.isMovingPhoto = false; - this.mXComponentController.setXComponentSurfaceRect({ - surfaceWidth: this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 1400 : - display.getDefaultDisplaySync().width, - surfaceHeight: this.isFoldAble && this.foldAbleStatus === FoldStatus.FOLD_STATUS_EXPANDED ? 1400 : - display.getDefaultDisplaySync().width * (this.isPhoto ? 4 / 3 : 16 / 9) - }) } switchFlash(flashMode: number): void { @@ -599,6 +626,26 @@ struct XComponentPage { this.photoUri = await photoAsset.getThumbnail(); } } + + updatePreview = (previewSize: Size) => { + const windowSize = getCurrentWindowSize(); + // Use setXComponentSurfaceRect to adjust preview size and position + this.mXComponentController.setXComponentSurfaceRect({ + surfaceWidth: previewSize.width, + surfaceHeight: previewSize.height, + offsetY: previewSize.height >= windowSize.height ? 0 : vp2px(DEFAULT_TOP_OFFSET) + }); + + // Adjust count down view size and position to match preview + this.countDownOffset = { + x: previewSize.width >= windowSize.width ? 0 : px2vp((windowSize.width - previewSize.width) / 2), + y: previewSize.height >= windowSize.height ? 0 : DEFAULT_TOP_OFFSET + }; + this.countDownSize = { + width: px2vp(previewSize.width), + height: px2vp(previewSize.height) + }; + } } export async function fromBack(context: Context): Promise { @@ -608,5 +655,5 @@ export async function fromBack(context: Context): Promise { storage.setOrCreate('isStabilization', false); storage.setOrCreate('isMovingPhoto', false); storage.setOrCreate('countDownTime', 0); - cameraShooting(isVideo, cameraPosition, surfaceId, context, foldAbleStatus); + cameraShooting(isVideo, cameraPosition, surfaceId, context); } \ No newline at end of file diff --git a/entry/src/main/ets/utils/CameraShooter.ets b/entry/src/main/ets/utils/CameraShooter.ets index 60fd483f5ab24e6003d6fac3573096927b149ebb..1b39336645c8be4de21481b2e18da5e5a0c3c526 100644 --- a/entry/src/main/ets/utils/CameraShooter.ets +++ b/entry/src/main/ets/utils/CameraShooter.ets @@ -18,9 +18,10 @@ import { videoRecording } from './VideoRecorder'; import { BusinessError } from '@kit.BasicServicesKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { common } from '@kit.AbilityKit'; -import { display } from '@kit.ArkUI'; import { colorSpaceManager } from '@kit.ArkGraphics2D'; import { getGravity } from './GravityUtil'; +import { getPreviewSize, getTargetPreviewProfile, setPreviewRotation } from './PreviewUtil'; +import { CameraConstants } from '../constants/CameraConstants'; let previewOutput: camera.PreviewOutput; let cameraInput: camera.CameraInput; @@ -30,9 +31,9 @@ let currentContext: Context; let uri: string; export async function cameraShooting(isVideo: boolean, cameraPosition: number, surfaceId: string, - context: Context, foldAbleStatus: number): Promise { + context: Context, callback?: (previewSize: Size) => void): Promise { if (isVideo) { - return videoRecording(false, cameraPosition, 0, surfaceId, context, foldAbleStatus); + return videoRecording(false, cameraPosition, 0, surfaceId, context, callback); } currentContext = context; isVideo = false; @@ -73,26 +74,17 @@ export async function cameraShooting(isVideo: boolean, cameraPosition: number, s // [Start photo_Profiles_Array] let photoProfilesArray: camera.Profile[] = cameraOutputCap.photoProfiles; // [End photo_Profiles_Array] - let previewProfile: undefined | camera.Profile = previewProfilesArray.find((profile: camera.Profile) => { - let screen = display.getDefaultDisplaySync(); - if (screen.width <= 1080) { - return profile.size.height === 1080 && profile.size.width === 1440; - } else if (screen.width <= 1440 && screen.width > 1080) { - return profile.size.height === 1440 && profile.size.width === 1920; - } - return profile.size.height <= screen.width && profile.size.height >= 1080 && - (profile.size.width / profile.size.height) < (screen.height / screen.width) && - (profile.size.width / profile.size.height) > - (foldAbleStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ? 1 : 4 / 3); - }); + + let previewProfile = getTargetPreviewProfile(CameraConstants.PHOTO_RATIO, previewProfilesArray); let photoProfile: undefined | camera.Profile = photoProfilesArray.find((profile: camera.Profile) => { if (previewProfile) { return profile.size.width <= 4096 && profile.size.width >= 2448 && - profile.size.height === (foldAbleStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ? 1 : - (previewProfile.size.height / previewProfile.size.width)) * profile.size.width; + profile.size.height === (previewProfile.size.height / previewProfile.size.width) * profile.size.width; } return undefined; }); + // Set preview window by callback + callback?.(getPreviewSize(false)); // [Start preview_ProfilesArray] previewOutput = cameraManager.createPreviewOutput(previewProfile, surfaceId); // [End preview_ProfilesArray] @@ -122,6 +114,8 @@ export async function cameraShooting(isVideo: boolean, cameraPosition: number, s await photoSession.commitConfig(); await photoSession.start(); // [End photo_Session1] + setPreviewRotation(previewOutput); + // Check whether the device supports the flash. let flashStatus: boolean = photoSession.hasFlash(); if (flashStatus) { @@ -217,6 +211,7 @@ function setPhotoOutputCb(photoOutput: camera.PhotoOutput): void { AppStorage.setOrCreate('photoUri', await photoAsset.getThumbnail()); }); } + // [End setPhotoOutput_Cb] /** * Jump to the system application gallery @@ -232,6 +227,7 @@ export function previewPhoto(context: Context): void { abilityName: 'com.huawei.hmos.photos.MainAbility' }) } + // [End preview_Photo] /** * Get cur photo camera.ImageRotation diff --git a/entry/src/main/ets/utils/PreviewUtil.ets b/entry/src/main/ets/utils/PreviewUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..397b2352ee7305c7ececb04d9d09da84b01c49ea --- /dev/null +++ b/entry/src/main/ets/utils/PreviewUtil.ets @@ -0,0 +1,117 @@ +/* + * 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, window } from "@kit.ArkUI"; +import { camera } from "@kit.CameraKit"; +import { CameraConstants } from "../constants/CameraConstants"; + +/** + * Get camera preview size + * @param isVideo + * @returns previewSize in Size + */ +export function getPreviewSize(isVideo: boolean) { + // Use display size to determine preview ratio in current direction + const displaySize = getCurrentDisplaySize(); + const isWidthLonger = displaySize.width > displaySize.height; + + // Get screen ratio and target ratio, use them to determine preview size + const windowSize = getCurrentWindowSize(); + const screenRatio = + isWidthLonger ? (windowSize.width / windowSize.height) : (windowSize.height / windowSize.width); + const targetRatio = isVideo ? CameraConstants.VIDEO_RATIO : CameraConstants.PHOTO_RATIO; + + let previewSize: Size = { width: 0, height: 0 }; + if (targetRatio >= screenRatio) { + previewSize.width = isWidthLonger ? windowSize.width : windowSize.height / targetRatio; + previewSize.height = isWidthLonger ? windowSize.width / targetRatio : windowSize.height; + } else { + previewSize.width = isWidthLonger ? windowSize.height * targetRatio : windowSize.width; + previewSize.height = isWidthLonger ? windowSize.height : windowSize.width * targetRatio; + } + + return previewSize; +} + +/** + * Get camera preview profile + * @param targetRatio - preview ratio, the default image ratio is 4 / 3 and the default video ratio is 16 / 9 + * @param previewProfilesArray - available preview profiles provided by camera + * @returns - target preview profile + */ +export function getTargetPreviewProfile(targetRatio: number, previewProfilesArray: camera.Profile[]) { + // The width of preview profile is always equal or larger than height, use window size to determine preview profile + const windowSize = getCurrentWindowSize(); + const isWidthLonger = windowSize.width > windowSize.height; + const screenRatio = + isWidthLonger ? (windowSize.width / windowSize.height) : (windowSize.height / windowSize.width); + let lowLength = isWidthLonger ? windowSize.height : windowSize.width; + let maxDiff = Number.MAX_VALUE; + + let previewProfile: camera.Profile | undefined = undefined; + for (let i = 0; i < previewProfilesArray.length; i++) { + const profileRatio = previewProfilesArray[i].size.width / previewProfilesArray[i].size.height; + // Find preview profile with target ratio + if (profileRatio !== targetRatio) { + continue; + } + + // Find profile size closest to window size + if (screenRatio >= profileRatio) { + const currDiff = Math.abs(previewProfilesArray[i].size.height - lowLength); + if (currDiff < maxDiff) { + previewProfile = previewProfilesArray[i]; + maxDiff = currDiff; + } + } else { + const currDiff = Math.abs(previewProfilesArray[i].size.width - lowLength * screenRatio); + if (currDiff < maxDiff) { + previewProfile = previewProfilesArray[i]; + maxDiff = currDiff; + } + } + } + + return previewProfile; +} + +/** + * Set property preview rotation according to display rotation + * @param previewOutput + */ +export function setPreviewRotation(previewOutput: camera.PreviewOutput) { + let initDisplayRotation = display.getDefaultDisplaySync().rotation; + let initPreviewRotation = previewOutput.getPreviewRotation(initDisplayRotation * camera.ImageRotation.ROTATION_90); + previewOutput.setPreviewRotation(initPreviewRotation, true); +} + +/** + * Get current window size + * @returns window size if window exist, otherwise zero + */ +export function getCurrentWindowSize() { + const windowClass = AppStorage.get('window'); + const windowSize = windowClass?.getWindowProperties().windowRect; + return { width: windowSize?.width ?? 0, height: windowSize?.height ?? 0 } as Size; +} + +/** + * Get current display size + * @returns screen display size + */ +export function getCurrentDisplaySize() { + const displaySize = display.getPrimaryDisplaySync(); + return { width: displaySize.width, height: displaySize.height } as Size; +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/VideoRecorder.ets b/entry/src/main/ets/utils/VideoRecorder.ets index bd7f99a78f2a79acabbcb426082f153749b71b44..5140b001cbae7494d72e10ecb5e1b0a1cfc71603 100644 --- a/entry/src/main/ets/utils/VideoRecorder.ets +++ b/entry/src/main/ets/utils/VideoRecorder.ets @@ -18,9 +18,10 @@ import { media } from '@kit.MediaKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { fileIo } from '@kit.CoreFileKit'; import { common } from '@kit.AbilityKit'; -import { display } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; import { getGravity } from './GravityUtil'; +import { getPreviewSize, getTargetPreviewProfile, setPreviewRotation } from './PreviewUtil'; +import { CameraConstants } from '../constants/CameraConstants'; let file: fileIo.File; let previewOutput: camera.PreviewOutput; @@ -31,7 +32,7 @@ let videoSession: camera.VideoSession; let uri: string; export async function videoRecording(isStabilization: boolean, cameraPosition: number, qualityLevel: number, - surfaceId: string, context: Context, foldAbleStatus: number): Promise { + surfaceId: string, context: Context, callback?: (previewSize: Size) => void): Promise { let cameraManager: camera.CameraManager = camera.getCameraManager(context); if (!cameraManager) { return []; @@ -66,39 +67,30 @@ export async function videoRecording(isStabilization: boolean, cameraPosition: n if (!videoProfilesArray) { return []; } - let previewProfile: undefined | camera.Profile = previewProfilesArray.find((profile: camera.Profile) => { - let screen = display.getDefaultDisplaySync(); - if (screen.width <= 1440) { - return profile.size.height === 1080 && profile.size.width === 1920; - } - return profile.size.height <= screen.width && profile.size.height >= 1080 && - (profile.size.width / profile.size.height) < (screen.height / screen.width) && - (profile.size.width / profile.size.height) > - (foldAbleStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ? 1 : 16 / 9); - }) + + let previewProfile = getTargetPreviewProfile(CameraConstants.VIDEO_RATIO, previewProfilesArray); // [Start video_profile1] let videoProfile: undefined | camera.VideoProfile = videoProfilesArray.find((profile: camera.VideoProfile) => { if (previewProfile && cameraPosition === 1) { return profile.size.width >= 1080 && profile.size.height >= 1080 && - profile.size.height === (foldAbleStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ? 1 : - (previewProfile.size.height / previewProfile.size.width)) * profile.size.width && + profile.size.height === (previewProfile.size.height / previewProfile.size.width) * profile.size.width && profile.frameRateRange.max === 30; } if (previewProfile && qualityLevel === 0) { return profile.size.width <= 1920 && profile.size.width >= 1080 && profile.size.height >= 1080 && - profile.size.height === (foldAbleStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ? 1 : - (previewProfile.size.height / previewProfile.size.width)) * profile.size.width && + profile.size.height === (previewProfile.size.height / previewProfile.size.width) * profile.size.width && profile.frameRateRange.max === 60; } if (previewProfile && qualityLevel === 1 && cameraPosition === 0) { return profile.size.width <= 4096 && profile.size.width >= 3000 && - profile.size.height === (foldAbleStatus === display.FoldStatus.FOLD_STATUS_EXPANDED ? 1 : - (previewProfile.size.height / previewProfile.size.width)) * profile.size.width && + profile.size.height === (previewProfile.size.height / previewProfile.size.width) * profile.size.width && profile.frameRateRange.max === 60; } return undefined; }) // [End video_profile1] + // Set preview window by callback + callback?.(getPreviewSize(true)); // Set the parameters based on the actual hardware range. let aVRecorderProfile: media.AVRecorderProfile = { audioBitrate: 48000, @@ -184,6 +176,7 @@ export async function videoRecording(isStabilization: boolean, cameraPosition: n // [EndExclude begin_config] await videoSession.start(); // [End begin_config] + setPreviewRotation(previewOutput); // Obtains the variable focal length ratio range supported by the camera. let zoomRatioRange = videoSession.getZoomRatioRange(); return zoomRatioRange; @@ -221,8 +214,8 @@ export function setVideoSmoothZoom(zoom: number): void { export async function startRecord(): Promise { // Update the rotation angle before starting recording - const deviceDegree = await getGravity() - await avRecorder.updateRotation(getVideoRotation(videoOutput,deviceDegree)) + const deviceDegree = await getGravity(); + await avRecorder.updateRotation(getVideoRotation(videoOutput, deviceDegree)); await videoOutput.start(); // [Start recorder1] await avRecorder.start(); @@ -241,6 +234,7 @@ export async function stopRecord(): Promise { fileIo.closeSync(file); return uri; } + // [Start pre_video1] export function previewVideo(context: Context, videoUri: string): void { let videoContext = context as common.UIAbilityContext; @@ -251,6 +245,7 @@ export function previewVideo(context: Context, videoUri: string): void { abilityName: 'com.huawei.hmos.photos.MainAbility' }) } + // [End pre_video1] /** * Get cur video camera.ImageRotation diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index ab008f50f7eb01bb4f7c98bc1fbd5f4cfc26ab94..e983875cc096acf71535102fa2a47af7a2a21b3f 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -6,6 +6,7 @@ "mainElement": "EntryAbility", "deviceTypes": [ "phone", + "tablet" ], "deliveryWithInstall": true, "installationFree": false, diff --git a/screenshots/capture.en.png b/screenshots/capture.en.png index 9aefcee1b0dc9b6affeb067a85c3323c8bd1e777..1c6a54f4f335e4d10626954371a13b3f09dd9f56 100644 Binary files a/screenshots/capture.en.png and b/screenshots/capture.en.png differ diff --git a/screenshots/capture.png b/screenshots/capture.png index 3c8c86d6946169b2b33079f8c0766bd6a03919ee..0692d13bc667a298f0116ecf71fdaf3a4a27f13c 100644 Binary files a/screenshots/capture.png and b/screenshots/capture.png differ diff --git a/screenshots/record.en.png b/screenshots/record.en.png index 1e7c5d0c1996d23b951ab058a4c17381434ddbaf..a73d220af71356eb42dda9e064ab51bbc8a3f8a1 100644 Binary files a/screenshots/record.en.png and b/screenshots/record.en.png differ diff --git a/screenshots/record.png b/screenshots/record.png index 64d40ce8db95beca885143a315f10d82ba1ace0b..2d1c383dbaa304a283470b73072e2bf12d0476dd 100644 Binary files a/screenshots/record.png and b/screenshots/record.png differ