已开启
feat(screenrecorder): 录屏前增加音频模式选择弹窗与3-2-1倒计时 #10
fbfb创建于 21 天前
feat(screenrecorder): 录屏前增加音频模式选择弹窗与3-2-1倒计时 #10
已开启
共 10 个文件变更+574-1
| @@ -26,6 +26,9 @@ export { | |||
| 26 | AI_TRIGGER_TYPE, | 26 | AI_TRIGGER_TYPE, |
| 27 | SELECT_DISPLAY_ID, | 27 | SELECT_DISPLAY_ID, |
| 28 | MicManager, | 28 | MicManager, |
| 29 | + RecordOptionsManager, | ||
| 30 | + RecordAudioMode, | ||
| 31 | + RecordOptionsChoice, | ||
| 29 | AccessManager, | 32 | AccessManager, |
| 30 | RecorderFileManager, | 33 | RecorderFileManager, |
| 31 | RecorderToast, | 34 | RecorderToast, |
| @@ -50,6 +53,8 @@ export { MaskPreviewer } from './src/main/ets/components/MaskPreviewer'; | |||
| 50 | 53 | ||
| 51 | export { ControlPhone } from './src/main/ets/components/ScreenRecorderControlPage/ControlPhone'; | 54 | export { ControlPhone } from './src/main/ets/components/ScreenRecorderControlPage/ControlPhone'; |
| 52 | 55 | ||
| 56 | +export { RecordOptionsDialog } from './src/main/ets/components/RecordOptionsDialog'; | ||
| 57 | + | ||
| 53 | export { RecorderUtil } from './src/main/ets/util/RecorderUtil'; | 58 | export { RecorderUtil } from './src/main/ets/util/RecorderUtil'; |
| 54 | 59 | ||
| 55 | export { PhoneRecorderConfig } from './src/main/ets/manager/config/PhoneRecorderConfigImpl'; | 60 | export { PhoneRecorderConfig } from './src/main/ets/manager/config/PhoneRecorderConfigImpl'; |
| @@ -0,0 +1,244 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) Huawei Device Co., Ltd. 2026. All rights reserved. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +import { LogUtil } from '@ohos/common'; | ||
| 17 | +import { RecordAudioMode, RecordOptionsManager } from '../../manager/RecordOptionsManager'; | ||
| 18 | +import { COUNTDOWN_INTERVAL, COUNTDOWN_START_VALUE } from '../../manager/types'; | ||
| 19 | + | ||
| 20 | +const TAG = 'RecordOptionsDialog'; | ||
| 21 | + | ||
| 22 | +const DIALOG_WIDTH = 320; | ||
| 23 | +const DIALOG_RADIUS = 24; | ||
| 24 | +const DIALOG_PADDING = 24; | ||
| 25 | +const OPTION_ROW_HEIGHT = 48; | ||
| 26 | +const RADIO_SIZE = 20; | ||
| 27 | +const CHECKBOX_SIZE = 20; | ||
| 28 | +const BUTTON_HEIGHT = 40; | ||
| 29 | +const BUTTON_SPACE = 12; | ||
| 30 | +const COUNTDOWN_TEXT_SIZE = 96; | ||
| 31 | +const COUNTDOWN_ANIM_DURATION = 400; | ||
| 32 | +const COUNTDOWN_SCALE_START = 1.6; | ||
| 33 | +const COUNTDOWN_OPACITY_START = 0.2; | ||
| 34 | + | ||
| 35 | +/** | ||
| 36 | + * Dialog shown before screen recording starts: | ||
| 37 | + * two audio options, a show-touches checkbox, Cancel/Start buttons, | ||
| 38 | + * followed by a 3-2-1 countdown after Start is tapped. | ||
| 39 | + * | ||
| 40 | + * @since 2026-10-15 | ||
| 41 | + */ | ||
| 42 | +@Component | ||
| 43 | +export struct RecordOptionsDialog { | ||
| 44 | + @State audioMode: RecordAudioMode = RecordAudioMode.AUDIO_DEVICE; | ||
| 45 | + @State showTouches: boolean = true; | ||
| 46 | + @State isCountingDown: boolean = false; | ||
| 47 | + @State countDownNumber: number = COUNTDOWN_START_VALUE; | ||
| 48 | + @State countdownScale: number = 1; | ||
| 49 | + @State countdownOpacity: number = 1; | ||
| 50 | + private recordOptionsManager: RecordOptionsManager = RecordOptionsManager.getInstance(); | ||
| 51 | + private isFinishing: boolean = false; | ||
| 52 | + | ||
| 53 | + aboutToAppear(): void { | ||
| 54 | + this.audioMode = this.recordOptionsManager.getInitialAudioMode(); | ||
| 55 | + this.showTouches = this.recordOptionsManager.getInitialShowTouches(); | ||
| 56 | + LogUtil.info(TAG, `aboutToAppear audioMode: ${this.audioMode}, showTouches: ${this.showTouches}`); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + aboutToDisappear(): void { | ||
| 60 | + LogUtil.info(TAG, 'aboutToDisappear'); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + private startCountdown(): void { | ||
| 64 | + if (this.isFinishing) { | ||
| 65 | + return; | ||
| 66 | + } | ||
| 67 | + this.isFinishing = true; | ||
| 68 | + this.isCountingDown = true; | ||
| 69 | + this.showCountdownNumber(COUNTDOWN_START_VALUE); | ||
| 70 | + this.runCountdown(COUNTDOWN_START_VALUE); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + private runCountdown(current: number): void { | ||
| 74 | + setTimeout(() => { | ||
| 75 | + if (current > 1) { | ||
| 76 | + const next = current - 1; | ||
| 77 | + this.showCountdownNumber(next); | ||
| 78 | + this.runCountdown(next); | ||
| 79 | + } else { | ||
| 80 | + this.recordOptionsManager.notifyStart(this.audioMode, this.showTouches); | ||
| 81 | + } | ||
| 82 | + }, COUNTDOWN_INTERVAL); | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + private showCountdownNumber(number: number): void { | ||
| 86 | + this.countDownNumber = number; | ||
| 87 | + this.countdownScale = COUNTDOWN_SCALE_START; | ||
| 88 | + this.countdownOpacity = COUNTDOWN_OPACITY_START; | ||
| 89 | + animateTo({ duration: COUNTDOWN_ANIM_DURATION, curve: Curve.EaseOut }, () => { | ||
| 90 | + this.countdownScale = 1; | ||
| 91 | + this.countdownOpacity = 1; | ||
| 92 | + }); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + private cancelDialog(): void { | ||
| 96 | + if (this.isFinishing) { | ||
| 97 | + return; | ||
| 98 | + } | ||
| 99 | + this.isFinishing = true; | ||
| 100 | + this.recordOptionsManager.notifyCancel(); | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + @Builder | ||
| 104 | + audioOptionRow(label: ResourceStr, mode: RecordAudioMode) { | ||
| 105 | + Row() { | ||
| 106 | + Radio({ value: mode.toString(), group: 'recordAudioMode' }) | ||
| 107 | + .checked(this.audioMode === mode) | ||
| 108 | + .width(RADIO_SIZE) | ||
| 109 | + .height(RADIO_SIZE) | ||
| 110 | + .radioStyle({ checkedBackgroundColor: $r('sys.color.ohos_id_color_emphasize') }) | ||
| 111 | + .onChange((checked: boolean) => { | ||
| 112 | + if (checked) { | ||
| 113 | + this.audioMode = mode; | ||
| 114 | + } | ||
| 115 | + }) | ||
| 116 | + Text(label) | ||
| 117 | + .fontSize($r('sys.float.ohos_id_text_size_body1')) | ||
| 118 | + .fontColor($r('sys.color.font_primary')) | ||
| 119 | + .maxLines(2) | ||
| 120 | + .margin({ left: 16 }) | ||
| 121 | + } | ||
| 122 | + .width(DIALOG_WIDTH - 2 * DIALOG_PADDING) | ||
| 123 | + .height(OPTION_ROW_HEIGHT) | ||
| 124 | + .justifyContent(FlexAlign.Start) | ||
| 125 | + .onClick(() => { | ||
| 126 | + this.audioMode = mode; | ||
| 127 | + }) | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @Builder | ||
| 131 | + optionsContent() { | ||
| 132 | + Column() { | ||
| 133 | + Text($r('app.string.record_options_title')) | ||
| 134 | + .fontSize($r('sys.float.ohos_id_text_size_dialog_tittle')) | ||
| 135 | + .fontWeight(FontWeight.Bold) | ||
| 136 | + .fontColor($r('sys.color.font_primary')) | ||
| 137 | + .maxLines(1) | ||
| 138 | + .margin({ bottom: 8 }) | ||
| 139 | + | ||
| 140 | + this.audioOptionRow($r('app.string.record_audio_device'), RecordAudioMode.AUDIO_DEVICE) | ||
| 141 | + this.audioOptionRow($r('app.string.record_audio_device_and_mic'), RecordAudioMode.AUDIO_DEVICE_AND_MIC) | ||
| 142 | + | ||
| 143 | + Divider() | ||
| 144 | + .color($r('sys.color.comp_divider')) | ||
| 145 | + .margin({ top: 4, bottom: 4 }) | ||
| 146 | + | ||
| 147 | + Row() { | ||
| 148 | + Checkbox() | ||
| 149 | + .select(this.showTouches) | ||
| 150 | + .width(CHECKBOX_SIZE) | ||
| 151 | + .height(CHECKBOX_SIZE) | ||
| 152 | + .selectedColor($r('sys.color.ohos_id_color_emphasize')) | ||
| 153 | + .onChange((value: boolean) => { | ||
| 154 | + this.showTouches = value; | ||
| 155 | + }) | ||
| 156 | + Text($r('app.string.record_show_touches')) | ||
| 157 | + .fontSize($r('sys.float.ohos_id_text_size_body1')) | ||
| 158 | + .fontColor($r('sys.color.font_primary')) | ||
| 159 | + .maxLines(2) | ||
| 160 | + .margin({ left: 16 }) | ||
| 161 | + } | ||
| 162 | + .width(DIALOG_WIDTH - 2 * DIALOG_PADDING) | ||
| 163 | + .height(OPTION_ROW_HEIGHT) | ||
| 164 | + .justifyContent(FlexAlign.Start) | ||
| 165 | + .onClick(() => { | ||
| 166 | + this.showTouches = !this.showTouches; | ||
| 167 | + }) | ||
| 168 | + | ||
| 169 | + Row() { | ||
| 170 | + Button($r('app.string.record_options_cancel')) | ||
| 171 | + .buttonStyle(ButtonStyleMode.TEXTUAL) | ||
| 172 | + .fontSize($r('sys.float.ohos_id_text_size_button1')) | ||
| 173 | + .fontColor($r('sys.color.font_secondary')) | ||
| 174 | + .height(BUTTON_HEIGHT) | ||
| 175 | + .layoutWeight(1) | ||
| 176 | + .backgroundColor(Color.Transparent) | ||
| 177 | + .onClick(() => { | ||
| 178 | + this.cancelDialog(); | ||
| 179 | + }) | ||
| 180 | + Button($r('app.string.record_options_start')) | ||
| 181 | + .buttonStyle(ButtonStyleMode.EMPHASIZED) | ||
| 182 | + .type(ButtonType.Capsule) | ||
| 183 | + .fontSize($r('sys.float.ohos_id_text_size_button1')) | ||
| 184 | + .height(BUTTON_HEIGHT) | ||
| 185 | + .layoutWeight(1) | ||
| 186 | + .backgroundColor($r('sys.color.ohos_id_color_emphasize')) | ||
| 187 | + .margin({ left: BUTTON_SPACE }) | ||
| 188 | + .onClick(() => { | ||
| 189 | + this.startCountdown(); | ||
| 190 | + }) | ||
| 191 | + } | ||
| 192 | + .width(DIALOG_WIDTH - 2 * DIALOG_PADDING) | ||
| 193 | + .margin({ top: 16 }) | ||
| 194 | + } | ||
| 195 | + .width(DIALOG_WIDTH) | ||
| 196 | + .padding(DIALOG_PADDING) | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + @Builder | ||
| 200 | + countdownContent() { | ||
| 201 | + Column() { | ||
| 202 | + Text(this.countDownNumber.toString()) | ||
| 203 | + .fontSize(COUNTDOWN_TEXT_SIZE) | ||
| 204 | + .fontWeight(FontWeight.Bold) | ||
| 205 | + .fontColor(Color.White) | ||
| 206 | + .scale({ x: this.countdownScale, y: this.countdownScale }) | ||
| 207 | + .opacity(this.countdownOpacity) | ||
| 208 | + } | ||
| 209 | + .width('100%') | ||
| 210 | + .height('100%') | ||
| 211 | + .justifyContent(FlexAlign.Center) | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + build() { | ||
| 215 | + Stack() { | ||
| 216 | + if (this.isCountingDown) { | ||
| 217 | + Column() | ||
| 218 | + .width('100%') | ||
| 219 | + .height('100%') | ||
| 220 | + .backgroundColor('#66000000') | ||
| 221 | + this.countdownContent() | ||
| 222 | + } else { | ||
| 223 | + Column() | ||
| 224 | + .width('100%') | ||
| 225 | + .height('100%') | ||
| 226 | + .backgroundColor('#99000000') | ||
| 227 | + .onClick(() => { | ||
| 228 | + this.cancelDialog(); | ||
| 229 | + }) | ||
| 230 | + Column() { | ||
| 231 | + this.optionsContent() | ||
| 232 | + } | ||
| 233 | + .width(DIALOG_WIDTH) | ||
| 234 | + .borderRadius(DIALOG_RADIUS) | ||
| 235 | + .backgroundColor($r('sys.color.ohos_id_color_dialog_bg')) | ||
| 236 | + .onClick(() => { | ||
| 237 | + }) | ||
| 238 | + } | ||
| 239 | + } | ||
| 240 | + .width('100%') | ||
| 241 | + .height('100%') | ||
| 242 | + .alignContent(Alignment.Center) | ||
| 243 | + } | ||
| 244 | +} | ||
| @@ -0,0 +1,183 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) Huawei Device Co., Ltd. 2026. All rights reserved. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +import { GlobalThisUtil, LogUtil, PreferenceUtils } from '@ohos/common'; | ||
| 17 | +import { BusinessError } from '@ohos.base'; | ||
| 18 | +import common from '@ohos.app.ability.common'; | ||
| 19 | +import display from '@ohos.display'; | ||
| 20 | +import lazy windowManager from '@ohos.window'; | ||
| 21 | +import { COLOR_TRANSPARENT, RECORD_OPTIONS_PAGE, RECORD_OPTIONS_WINDOW_NAME } from './types'; | ||
| 22 | +import { CAPSULE_WINDOW_SET_FOCUS_ABLE_DELAY } from './types'; | ||
| 23 | + | ||
| 24 | +const TAG = 'RecordOptionsManager'; | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * Audio mode chosen by the user before starting screen recording, | ||
| 28 | + * aligned with common screen-record dialog options. | ||
| 29 | + */ | ||
| 30 | +export enum RecordAudioMode { | ||
| 31 | + /** No sound */ | ||
| 32 | + AUDIO_NONE = 0, | ||
| 33 | + /** Device (inner) audio only, without the microphone */ | ||
| 34 | + AUDIO_DEVICE = 1, | ||
| 35 | + /** Device (inner) audio plus the microphone */ | ||
| 36 | + AUDIO_DEVICE_AND_MIC = 2 | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +/** | ||
| 40 | + * Result of the pre-record options dialog. | ||
| 41 | + */ | ||
| 42 | +export interface RecordOptionsChoice { | ||
| 43 | + /** True when the user dismissed the dialog without starting recording */ | ||
| 44 | + canceled: boolean; | ||
| 45 | + /** Selected audio mode, valid when canceled is false */ | ||
| 46 | + audioMode: RecordAudioMode; | ||
| 47 | + /** Whether touches are shown as white dots during recording */ | ||
| 48 | + showTouches: boolean; | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +/** | ||
| 52 | + * Manages the pre-record options dialog: | ||
| 53 | + * creates a full-screen system window hosting RecordOptionsPage, | ||
| 54 | + * waits for the user's choice (audio mode + show touches), | ||
| 55 | + * persists the choice and resolves the caller before the recorder starts. | ||
| 56 | + */ | ||
| 57 | +export class RecordOptionsManager { | ||
| 58 | + private static instance: RecordOptionsManager; | ||
| 59 | + private globalThisInstance: GlobalThisUtil = GlobalThisUtil.getInstance(); | ||
| 60 | + private dataPreferences: PreferenceUtils = new PreferenceUtils(); | ||
| 61 | + private optionsWindow?: windowManager.Window; | ||
| 62 | + private pendingResolve: ((choice: RecordOptionsChoice) => void) | null = null; | ||
| 63 | + | ||
| 64 | + public static getInstance(): RecordOptionsManager { | ||
| 65 | + if (!RecordOptionsManager.instance) { | ||
| 66 | + RecordOptionsManager.instance = new RecordOptionsManager(); | ||
| 67 | + } | ||
| 68 | + return RecordOptionsManager.instance; | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * Show the options dialog and wait until the user taps Start (after the | ||
| 73 | + * 3-2-1 countdown) or Cancel. | ||
| 74 | + * | ||
| 75 | + * @returns the user's choice, or null when the dialog window cannot be created | ||
| 76 | + * or another dialog is already waiting. | ||
| 77 | + */ | ||
| 78 | + public async showAndWait(): Promise<RecordOptionsChoice | null> { | ||
| 79 | + if (this.pendingResolve) { | ||
| 80 | + LogUtil.warn(TAG, 'options dialog is already waiting, ignore this request'); | ||
| 81 | + return null; | ||
| 82 | + } | ||
| 83 | + try { | ||
| 84 | + await this.createOptionsWindow(); | ||
| 85 | + } catch (error) { | ||
| 86 | + LogUtil.error(TAG, | ||
| 87 | + `create options window failed, code: ${(error as BusinessError)?.code}, message: ${(error as BusinessError)?.message}`); | ||
| 88 | + await this.destroyOptionsWindow(); | ||
| 89 | + return null; | ||
| 90 | + } | ||
| 91 | + return new Promise<RecordOptionsChoice>((resolve) => { | ||
| 92 | + this.pendingResolve = resolve; | ||
| 93 | + }); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + /** | ||
| 97 | + * Initial audio mode for the dialog: microphone state comes from the last | ||
| 98 | + * recording setting; with the mic off the default is device audio. | ||
| 99 | + */ | ||
| 100 | + public getInitialAudioMode(): RecordAudioMode { | ||
| 101 | + const context = this.globalThisInstance!.getObject('screenRecorderContext') as common.ServiceExtensionContext; | ||
| 102 | + const isMicEnabled = | ||
| 103 | + this.dataPreferences.getPreferencesValueByKeySync(context, 'isMicEnabled', false) as boolean; | ||
| 104 | + return isMicEnabled ? RecordAudioMode.AUDIO_DEVICE_AND_MIC : RecordAudioMode.AUDIO_DEVICE; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + /** | ||
| 108 | + * Initial show-touches state for the dialog, shared with the control center page. | ||
| 109 | + */ | ||
| 110 | + public getInitialShowTouches(): boolean { | ||
| 111 | + const context = this.globalThisInstance!.getObject('screenRecorderContext') as common.ServiceExtensionContext; | ||
| 112 | + return this.dataPreferences.getPreferencesValueByKeySync(context, 'showTouchState', 'true') === 'true'; | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + /** | ||
| 116 | + * Called by the dialog page after the countdown finishes and Start was confirmed. | ||
| 117 | + * Persists the choice so that the recorder picks it up when initializing. | ||
| 118 | + */ | ||
| 119 | + public async notifyStart(audioMode: RecordAudioMode, showTouches: boolean): Promise<void> { | ||
| 120 | + LogUtil.info(TAG, `notifyStart audioMode: ${audioMode}, showTouches: ${showTouches}`); | ||
| 121 | + const context = this.globalThisInstance!.getObject('screenRecorderContext') as common.ServiceExtensionContext; | ||
| 122 | + this.dataPreferences.setPreferenceKeyValueSync('isMicEnabled', audioMode === RecordAudioMode.AUDIO_DEVICE_AND_MIC, | ||
| 123 | + context); | ||
| 124 | + this.dataPreferences.setPreferenceKeyValueSync('showTouchState', showTouches ? 'true' : 'false', context); | ||
| 125 | + await this.finish({ canceled: false, audioMode: audioMode, showTouches: showTouches }); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + /** | ||
| 129 | + * Called by the dialog page when the user cancels. | ||
| 130 | + */ | ||
| 131 | + public async notifyCancel(): Promise<void> { | ||
| 132 | + LogUtil.info(TAG, 'notifyCancel'); | ||
| 133 | + await this.finish({ canceled: true, audioMode: RecordAudioMode.AUDIO_NONE, showTouches: false }); | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + private async finish(choice: RecordOptionsChoice): Promise<void> { | ||
| 137 | + await this.destroyOptionsWindow(); | ||
| 138 | + const resolve = this.pendingResolve; | ||
| 139 | + this.pendingResolve = null; | ||
| 140 | + resolve?.(choice); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + private async createOptionsWindow(): Promise<void> { | ||
| 144 | + const context = this.globalThisInstance!.getObject('screenRecorderContext') as common.ServiceExtensionContext; | ||
| 145 | + const dftDisplay = display.getDefaultDisplaySync(); | ||
| 146 | + const windowConfig: windowManager.Configuration = { | ||
| 147 | + name: RECORD_OPTIONS_WINDOW_NAME, | ||
| 148 | + windowType: windowManager.WindowType.TYPE_SCREENSHOT, | ||
| 149 | + ctx: context, | ||
| 150 | + displayId: dftDisplay.id | ||
| 151 | + }; | ||
| 152 | + this.optionsWindow = await windowManager.createWindow(windowConfig); | ||
| 153 | + await this.optionsWindow.resize(dftDisplay.width, dftDisplay.height); | ||
| 154 | + await this.optionsWindow.moveWindowTo(0, 0); | ||
| 155 | + await this.optionsWindow.setUIContent(RECORD_OPTIONS_PAGE); | ||
| 156 | + this.optionsWindow.setWindowBackgroundColor(COLOR_TRANSPARENT); | ||
| 157 | + // 与胶囊窗一致:显示前不可获焦,避免其他界面失焦;显示后延时设为可获焦,否则窗口收不到点击 | ||
| 158 | + await this.optionsWindow.setWindowFocusable(false); | ||
| 159 | + await this.optionsWindow.showWindow(); | ||
| 160 | + setTimeout(() => { | ||
| 161 | + this.optionsWindow?.setWindowFocusable(true).catch((error: BusinessError) => { | ||
| 162 | + LogUtil.error(TAG, `setWindowFocusable failed, message: ${error?.message}`); | ||
| 163 | + }); | ||
| 164 | + }, CAPSULE_WINDOW_SET_FOCUS_ABLE_DELAY); | ||
| 165 | + LogUtil.info(TAG, 'options window shown'); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + private async destroyOptionsWindow(): Promise<void> { | ||
| 169 | + try { | ||
| 170 | + let optionsWindow = this.optionsWindow ?? windowManager.findWindow(RECORD_OPTIONS_WINDOW_NAME); | ||
| 171 | + this.optionsWindow = undefined; | ||
| 172 | + if (!optionsWindow) { | ||
| 173 | + return; | ||
| 174 | + } | ||
| 175 | + await optionsWindow.hide(); | ||
| 176 | + await optionsWindow.destroyWindow(); | ||
| 177 | + LogUtil.info(TAG, 'options window destroyed'); | ||
| 178 | + } catch (error) { | ||
| 179 | + LogUtil.error(TAG, | ||
| 180 | + `destroy options window failed, code: ${(error as BusinessError)?.code}, message: ${(error as BusinessError)?.message}`); | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | +} | ||
| @@ -36,6 +36,8 @@ export { NotificationManager } from './NotificationManager'; | |||
| 36 | 36 | ||
| 37 | export { MicManager } from './MicManager'; | 37 | export { MicManager } from './MicManager'; |
| 38 | 38 | ||
| 39 | +export { RecordOptionsManager, RecordAudioMode, RecordOptionsChoice } from './RecordOptionsManager'; | ||
| 40 | + | ||
| 39 | export { AccessManager } from './AccessManager'; | 41 | export { AccessManager } from './AccessManager'; |
| 40 | 42 | ||
| 41 | export { RecorderFileManager } from './RecorderFileManager'; | 43 | export { RecorderFileManager } from './RecorderFileManager'; |
| @@ -40,6 +40,10 @@ export const PREVIEW_PAGE = 'pages/PreviewPage'; | |||
| 40 | 40 | ||
| 41 | export const MASK_PAGE = 'pages/MaskPage'; | 41 | export const MASK_PAGE = 'pages/MaskPage'; |
| 42 | 42 | ||
| 43 | +export const RECORD_OPTIONS_PAGE = 'pages/RecordOptionsPage'; | ||
| 44 | + | ||
| 45 | +export const RECORD_OPTIONS_WINDOW_NAME = 'RecordOptionsWindow'; | ||
| 46 | + | ||
| 43 | export const SHARE_EMPTY_PAGE = 'pages/EmptyPage'; | 47 | export const SHARE_EMPTY_PAGE = 'pages/EmptyPage'; |
| 44 | 48 | ||
| 45 | export const SHARE_UI_ABILITY_NAME = 'ShareUIAbility'; | 49 | export const SHARE_UI_ABILITY_NAME = 'ShareUIAbility'; |
| @@ -50,6 +54,12 @@ export const PREVIEW_WINDOW_NAME = 'PreviewWindow'; | |||
| 50 | 54 | ||
| 51 | export const MASK_WINDOW_NAME = 'MaskWindow'; | 55 | export const MASK_WINDOW_NAME = 'MaskWindow'; |
| 52 | 56 | ||
| 57 | +// Duration of one step of the 3-2-1 countdown before recording starts | ||
| 58 | +export const COUNTDOWN_INTERVAL = 1000; | ||
| 59 | + | ||
| 60 | +// Start value of the 3-2-1 countdown before recording starts | ||
| 61 | +export const COUNTDOWN_START_VALUE = 3; | ||
| 62 | + | ||
| 53 | export const TWO_SIDE = 2; | 63 | export const TWO_SIDE = 2; |
| 54 | 64 | ||
| 55 | export interface ResolutionModel { | 65 | export interface ResolutionModel { |
| @@ -469,6 +469,48 @@ | |||
| 469 | "priority":"translate" | 469 | "priority":"translate" |
| 470 | } | 470 | } |
| 471 | }, | 471 | }, |
| 472 | + { | ||
| 473 | + "name":"record_options_title", | ||
| 474 | + "value":"Start recording?", | ||
| 475 | + "attr":{ | ||
| 476 | + "priority":"translate" | ||
| 477 | + } | ||
| 478 | + }, | ||
| 479 | + { | ||
| 480 | + "name":"record_audio_device", | ||
| 481 | + "value":"Device audio", | ||
| 482 | + "attr":{ | ||
| 483 | + "priority":"translate" | ||
| 484 | + } | ||
| 485 | + }, | ||
| 486 | + { | ||
| 487 | + "name":"record_audio_device_and_mic", | ||
| 488 | + "value":"Device audio and microphone", | ||
| 489 | + "attr":{ | ||
| 490 | + "priority":"translate" | ||
| 491 | + } | ||
| 492 | + }, | ||
| 493 | + { | ||
| 494 | + "name":"record_show_touches", | ||
| 495 | + "value":"Show touches on screen", | ||
| 496 | + "attr":{ | ||
| 497 | + "priority":"translate" | ||
| 498 | + } | ||
| 499 | + }, | ||
| 500 | + { | ||
| 501 | + "name":"record_options_cancel", | ||
| 502 | + "value":"CANCEL", | ||
| 503 | + "attr":{ | ||
| 504 | + "priority":"translate" | ||
| 505 | + } | ||
| 506 | + }, | ||
| 507 | + { | ||
| 508 | + "name":"record_options_start", | ||
| 509 | + "value":"Start", | ||
| 510 | + "attr":{ | ||
| 511 | + "priority":"translate" | ||
| 512 | + } | ||
| 513 | + }, | ||
| 472 | { | 514 | { |
| 473 | "name":"share_audio", | 515 | "name":"share_audio", |
| 474 | "value":"Share audio", | 516 | "value":"Share audio", |
| @@ -469,6 +469,48 @@ | |||
| 469 | "priority":"translate" | 469 | "priority":"translate" |
| 470 | } | 470 | } |
| 471 | }, | 471 | }, |
| 472 | + { | ||
| 473 | + "name":"record_options_title", | ||
| 474 | + "value":"开始录屏?", | ||
| 475 | + "attr":{ | ||
| 476 | + "priority":"translate" | ||
| 477 | + } | ||
| 478 | + }, | ||
| 479 | + { | ||
| 480 | + "name":"record_audio_device", | ||
| 481 | + "value":"媒体声音(仅系统内音)", | ||
| 482 | + "attr":{ | ||
| 483 | + "priority":"translate" | ||
| 484 | + } | ||
| 485 | + }, | ||
| 486 | + { | ||
| 487 | + "name":"record_audio_device_and_mic", | ||
| 488 | + "value":"媒体声音和麦克风", | ||
| 489 | + "attr":{ | ||
| 490 | + "priority":"translate" | ||
| 491 | + } | ||
| 492 | + }, | ||
| 493 | + { | ||
| 494 | + "name":"record_show_touches", | ||
| 495 | + "value":"录制时显示点击白点", | ||
| 496 | + "attr":{ | ||
| 497 | + "priority":"translate" | ||
| 498 | + } | ||
| 499 | + }, | ||
| 500 | + { | ||
| 501 | + "name":"record_options_cancel", | ||
| 502 | + "value":"取消", | ||
| 503 | + "attr":{ | ||
| 504 | + "priority":"translate" | ||
| 505 | + } | ||
| 506 | + }, | ||
| 507 | + { | ||
| 508 | + "name":"record_options_start", | ||
| 509 | + "value":"开始录制", | ||
| 510 | + "attr":{ | ||
| 511 | + "priority":"translate" | ||
| 512 | + } | ||
| 513 | + }, | ||
| 472 | { | 514 | { |
| 473 | "name":"share_audio", | 515 | "name":"share_audio", |
| 474 | "value":"共享音频", | 516 | "value":"共享音频", |
| @@ -0,0 +1,34 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) Huawei Device Co., Ltd. 2026. All rights reserved. | ||
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 4 | + * you may not use this file except in compliance with the License. | ||
| 5 | + * You may obtain a copy of the License at | ||
| 6 | + * | ||
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 8 | + * | ||
| 9 | + * Unless required by applicable law or agreed to in writing, software | ||
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 12 | + * See the License for the specific language governing permissions and | ||
| 13 | + * limitations under the License. | ||
| 14 | + */ | ||
| 15 | + | ||
| 16 | +import { RecordOptionsDialog } from '@ohos/screen-recorder'; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * Pre-record options dialog window entrance: audio mode, show touches, | ||
| 20 | + * Cancel/Start buttons and the 3-2-1 countdown. | ||
| 21 | + * | ||
| 22 | + * @since 2026-10-15 | ||
| 23 | + */ | ||
| 24 | +@Entry | ||
| 25 | +@Component | ||
| 26 | +struct RecordOptionsPage { | ||
| 27 | + build() { | ||
| 28 | + Column() { | ||
| 29 | + RecordOptionsDialog() | ||
| 30 | + } | ||
| 31 | + .width('100%') | ||
| 32 | + .height('100%') | ||
| 33 | + } | ||
| 34 | +} | ||
| @@ -36,6 +36,7 @@ import { | |||
| 36 | RecorderToast, | 36 | RecorderToast, |
| 37 | RecorderUtil, | 37 | RecorderUtil, |
| 38 | RecordManager, | 38 | RecordManager, |
| 39 | + RecordOptionsManager, | ||
| 39 | WindowManager | 40 | WindowManager |
| 40 | } from '@ohos/screen-recorder'; | 41 | } from '@ohos/screen-recorder'; |
| 41 | import { common } from '@kit.AbilityKit'; | 42 | import { common } from '@kit.AbilityKit'; |
| @@ -230,6 +231,15 @@ export default class ExtAbilityProxy { | |||
| 230 | this.initDisplayWidth = initDisplay.width; | 231 | this.initDisplayWidth = initDisplay.width; |
| 231 | this.initDisplayHeight = initDisplay.height; | 232 | this.initDisplayHeight = initDisplay.height; |
| 232 | } | 233 | } |
| 234 | + // 未在录屏时,先弹窗让用户选择声音模式与显示触摸,倒计时结束后再启动录屏 | ||
| 235 | + if (!this.recordManager!.getIsRecording()) { | ||
| 236 | + const choice = await RecordOptionsManager.getInstance().showAndWait(); | ||
| 237 | + if (choice === null || choice.canceled) { | ||
| 238 | + LogUtil.info(TAG, 'record options dialog canceled, do not start recording'); | ||
| 239 | + RecorderUtil.postKillProcessMessage(); | ||
| 240 | + return; | ||
| 241 | + } | ||
| 242 | + } | ||
| 233 | await this.recordManager!.trigger(triggerType.toString(), selectId, isBCRecord); | 243 | await this.recordManager!.trigger(triggerType.toString(), selectId, isBCRecord); |
| 234 | } catch (error) { | 244 | } catch (error) { |
| 235 | LogUtil.error(TAG, `triggerRecorder request failed because, code:${error?.code} message:${error?.message}`); | 245 | LogUtil.error(TAG, `triggerRecorder request failed because, code:${error?.code} message:${error?.message}`); |
| @@ -5,6 +5,7 @@ | |||
| 5 | "pages/ScreenRecorderControlPage", | 5 | "pages/ScreenRecorderControlPage", |
| 6 | "pages/EmptyPage", | 6 | "pages/EmptyPage", |
| 7 | "pages/ScreenRecorderPage", | 7 | "pages/ScreenRecorderPage", |
| 8 | - "pages/PrivacyControlPage" | 8 | + "pages/PrivacyControlPage", |
| 9 | + "pages/RecordOptionsPage" | ||
| 9 | ] | 10 | ] |
| 10 | } | 11 | } |