已开启
feat(screenrecorder): 录屏前增加音频模式选择弹窗与3-2-1倒计时 #10
feat(screenrecorder): 录屏前增加音频模式选择弹窗与3-2-1倒计时 #10
已开启
fbfb创建于 21 天前
共 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 
51export { ControlPhone } from './src/main/ets/components/ScreenRecorderControlPage/ControlPhone';54export { ControlPhone } from './src/main/ets/components/ScreenRecorderControlPage/ControlPhone';
52 55 
56+export { RecordOptionsDialog } from './src/main/ets/components/RecordOptionsDialog';
57+ 
53export { RecorderUtil } from './src/main/ets/util/RecorderUtil';58export { RecorderUtil } from './src/main/ets/util/RecorderUtil';
54 59 
55export { PhoneRecorderConfig } from './src/main/ets/manager/config/PhoneRecorderConfigImpl';60export { 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 
37export { MicManager } from './MicManager';37export { MicManager } from './MicManager';
38 38 
39+export { RecordOptionsManager, RecordAudioMode, RecordOptionsChoice } from './RecordOptionsManager';
40+ 
39export { AccessManager } from './AccessManager';41export { AccessManager } from './AccessManager';
40 42 
41export { RecorderFileManager } from './RecorderFileManager';43export { RecorderFileManager } from './RecorderFileManager';
@@ -40,6 +40,10 @@ export const PREVIEW_PAGE = 'pages/PreviewPage';
40 40 
41export const MASK_PAGE = 'pages/MaskPage';41export 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+ 
43export const SHARE_EMPTY_PAGE = 'pages/EmptyPage';47export const SHARE_EMPTY_PAGE = 'pages/EmptyPage';
44 48 
45export const SHARE_UI_ABILITY_NAME = 'ShareUIAbility';49export const SHARE_UI_ABILITY_NAME = 'ShareUIAbility';
@@ -50,6 +54,12 @@ export const PREVIEW_WINDOW_NAME = 'PreviewWindow';
50 54 
51export const MASK_WINDOW_NAME = 'MaskWindow';55export 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+ 
53export const TWO_SIDE = 2;63export const TWO_SIDE = 2;
54 64 
55export interface ResolutionModel {65export 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 WindowManager40 WindowManager
40} from '@ohos/screen-recorder';41} from '@ohos/screen-recorder';
41import { common } from '@kit.AbilityKit';42import { 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}