修复大厅启动页→H5"白闪+二次显示 splash":启动图覆盖层上移到根层

现象:大厅资源下载完成、splash 进度满后,进入 H5 前先闪一下白屏、又重新
显示一次 splash 背景图,才进 H5(真机 nova 14 复现)。

根因:原启动图视觉随 SplashPage 渲染,replacePathByName 切到
BridgeGameContainer 时,旧页销毁与新页 Web 首帧之间存在导航空隙(白闪);
而 BridgeGameContainer 自带的大厅 cover 又在切换后重新挂载,看起来像"第二次
splash"。两者皆因视觉绑定在会被切换/重建的页面上、与时序耦合。

修法(H5 零改动,纯原生):
- 启动图 + 金色进度条整体上移到 Index 根层 Stack 覆盖层,由 AppStorage
  KEY_SPLASH_VISIBLE 控制,从 app 启动持续显示到大厅首帧上屏(onPageEnd)才撤。
- SplashPage 退化为纯启动逻辑载体(透明 NavDestination),仅把进度/文案/
  阻断写入 AppStorage 供根层覆盖层呈现。
- BridgeGameContainer 移除自带大厅 cover,onPageEnd 置 splashVisible=false。

SplashPage→BridgeGameContainer 的页面切换由此发生在常驻覆盖层之下,时序无关,
彻底消除导航空隙白闪与二次 splash,大厅就绪即直接显示 H5。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-27 08:57:30 +08:00
co-authored by Claude Opus 4.8
parent 0425123465
commit db206068c0
3 changed files with 108 additions and 94 deletions
@@ -10,6 +10,7 @@ import { AppEnv, EventBus, EventPayload, NavEvents, OpenGenericWebPayload, Switc
import { BridgeGameParams, GenericWebParams, GenericWebResult, RouteName, AppEvents } from '../routes/AppRoutes';
import { WebSlot } from '../web/WebSlot';
import { SharePanel } from '../components/SharePanel';
import { KEY_SPLASH_VISIBLE } from './Index';
/**
* 大厅/子游戏容器(对应 webviewActivity,框架 §7.1/§7.5)。**双 Web 槽模型**
@@ -35,12 +36,12 @@ export struct BridgeGameContainer {
private cancels: Array<() => void> = [];
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
@State private subgameUrl: string = '';
/** 大厅首帧是否已绘制:未绘制时用启动图盖住大厅 Web,消除 splash→大厅的白屏闪烁。 */
@State private hallPainted: boolean = false;
/** 子游戏资源下载中(按需下载期间显示启动页同款进度条)。 */
@State private subgameDownloading: boolean = false;
/** 子游戏下载进度(0~100)。 */
@State private subgamePercent: number = 0;
/** 子游戏首帧是否已绘制:下载完到子游戏 H5 上屏间用启动图盖住,消除白屏。 */
@State private subgamePainted: boolean = false;
/** 远程配置 10 分钟缓存(对齐 Android:避免每次进子游戏都重拉 config)。 */
private cachedRemote: RemoteConfig | undefined = undefined;
private cachedRemoteAt: number = 0;
@@ -187,6 +188,7 @@ export struct BridgeGameContainer {
this.slotL?.deactivate();
this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer);
this.applyOrientation(d.webtype !== '2'); // "3" 横 / "2" 竖
this.subgamePainted = false; // 子游戏首帧前用启动图盖住,避免 下载完→子游戏上屏 间白屏
this.subgameUrl = `file://${subDir}/index.html?Launchtype=1`; // 触发子游戏 Web 渲染 → setupSubgame
}
@@ -317,9 +319,8 @@ export struct BridgeGameContainer {
this.slotL !== undefined ? this.slotL.onLoadIntercept(event.data.getRequestUrl()) : false)
.onPageEnd(() => {
this.slotL?.onPageEnd();
// 页面加载完成(已稳定上屏)再撤启动覆盖层——不在 onFirstContentfulPaint 撤,
// 避免首帧绘制到合成上屏间一帧黑屏。
this.hallPainted = true;
// 大厅页面加载完成(已稳定上屏)→ 撤根层启动覆盖,直接显示大厅 H5。
AppStorage.setOrCreate(KEY_SPLASH_VISIBLE, false);
})
.onProgressChange((event: OnProgressChangeEvent) => {
if (event.newProgress === 100) {
@@ -339,7 +340,10 @@ export struct BridgeGameContainer {
.onControllerAttached(() => this.setupSubgame())
.onLoadIntercept((event: OnLoadInterceptEvent) =>
this.slotS !== undefined ? this.slotS.onLoadIntercept(event.data.getRequestUrl()) : false)
.onPageEnd(() => this.slotS?.onPageEnd())
.onPageEnd(() => {
this.slotS?.onPageEnd();
this.subgamePainted = true; // 子游戏页面加载完、稳定上屏 → 撤启动图覆盖层
})
.onProgressChange((event: OnProgressChangeEvent) => {
if (event.newProgress === 100) {
this.slotS?.onFirstProgress();
@@ -348,29 +352,33 @@ export struct BridgeGameContainer {
.onRenderExited((event: OnRenderExitedEvent) => this.slotS?.onRenderExited(event.renderExitReason))
}
// 大厅首帧前启动图盖住(消除 splash→大厅白屏);仅大厅阶段、首帧后撤除
if (!this.hallPainted && this.subgameUrl === '') {
Image($r('app.media.launch_image'))
.width('100%').height('100%')
.objectFit(ImageFit.Cover)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
// 大厅首帧前启动图覆盖已上移到 Index.ets 根层,避免导航空隙白闪 + 二次 splash)
// 子游戏按需下载进度(启动页同款:金色进度条 + 文案,覆盖全屏白底)
if (this.subgameDownloading) {
Column({ space: 14 }) {
Progress({ value: this.subgamePercent, total: 100, type: ProgressType.Linear })
.width('56%')
.color('#D2A312')
.backgroundColor('#E6E6E6')
Row({ space: 8 }) {
LoadingProgress().width(20).height(20).color('#D2A312')
Text(`正在下载游戏… ${this.subgamePercent}%`).fontSize(15).fontColor('#1B2A4A')
// 子游戏进入覆盖层(与启动页 SplashPage 完全一致):覆盖「下载中」与「下载完→子游戏首帧上屏」
// 两段,启动图铺底 + 底部金色进度条 + 文案,直到子游戏 onPageEnd 才撤——消除两段间白屏。
if (this.subgameDownloading || (this.subgameUrl !== '' && !this.subgamePainted)) {
Stack() {
Image($r('app.media.launch_image'))
.width('100%').height('100%')
.objectFit(ImageFit.Cover)
Column({ space: 14 }) {
Progress({ value: this.subgameDownloading ? this.subgamePercent : 100, total: 100, type: ProgressType.Linear })
.width('56%')
.color('#D2A312')
.backgroundColor('#E6E6E6')
Row({ space: 8 }) {
LoadingProgress().width(20).height(20).color('#D2A312')
Text(this.subgameDownloading ? `正在下载游戏… ${this.subgamePercent}%` : '正在进入游戏…')
.fontSize(15).fontColor('#1B2A4A')
}
}
.width('100%').height('100%')
.justifyContent(FlexAlign.End)
.padding({ bottom: 24 })
}
.width('100%').height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
// 分享面板(顶层叠加;用户选平台/取消 → 一次性回投 ShareProvider