From 4c02ef0dc4c6df93a938f7aae6f68c19a012726c Mon Sep 17 00:00:00 2001 From: lanterngamescn Date: Sat, 27 Jun 2026 17:27:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BD=95=E9=9F=B3):=20=E5=AE=B9=E5=99=A8?= =?UTF-8?q?=E6=8E=A5=20Web=E8=A7=A6=E6=91=B8=E8=A7=82=E5=AF=9F(=E6=96=B9?= =?UTF-8?q?=E6=A1=88A=20onTouch)+=E5=BD=95=E9=9F=B3=E6=B5=AE=E5=B1=82+SHOW?= =?UTF-8?q?=5FRECORDING+=E8=AE=A1=E6=97=B6/=E4=B8=8A=E6=BB=91=E5=8F=96?= =?UTF-8?q?=E6=B6=88/=E5=A4=AA=E7=9F=AD=E5=86=B3=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补 import: AudioEvents, ShowRecordingRequest, RecordResult (from common) + RecordingOverlay - 新增字段: recordOverlayVisible/recordState/recordResultEvent/touchDown/touchStartY/ recordingActive/wantCancel/recordSeconds/recordTimer - subscribeEvents() 追加 SHOW_RECORDING → onShowRecording - onShowRecording: 手指未按时立即 cancel 回投; 否则显浮层+启计时器 - onWebTouch(TouchEvent): Down记起点/Move判上滑(>100vp)/Up调 finishRecording - finishRecording: <0.8s→tooShort延1.3s撤层; 上滑→cancel; 否则→send; 一次性清 resultEvent - 大厅/子游戏 Web 各加 .onTouch((e)=>this.onWebTouch(e)) (方案A, 不消费触摸) - Stack 顶层(ShareGuideDialog 之后)追加 RecordingOverlay({ state }) - aboutToDisappear 清 recordTimer 对应 §10 录音浮层设计; 手势观察接线 --- .../main/ets/pages/BridgeGameContainer.ets | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/entry/src/main/ets/pages/BridgeGameContainer.ets b/entry/src/main/ets/pages/BridgeGameContainer.ets index 1ea691a..8da3632 100644 --- a/entry/src/main/ets/pages/BridgeGameContainer.ets +++ b/entry/src/main/ets/pages/BridgeGameContainer.ets @@ -6,11 +6,13 @@ import { ConfigManager, ResourceManager, AppDataInjector, AppDataValues, RemoteC import { KvStore, LocalUploadServer } from 'platform'; import { OutboundHandlers } from 'contracts'; import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, SwitchGamePayload, - BackGamePayload, ShareEvents, SharePanelRequest, SharePanelResult, ShareGuideRequest, ShareGuideResult, Logger } from 'common'; + BackGamePayload, ShareEvents, SharePanelRequest, SharePanelResult, ShareGuideRequest, ShareGuideResult, Logger, + AudioEvents, ShowRecordingRequest, RecordResult } from 'common'; import { BridgeGameParams, GenericWebParams, GenericWebResult, RouteName, AppEvents } from '../routes/AppRoutes'; import { WebSlot } from '../web/WebSlot'; import { SharePanel } from '../components/SharePanel'; import { ShareGuideDialog } from '../components/ShareGuideDialog'; +import { RecordingOverlay } from '../components/RecordingOverlay'; import { KEY_SPLASH_VISIBLE } from './Index'; /** @@ -59,6 +61,19 @@ export struct BridgeGameContainer { private shareGuideTimeline: boolean = false; /** 当前指引窗的一次性回投事件名(用户选动作后 emit 回 ShareProvider)。 */ private shareGuideResultEvent: string = ''; + /** 录音浮层显隐与状态('recording'|'cancel'|'tooShort')。 */ + @State private recordOverlayVisible: boolean = false; + @State private recordState: string = 'recording'; + /** 当前录音的一次性回投事件名。 */ + private recordResultEvent: string = ''; + /** 触摸跟踪(始终更新,供录音激活时读取)。 */ + private touchDown: boolean = false; + private touchStartY: number = 0; + /** 录音激活中(收到 SHOW_RECORDING 且手指在按)。 */ + private recordingActive: boolean = false; + private wantCancel: boolean = false; + private recordSeconds: number = 0; + private recordTimer: number = -1; aboutToAppear(): void { if (AppEnv.isDebug()) { @@ -72,6 +87,10 @@ export struct BridgeGameContainer { cancel(); } this.cancels = []; + if (this.recordTimer >= 0) { + clearInterval(this.recordTimer); + this.recordTimer = -1; + } this.slotS?.dispose(); this.slotL?.dispose(); this.uploadServer.stop(); @@ -128,6 +147,83 @@ export struct BridgeGameContainer { this.cancels.push(EventBus.on(NavEvents.BACK_GAME, (p?: EventPayload) => this.backToLobby(p))); this.cancels.push(EventBus.on(ShareEvents.SHOW_PANEL, (p?: EventPayload) => this.showSharePanel(p))); this.cancels.push(EventBus.on(ShareEvents.SHOW_GUIDE, (p?: EventPayload) => this.showShareGuide(p))); + this.cancels.push(EventBus.on(AudioEvents.SHOW_RECORDING, (p?: EventPayload) => this.onShowRecording(p))); + } + + /** 录音器已起:手指仍在按则显示浮层+计时;已松开(异步起录音期间抬手)则立即按取消回投。 */ + private onShowRecording(p?: EventPayload): void { + if (p === undefined) { + return; + } + const req = p as ShowRecordingRequest; + this.recordResultEvent = req.resultEvent; + if (!this.touchDown) { + this.emitRecordResult('cancel', 0); + return; + } + this.recordingActive = true; + this.wantCancel = false; + this.recordSeconds = 0; + this.recordState = 'recording'; + this.recordOverlayVisible = true; + this.recordTimer = setInterval(() => { + this.recordSeconds += 0.1; + }, 100); + } + + /** Web 触摸观察(不消费):Down 记起点 / Move 判上滑取消 / Up 结束决策。 */ + private onWebTouch(e: TouchEvent): void { + if (e.type === TouchType.Down) { + this.touchDown = true; + this.touchStartY = e.touches.length > 0 ? e.touches[0].y : 0; + if (this.recordingActive) { + this.wantCancel = false; + this.recordState = 'recording'; + } + } else if (e.type === TouchType.Move) { + if (this.recordingActive && e.touches.length > 0) { + const dy: number = this.touchStartY - e.touches[0].y; + this.wantCancel = dy > 100; + this.recordState = this.wantCancel ? 'cancel' : 'recording'; + } + } else if (e.type === TouchType.Up || e.type === TouchType.Cancel) { + this.touchDown = false; + if (this.recordingActive) { + this.finishRecording(); + } + } + } + + /** 抬手:按时长/上滑决定 send/cancel/tooShort,撤浮层并回投。 */ + private finishRecording(): void { + this.recordingActive = false; + if (this.recordTimer >= 0) { + clearInterval(this.recordTimer); + this.recordTimer = -1; + } + const elapsed: number = this.recordSeconds; + if (elapsed < 0.8) { + this.recordState = 'tooShort'; + setTimeout(() => { + this.recordOverlayVisible = false; + }, 1300); + this.emitRecordResult('tooShort', elapsed); + } else if (this.wantCancel) { + this.recordOverlayVisible = false; + this.emitRecordResult('cancel', elapsed); + } else { + this.recordOverlayVisible = false; + this.emitRecordResult('send', elapsed); + } + } + + private emitRecordResult(action: string, timeSec: number): void { + const ev: string = this.recordResultEvent; + this.recordResultEvent = ''; + if (ev !== '') { + const r: RecordResult = { action: action, timeSec: timeSec }; + EventBus.emit(ev, r); + } } /** 收到 ShareProvider 的 SHOW_PANEL:记下一次性回投事件名并弹出面板。 */ @@ -355,6 +451,8 @@ export struct BridgeGameContainer { // 铺满系统安全区(消除底部导航手势区白边),与原 Android 全屏 WebView 一致 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) .visibility(this.subgameUrl !== '' ? Visibility.Hidden : Visibility.Visible) + // 录音手势观察(方案A,不消费触摸) + .onTouch((e: TouchEvent) => this.onWebTouch(e)) .onControllerAttached(() => this.setupLobby()) .onLoadIntercept((event: OnLoadInterceptEvent) => this.slotL !== undefined ? this.slotL.onLoadIntercept(event.data.getRequestUrl()) : false) @@ -379,6 +477,8 @@ export struct BridgeGameContainer { .geolocationAccess(true).zoomAccess(false) .width('100%').height('100%') .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + // 录音手势观察(方案A,不消费触摸) + .onTouch((e: TouchEvent) => this.onWebTouch(e)) .onControllerAttached(() => this.setupSubgame()) .onLoadIntercept((event: OnLoadInterceptEvent) => this.slotS !== undefined ? this.slotS.onLoadIntercept(event.data.getRequestUrl()) : false) @@ -435,6 +535,10 @@ export struct BridgeGameContainer { onPick: (action: string) => this.emitShareGuideResult(action), }) } + // 录音浮层(顶层叠加,触摸穿透到下方 Web 由 onTouch 观察) + if (this.recordOverlayVisible) { + RecordingOverlay({ state: this.recordState }) + } } .width('100%').height('100%') // 垫白底:Web 首帧前/页面切换间隙显白色(与启动页白底无缝),避免黑屏闪烁