feat(录音): 容器接 Web触摸观察(方案A onTouch)+录音浮层+SHOW_RECORDING+计时/上滑取消/太短决策

- 补 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 录音浮层设计; 手势观察接线
This commit is contained in:
lanterngamescn
2026-06-27 17:27:00 +08:00
parent 2680590590
commit 4c02ef0dc4
@@ -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 首帧前/页面切换间隙显白色(与启动页白底无缝),避免黑屏闪烁