From 89a9e37a3b23857d23746c049d5397090fb57b7d Mon Sep 17 00:00:00 2001 From: lon9 <815882449@qq.com> Date: Tue, 29 Apr 2025 20:30:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E8=AE=BE=E5=A4=87=E7=9B=B8=E6=9C=BASa?= =?UTF-8?q?mple=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppScope/app.json5 | 2 +- AppScope/resources/base/element/string.json | 2 +- .../main/ets/entryability/EntryAbility.ets | 20 +- entry/src/main/ets/pages/Index.ets | 184 +++++++++++++----- entry/src/main/ets/utils/CameraUtil.ets | 10 +- entry/src/main/ets/utils/WindowUtil.ets | 70 +++++++ entry/src/main/ets/views/CommonView.ets | 50 ++++- entry/src/main/module.json5 | 2 +- 8 files changed, 289 insertions(+), 51 deletions(-) create mode 100644 entry/src/main/ets/utils/WindowUtil.ets diff --git a/AppScope/app.json5 b/AppScope/app.json5 index 1c93f41..a2c38ed 100644 --- a/AppScope/app.json5 +++ b/AppScope/app.json5 @@ -1,6 +1,6 @@ { "app": { - "bundleName": "com.example.multishortvideo", + "bundleName": "com.example.multidevicecamera", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", diff --git a/AppScope/resources/base/element/string.json b/AppScope/resources/base/element/string.json index d0af575..9578586 100644 --- a/AppScope/resources/base/element/string.json +++ b/AppScope/resources/base/element/string.json @@ -2,7 +2,7 @@ "string": [ { "name": "app_name", - "value": "MultiShortVideo" + "value": "MultiDeviceCamera" } ] } diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 4f91d3c..943098a 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -19,6 +19,7 @@ import { display, window } from '@kit.ArkUI'; import { BusinessError, deviceInfo } from '@kit.BasicServicesKit'; import { CameraUtil } from '../utils/CameraUtil'; import { camera } from '@kit.CameraKit'; +import { WindowUtil } from '../utils/WindowUtil'; const DOMAIN = 0x0000; @@ -26,6 +27,8 @@ export default class EntryAbility extends UIAbility { windowData?: window.Window; uiContext?: UIContext; cameraUtil?: CameraUtil = CameraUtil.getInstance(); + windowUtil?: WindowUtil = WindowUtil.getInstance(); + isFirstCreated: boolean = true; onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => { let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint(); AppStorage.setOrCreate('widthBp', widthBp); @@ -38,12 +41,23 @@ export default class EntryAbility extends UIAbility { let surfaceId: string | undefined = AppStorage.get('surfaceId'); if (widthBp === WidthBreakpoint.WIDTH_MD && heightBp === HeightBreakpoint.HEIGHT_MD && deviceInfo.productSeries === 'GRL') { + console.info(`testLog ---> 窗口变换拉起 grl`); this.cameraUtil?.cameraShooting(surfaceId!, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); return; } + if (widthBp === WidthBreakpoint.WIDTH_SM && heightBp === HeightBreakpoint.HEIGHT_MD) { + // When switching to a wide folding external screen, onForeground() will be triggered. + return; + } + if (this.isFirstCreated) { + this.isFirstCreated = false; + return; + } if (isFront) { + console.info(`testLog ---> 窗口变换拉起 front`); this.cameraUtil?.cameraShooting(surfaceId!, this.context!, camera.CameraPosition.CAMERA_POSITION_FRONT); } else { + console.info(`testLog ---> 窗口变换拉起 back`); this.cameraUtil?.cameraShooting(surfaceId!, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); } } @@ -69,6 +83,7 @@ export default class EntryAbility extends UIAbility { // Main window is created, set main page for this ability hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + this.windowUtil!.setWindowStage(windowStage); windowStage.loadContent('pages/Index', (err) => { if (err.code) { hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); @@ -104,7 +119,8 @@ export default class EntryAbility extends UIAbility { onForeground(): void { // Ability has brought to foreground hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); - if (AppStorage.get('isBackground')) { + if (AppStorage.get('isBackground')) { + console.info(`testLog ---> 前台拉起`); this.cameraUtil?.fromBack(); AppStorage.setOrCreate('isBackground', false); } @@ -113,7 +129,7 @@ export default class EntryAbility extends UIAbility { onBackground(): void { // Ability has back to background hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); - this.cameraUtil?.releaseCamera();`` + this.cameraUtil?.releaseCamera(); AppStorage.setOrCreate('isBackground', true); } } \ 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 0c483c6..e4eeabb 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -13,13 +13,14 @@ * limitations under the License. */ -import { display, window } from '@kit.ArkUI'; +import { ComposeListItem, display, window } from '@kit.ArkUI'; import { camera } from '@kit.CameraKit'; import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; import { BusinessError, deviceInfo } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; -import { ChooseMusic, SettingButton, ShotArea, ShotAreaSm } from '../views/CommonView'; +import { ChooseMusic, SettingButton, ShotArea, ShotAreaHalfFolded, ShotAreaSm } from '../views/CommonView'; import { CameraUtil } from '../utils/CameraUtil'; +import { WindowUtil } from '../utils/WindowUtil'; @Entry @Component @@ -31,16 +32,35 @@ struct Index { @StorageLink('surfaceId') surfaceId: string = ''; @StorageLink('rotation') rotation: number = 0; @StorageLink('isFront') isFront: boolean = false; + @StorageLink('isHalfFolded') isHalfFolded: boolean = false; + @StorageLink('creaseRegion') creaseRegion: number[] = []; context?: Context = this.getUIContext().getHostContext(); xComponentController: XComponentController = new XComponentController(); cameraUtil?: CameraUtil = CameraUtil.getInstance(); + windowUtil?: WindowUtil = WindowUtil.getInstance(); permissions: Array = [ 'ohos.permission.CAMERA', 'ohos.permission.READ_IMAGEVIDEO', 'ohos.permission.WRITE_IMAGEVIDEO' ]; + onFoldStatusChange: (foldStatus: display.FoldStatus) => void = (foldStatus: display.FoldStatus) => { + if (foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) { + let orientation: display.Orientation = display.getDefaultDisplaySync().orientation; + console.info(`testLog ---> 当前显示方向: ${orientation}`); + if (this.widthBp === WidthBreakpoint.WIDTH_MD && (orientation === display.Orientation.LANDSCAPE || + orientation === display.Orientation.LANDSCAPE_INVERTED)) { + this.isHalfFolded = true; + this.windowUtil?.setMainWindowOrientation(window.Orientation.LANDSCAPE); + } + } else { + this.isHalfFolded = false; + this.windowUtil?.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED); + } + }; aboutToAppear(): void { + this.windowUtil!.getFoldCreaseRegion(); + display.on('foldStatusChange', this.onFoldStatusChange); abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, this.permissions).then(() => { setTimeout(async () => { this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); @@ -49,22 +69,13 @@ struct Index { } aboutToDisappear(): void { - try { - window.getLastWindow(this.context, (err: BusinessError, data) => { - if (err) { - hilog.error(0x0000, 'testTag', `Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`); - return; - } - data.off('windowSizeChange'); - }); - } catch (err) { - hilog.error(0x0000, 'testTag', `Failed to obtain the top window. Cause code: ${err.code}, message: ${err.message}`); - } + display.off('foldStatusChange'); + this.windowUtil!.offWindowSizeChange(); } build() { Navigation() { - FolderStack({ upperItems: ['upper'] }) { + Stack() { // 相机组件 Column() { XComponent({ @@ -85,33 +96,14 @@ struct Index { .aspectRatio(this.getXComponentAspectRatio()) } .width('100%') - .layoutWeight(1) - .id('upper') - .alignItems(this.widthBp === WidthBreakpoint.WIDTH_MD && this.heightBp === HeightBreakpoint.HEIGHT_MD ? - HorizontalAlign.Start : HorizontalAlign.Center) + .height(this.isHalfFolded ? this.creaseRegion[0] : '') + .layoutWeight(this.isHalfFolded ? 0 : 1) + .alignItems(this.widthBp === WidthBreakpoint.WIDTH_MD && this.heightBp === HeightBreakpoint.HEIGHT_MD && + !this.isHalfFolded ? HorizontalAlign.Start : HorizontalAlign.Center) .margin({ bottom: deviceInfo.productSeries !== 'VDE' && this.widthBp === WidthBreakpoint.WIDTH_SM ? 156 : 0 }) // 拍照组件 Stack() { - // sm断点对应选择音乐区 - Row() { - Image($r('app.media.icon_close')) - .width(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) - .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) - .position({ x: 16, y: 0 }) - - ChooseMusic() - } - .width('100%') - .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 60 : 72) - .padding({ - top: 16, - bottom: 16 - }) - .position({ x: 0, y: 0 }) - .justifyContent(FlexAlign.Center) - .visibility(this.widthBp === WidthBreakpoint.WIDTH_SM ? Visibility.Visible : Visibility.None) - // sm断点对应设置区 Column() { SettingButton({ @@ -130,7 +122,25 @@ struct Index { .width(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 30 : 48) .height('100%') .margin({ right: 16 }) - .padding({ top: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 16 : 80}) + .padding({ top: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 16 : 108 }) + .visibility(this.widthBp === WidthBreakpoint.WIDTH_SM ? Visibility.Visible : Visibility.None) + + // sm断点对应选择音乐区 + Row() { + Image($r('app.media.icon_close')) + .width(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) + .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) + .position({ x: 16, y: 0 }) + + ChooseMusic() + } + .width('100%') + .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) + .position({ + x: 0, + y: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 16 : 28 + }) + .justifyContent(FlexAlign.Center) .visibility(this.widthBp === WidthBreakpoint.WIDTH_SM ? Visibility.Visible : Visibility.None) // sm断点对应拍照区 @@ -194,20 +204,97 @@ struct Index { Column() { ShotArea() } - .width(this.widthBp === WidthBreakpoint.WIDTH_MD ? 92 : 132) + .width(this.widthBp === WidthBreakpoint.WIDTH_LG && deviceInfo.productSeries === 'GRL' ? 132 : 92) .height('100%') .justifyContent(FlexAlign.Center) - .padding({ right: this.widthBp === WidthBreakpoint.WIDTH_MD ? 16 : 56 }) + .padding({ right: this.widthBp === WidthBreakpoint.WIDTH_LG && deviceInfo.productSeries === 'GRL' ? 56 : 16 }) .visibility(this.widthBp === WidthBreakpoint.WIDTH_MD || this.widthBp === WidthBreakpoint.WIDTH_LG ? Visibility.Visible : Visibility.None) } .height('100%') .width('100%') .alignContent(Alignment.BottomEnd) + .visibility(this.isHalfFolded ? Visibility.None : Visibility.Visible) + + Stack() { + // 悬停态相关按钮 + Row() { + SettingButton({ + imageButton: $r('app.media.icon_lighting'), + text: '闪光灯' + }) + SettingButton({ + imageButton: $r('app.media.icon_filters'), + text: '滤镜' + }) + .margin({ + left: 16, + right: 16 + }) + SettingButton({ + imageButton: $r('app.media.icon_setting'), + text: '设置' + }) + } + .width('100%') + .height(72) + .padding({ top: 6 }) + .justifyContent(FlexAlign.Center) + .position({ x: 0, y: this.creaseRegion[0] + this.creaseRegion[1] }) + + Row() { + ChooseMusic() + } + .width('100%') + .justifyContent(FlexAlign.Center) + .padding({ bottom: 32 }) + + Column() { + Image($r('app.media.icon_close')) + .width(40) + .height(40) + .position({ x: 0, y: this.creaseRegion[0] + this.creaseRegion[1] }) + + Column() { + Row() { + Text('照片') + .fontSize(14) + .fontColor(Color.White) + .fontWeight(700) + Blank() + Image($r('app.media.icon_red_circle')) + .height(6) + .width(6) + } + .height(20) + .width(40) + .margin({ bottom: 16 }) + + Text('视频') + .fontSize(14) + .fontColor('#80FFFFFF') + .width(40) + } + .width(40) + .height(56) + .position({ x: 0, y: this.creaseRegion[0] + this.creaseRegion[1] + 108 }) + } + .width(64) + .padding({ left: 24 }) + .position({ x: 0, y: 0 }) + + ShotAreaHalfFolded() + .position({ x: this.creaseRegion[2] - 100, y: this.creaseRegion[0] + this.creaseRegion[1] + 24 }) + } + .width('100%') + .height('100%') + .alignContent(Alignment.BottomEnd) + .visibility(this.isHalfFolded ? Visibility.Visible : Visibility.None) } .height('100%') .width('100%') - .alignContent(this.widthBp === WidthBreakpoint.WIDTH_MD ? Alignment.Start : Alignment.Center) + .alignContent(this.widthBp === WidthBreakpoint.WIDTH_MD ? (this.isHalfFolded ? Alignment.Top : Alignment.Start) : + Alignment.Center) } .height('100%') .width('100%') @@ -225,12 +312,21 @@ struct Index { return 1; } if (this.widthBp === WidthBreakpoint.WIDTH_MD) { - if (this.heightBp === HeightBreakpoint.HEIGHT_MD && (this.displayOrientation === display.Orientation.PORTRAIT || - this.displayOrientation === display.Orientation.PORTRAIT_INVERTED)) { + if (this.heightBp === HeightBreakpoint.HEIGHT_MD && (this.displayOrientation === display.Orientation.LANDSCAPE || + this.displayOrientation === display.Orientation.LANDSCAPE_INVERTED)) { + return 1.33; + } else if (this.heightBp === HeightBreakpoint.HEIGHT_LG && deviceInfo.productSeries === 'GRL') { + return 1.33; + } else { return 0.75; } + } + if (this.widthBp === WidthBreakpoint.WIDTH_LG) { + if (deviceInfo.productSeries === 'GRL') { + return 0.75 + } return 1.33; } - return 0.75; + return 1.33; } } \ No newline at end of file diff --git a/entry/src/main/ets/utils/CameraUtil.ets b/entry/src/main/ets/utils/CameraUtil.ets index 6903ac3..1a7571a 100644 --- a/entry/src/main/ets/utils/CameraUtil.ets +++ b/entry/src/main/ets/utils/CameraUtil.ets @@ -55,7 +55,7 @@ export class CameraUtil { } let cameraIndex: number = this.getCamera(cameraArray, cameraPosition); this.cameraInput = cameraManager.createCameraInput(cameraArray[cameraIndex]); - // console.info(`testLog ---> 选择相机:${JSON.stringify(cameraArray[cameraIndex])}`); + console.info(`testLog ---> 选择相机:${JSON.stringify(cameraArray[cameraIndex])}`); // Open the camera. await this.cameraInput.open(); let cameraRotation: number = cameraArray[cameraIndex].cameraOrientation; @@ -72,13 +72,16 @@ export class CameraUtil { } let previewProfileArray: camera.Profile[] = cameraOutputCap.previewProfiles; + // console.info(`testLog ---> 预览配置:${JSON.stringify(previewProfileArray)}`); let previewProfile: camera.Profile = this.getProfile(previewProfileArray); + console.info(`testLog ---> 选择预览配置:${JSON.stringify(previewProfile)}`); let photoProfileArray: camera.Profile[] = cameraOutputCap.photoProfiles; let photoProfile: camera.Profile = this.getProfile(photoProfileArray); this.previewOutput = cameraManager.createPreviewOutput(previewProfile, surfaceId); if (this.previewOutput === undefined) { return; } + let deviceRotation: number = display.getDefaultDisplaySync().rotation; console.info(`testLog ---> 当前设备旋转方向 ${deviceRotation}`); this.photoOutput = cameraManager.createPhotoOutput(photoProfile); @@ -98,6 +101,11 @@ export class CameraUtil { this.photoSession.addOutput(this.photoOutput); this.photoSession.setColorSpace(colorSpaceManager.ColorSpace.DISPLAY_P3); await this.photoSession.commitConfig(); + try { + this.previewOutput.setPreviewRotation(camera.ImageRotation.ROTATION_270, true); + } catch (err) { + hilog.error(0x0000, 'testLog', `Fail to set rotation, err: ${JSON.stringify(err)}`); + } await this.photoSession.start(); return; } diff --git a/entry/src/main/ets/utils/WindowUtil.ets b/entry/src/main/ets/utils/WindowUtil.ets new file mode 100644 index 0000000..8539e80 --- /dev/null +++ b/entry/src/main/ets/utils/WindowUtil.ets @@ -0,0 +1,70 @@ +/* + * 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 { hilog } from "@kit.PerformanceAnalysisKit"; +import { BusinessError } from "@kit.BasicServicesKit"; + +export class WindowUtil { + windowStage?: window.WindowStage; + mainWindow?: window.Window; + + static getInstance(): WindowUtil | undefined { + if (!AppStorage.get('windowUtil')) { + AppStorage.setOrCreate('windowUtil', new WindowUtil()); + } else { + hilog.info(0x0000, 'testTag', '%{public}s', `AppStorage does not have windowUtil.`); + } + return AppStorage.get('windowUtil'); + } + + setWindowStage(windowStage: window.WindowStage): void { + this.windowStage = windowStage; + this.windowStage.getMainWindow((err, windowClass: window.Window) => { + this.mainWindow = windowClass; + if (err.code) { + hilog.error(0x0000, 'testTag', `Failed to obtain the main window. Code:${err.code}, message:${err.message}`, + JSON.stringify(err) ?? ''); + return; + } + }); + } + + setMainWindowOrientation(orientation: window.Orientation): void { + // Setting orientation. + this.mainWindow!.setPreferredOrientation(orientation) + .then(() => { + hilog.info(0x0000, 'testTag', '%{public}s', `Succeed in setting the orientation.`); + }) + .catch((err: BusinessError) => { + hilog.error(0x0000, 'testTag', `Failed to set the orientation. Code: ${err.code}, message: ${err.message}`, + JSON.stringify(err) ?? ''); + }); + } + + offWindowSizeChange(): void { + this.mainWindow!.off('windowSizeChange'); + } + + getFoldCreaseRegion(): void { + if (display.isFoldable()) { + let foldRegion: display.FoldCreaseRegion = display.getCurrentFoldCreaseRegion(); + let rect: display.Rect = foldRegion.creaseRects[0]; + // Height of the avoidance area in the upper half screen and height of the avoidance area. + let creaseRegion: number[] = [px2vp(rect.top), px2vp(rect.height), px2vp(rect.width)]; + AppStorage.setOrCreate('creaseRegion', creaseRegion); + } + } +} \ No newline at end of file diff --git a/entry/src/main/ets/views/CommonView.ets b/entry/src/main/ets/views/CommonView.ets index 4efb4a9..9dd7cd2 100644 --- a/entry/src/main/ets/views/CommonView.ets +++ b/entry/src/main/ets/views/CommonView.ets @@ -59,7 +59,7 @@ export struct SettingButton { .fontSize(new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 12 : 10), 12, 12).getValue(this.widthBp)) .fontColor(Color.White) } - .width('100%') + .width(40) .margin({ bottom: new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 16 : 8), 16, 16).getValue(this.widthBp)}) } @@ -224,4 +224,52 @@ export struct ShotArea { .width('100%') .alignContent(Alignment.Center) } +} + +@Component +export struct ShotAreaHalfFolded { + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + @StorageLink('widthBp') widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM; + @StorageLink('photoUri') photoUri: string | Resource | PixelMap = ''; + @StorageLink('surfaceId') surfaceId: string = ''; + cameraUtil?: CameraUtil = CameraUtil.getInstance(); + context?: Context = this.getUIContext().getHostContext(); + + build() { + Column() { + Image($r('app.media.icon_flip')) + .width(44) + .height(44) + .onClick(() => { + let isFront: boolean | undefined = AppStorage.get('isFront'); + if (isFront) { + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + return; + } + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_FRONT); + }) + Image($r('app.media.icon_shoot')) + .width(76) + .height(76) + .onClick(() => { + this.cameraUtil?.capture(); + }) + Image(this.photoUri) + .width(44) + .height(44) + .borderWidth(this.photoUri === '' ? 0 : 1) + .borderColor(Color.White) + .borderRadius(22) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + if (this.photoUri !== '') { + this.cameraUtil?.previewPhoto(); + } + }) + } + .height(236) + .width(76) + .justifyContent(FlexAlign.SpaceBetween) + .margin({ right: 24 }) + } } \ No newline at end of file diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 index fb2d4f0..1946b38 100644 --- a/entry/src/main/module.json5 +++ b/entry/src/main/module.json5 @@ -22,7 +22,7 @@ "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", "exported": true, - "supportWindowMode": ["fullscreen", "floating"], + "supportWindowMode": ["fullscreen"], "skills": [ { "entities": [ -- Gitee