修复大厅启动页→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
+15 -65
View File
@@ -1,27 +1,18 @@
import { common } from '@kit.AbilityKit';
import { StartupOrchestrator, StartupResult, StartupStage } from 'domain_resource';
import { RouteName, BridgeGameParams } from '../routes/AppRoutes';
import { KEY_SPLASH_VISIBLE, KEY_SPLASH_PERCENT, KEY_SPLASH_LABEL, KEY_SPLASH_BLOCKED } from './Index';
/**
* 启动引导页(对应 weclomeactivity1,框架 §4/§8.1)。
* 进入即跑 StartupOrchestrator(本地配置→远程配置→资源准备→app_data 注入),
* 完成后 replace 进大厅容器;被 showmessage 阻断时弹公告
* 启动引导页(对应 weclomeactivity1,框架 §4/§8.1——**纯启动逻辑载体,无可见 UI**
* 进入即跑 StartupOrchestrator(本地配置→远程配置→资源准备→app_data 注入),进度/文案/阻断写入
* AppStorage,由 Index 根层启动页覆盖层统一呈现;完成后 replace 进大厅容器(覆盖层在大厅首帧后撤)
*
* 启动时序:系统启动窗用透明 startWindowIcon($media:start_window_blank) + 白底 →
* **纯白屏**;随后本页淡入全屏启动图 launch_image(白底 + 金色 logo)。系统窗无 logo
* 故不存在"两套资源比例不一致"问题。覆盖层文字用品牌 navy(白底可见),进度条金色,
* 展示当前阶段中文文案。
* 视觉放在 Index 根层而非本页:使 splash→大厅的页面切换发生在覆盖层之下,彻底消除导航空隙白闪。
*/
@Component
export struct SplashPage {
pathStack: NavPathStack = new NavPathStack();
@State private stage: StartupStage = StartupStage.INIT;
@State private percent: number = 0;
@State private blocked: string = '';
// 品牌色(取自 logo):navy 文字 + 金色进度
private static readonly NAVY: string = '#1B2A4A';
private static readonly GOLD: string = '#D2A312';
aboutToAppear(): void {
this.runStartup();
@@ -30,26 +21,27 @@ export struct SplashPage {
private async runStartup(): Promise<void> {
const ctx: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
const orchestrator = new StartupOrchestrator(ctx, (stage: StartupStage, percent: number) => {
this.stage = stage;
this.percent = percent;
AppStorage.setOrCreate(KEY_SPLASH_PERCENT, percent);
AppStorage.setOrCreate(KEY_SPLASH_LABEL, SplashPage.stageLabel(stage, percent));
});
try {
const result: StartupResult = await orchestrator.run();
if (result.enter) {
// 切到大厅:保持 splashVisible=true,根层覆盖层持续盖住,由大厅首帧 onPageEnd 撤除。
const params: BridgeGameParams = { entryUrl: result.entryUrl, launchType: '0' };
this.pathStack.replacePathByName(RouteName.BRIDGE_GAME, params, false);
} else {
this.blocked = result.message;
AppStorage.setOrCreate(KEY_SPLASH_BLOCKED, result.message);
}
} catch (e) {
const err = e as Error;
this.blocked = `启动失败:${err.message}`;
AppStorage.setOrCreate(KEY_SPLASH_BLOCKED, `启动失败:${err.message}`);
}
}
/** 阶段 → 用户可读中文文案(PREPARE_RESOURCE 按进度区分"准备资源/下载更新")。 */
private stageLabel(): string {
switch (this.stage) {
private static stageLabel(stage: StartupStage, percent: number): string {
switch (stage) {
case StartupStage.INIT:
return '正在启动…';
case StartupStage.LOAD_LOCAL_CONFIG:
@@ -62,7 +54,7 @@ export struct SplashPage {
return '检测升级版本…';
case StartupStage.PREPARE_RESOURCE:
// 35% 为内置资源准备;55%~85% 为远程 zip 下载
return this.percent >= 55 ? '正在下载更新…' : '准备游戏资源…';
return percent >= 55 ? '正在下载更新…' : '准备游戏资源…';
case StartupStage.INJECT_APP_DATA:
return '即将进入大厅…';
case StartupStage.ENTER_HALL:
@@ -73,52 +65,10 @@ export struct SplashPage {
}
build() {
// 视觉由 Index 根层启动页覆盖层呈现;本页仅承载启动逻辑(透明、无内容)
NavDestination() {
Stack() {
// 全屏启动图(白底 + 金色 logo)
Image($r('app.media.launch_image'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
// 启动进度/公告叠加在底部(深色文字,白底可见)
Column({ space: 14 }) {
if (this.blocked === '') {
Progress({ value: this.percent, total: 100, type: ProgressType.Linear })
.width('56%')
.color(SplashPage.GOLD)
.backgroundColor('#E6E6E6')
// 阶段提示文字放在进度条下方
Row({ space: 8 }) {
LoadingProgress()
.width(20)
.height(20)
.color(SplashPage.GOLD)
Text(`${this.stageLabel()} ${this.percent}%`)
.fontSize(15)
.fontColor(SplashPage.NAVY)
}
} else {
Text(this.blocked)
.fontSize(16)
.fontColor(SplashPage.NAVY)
.textAlign(TextAlign.Center)
.backgroundColor('rgba(255,255,255,0.9)')
.borderRadius(8)
.padding(14)
.margin({ left: 24, right: 24 })
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
// 横屏(auto_rotation_landscape)下屏幕"高"为短边,过大的底边距会把进度块顶进居中 logo。
// 以底部为基准只留一小段间距,使进度/文字落在 logo 下方的空白带、贴近底部(系统手势条由安全区自动避让)。
.padding({ bottom: 24 })
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
.hideTitleBar(true)
.backgroundColor(Color.Transparent)
}
}