侧滑秒退=验收不通过:HarmonyOS NEXT 游戏如何正确拦截退出手势
做游戏片上架华为应用市场的朋友,多半在验收报告里见过这条扎眼的评语------"应用侧滑直接退出,未提示用户保存进度,判定为严重体验问题,不予通过。"
第一次遇到难免懵:明明系统默认行为就是侧滑清后台任务啊,凭啥找我茬?但站在验收官角度想,玩家正打到 BOSS 关,大拇指无意从边缘一划,游戏瞬间消失、存档全无------这锅确实得开发者背。系统把侧滑默认映射成了 terminateSelf(),而非普通的 onBackPressed 回调,你的游戏自然没机会弹"确认退出?"或先存盘。
今天咱们从底层手势分发聊起,配上彩色流程图、完整代码实战,以及面向 HarmonyOS 6 (API 22) 的新适配手段,争取让你一次把这坑填平。
一、 侧滑退出在鸿蒙里到底触发了什么?
先建立直觉。HarmonyOS(特别是 NEXT 及全面屏手势)的"侧滑返回/退出"在不同场景下走的分发路径不一样:
- 普通 Page/UIAbility :侧滑通常等价"系统返回手势" → 派发到
UIAbility.onBackPressed()→ 若未消费则返回false,由系统执行默认的terminateSelf()。 - 游戏(全屏、可能禁用了默认返回栈)或某些机型/版本组合 :侧滑直接被识别为"从多任务列表移除"或触发立即 terminate,不走
onBackPressed------尤其当你的 Ability 在module.json5中没有正确配置返回栈,或游戏引擎(如 Cocos/CocosCreator/Unity 导出)接管了触摸后未透传返回手势时。
关键点:验收要求游戏必须拦截这个行为,让用户有机会取消退出或先自动存档。默认"侧滑=秒杀进程"就是不合格的根因。
彩色分区流程图看清系统分发差异:
覆写且 return true (消费)
未覆写 或 return false
游戏引擎屏蔽手势透传
用户侧滑(边缘右滑/左滑 返回手势)
Ability / Window
是否启用返回手势监听?
onBackPressed 是否被覆写并 return true?
✅ 走 UIAbility.onBackPressed()
开发者可弹 Dialog / 存盘 / return true 消费掉
默认处理:
Ability 调用 terminateSelf()
→ 进程销毁(无存档提示)❌ 验收不通过!
部分全屏游戏/定制主题
→ 手势被游戏引擎消费未透传
→ 等效直接 terminate
弹'确认退出?'Dialog
是→存盘→terminateSelf()
否→关闭弹窗继续游戏 ✅
记住结论:覆写 onBackPressed() 并返回 true(表示已消费),在里面加入退出确认或自动存档逻辑,是从验收角度唯一合规的解法。 顺带一提,部分游戏引擎需确认触摸事件没有吞掉系统返回手势。
二、 代码实战:拦截返回 + 退出二次确认
以下是标准 Stage 模型 EntryAbility.ts 的正确写法。注意关键点------return true 消费事件,阻止系统继续执行 terminate。
typescript
// EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import Want from '@ohos.app.ability.Want';
import window from '@ohos.window';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG = 'GameEntryAbility';
export default class EntryAbility extends UIAbility {
private mainWindow: window.Window | undefined = undefined;
onWindowStageCreate(windowStage: window.WindowStage): void {
this.mainWindow = windowStage.getMainWindowSync();
// 可选:设置全屏、沉浸等游戏常用配置
this.mainWindow.setWindowLayoutFullScreen(true).catch((err: BusinessError) => {
console.error(`${TAG} setFullScreen failed: ${err.message}`);
});
windowStage.loadContent('pages/GameIndex', (err) => {
if (err.code) console.error(`${TAG} loadContent failed: ${err.message}`);
});
}
/**
* 核心:覆写返回键 / 侧滑手势回调
* @returns true → 事件已消费,系统不再 terminate
* false → 交还系统执行默认 terminateSelf()
*/
onBackPressed(): boolean | void {
// 弹出自定义退出确认(ArkUI 弹窗需借助 UIContext.runScopedTask 或在 Page 中触发)
// 简化演示:直接调一个全局方法让 GameIndex 页面 show Dialog
if (globalThis.gameExitController) {
globalThis.gameExitController.requestExitConfirm();
} else {
// fallback:无控制器时给个温和提示再退出
console.warn(`${TAG} no exit controller, will exit after auto-save stub`);
this.doAutoSaveAndExit();
return true; // 已处理,不重复 terminate
}
return true; // ★ 必须 return true 消费掉!
}
/** 存档后退出(通常在用户点"确认"时调用) */
public doAutoSaveAndExit(): void {
// TODO: 调游戏引擎存档接口(如 Cocos callNative 'saveProgress')
console.info(`${TAG} auto-saving progress...`);
setTimeout(() => {
this.context.terminateSelf().catch(() => {});
}, 300); // 给存档留一点点时间
}
onDestroy(): void {
this.mainWindow = undefined;
}
}
与之配合,在首页(游戏画布所在 Page)挂载/卸载退出控制器:
typescript
// pages/GameIndex.ets
import { promptAction } from '@kit.ArkUI';
@Entry @Component
struct GameIndex {
private dialogController: CustomDialogController | undefined = undefined;
aboutToAppear() {
// 把退出请求方法挂到全局,供 Ability.onBackPressed() 调用
globalThis.gameExitController = {
requestExitConfirm: () => this.showExitDialog()
};
}
private showExitDialog() {
this.dialogController = new CustomDialogController({
builder: ExitConfirmDialog({ onConfirm: () => {
// 用户确认 → 存档并退出
const abilityCtx = getContext(this).getApplicationContext()
.abilityDelegator?.getCurrentAbility?.() ?? getContext(this);
(abilityCtx as any).doAutoSaveAndExit?.();
this.dialogController?.close();
}, onCancel: () => {
this.dialogController?.close();
}}),
alignment: DialogAlignment.Center,
autoCancel: false
});
this.dialogController.open();
}
aboutToDisappear() {
globalThis.gameExitController = undefined;
this.dialogController?.close();
}
build() {
Stack() {
// 游戏渲染区域占位
Text('游戏画面区域')
.fontSize(24)
.fontColor(Color.White)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
}
}
// 简易确认退出对话框组件
@Component
struct ExitConfirmDialog {
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Column({ space: 16 }) {
Text('确认退出游戏?').fontSize(18).fontWeight(FontWeight.Bold)
Text('当前进度将自动保存').fontSize(14).fontColor(Color.Grey)
Row({ space: 30 }) {
Button('取消').onClick(this.onCancel)
Button('退出', { type: ButtonType.Capsule })
.backgroundColor('#E53935')
.onClick(this.onConfirm)
}
}
.padding(24)
}
}
验收关键点复核:
- ✅
onBackPressed()return true ------ 消费手势,不默退 - ✅ 弹 Dialog 让用户选择,或静默调引擎存档再
terminateSelf() - ✅ 存档完成后才调
terminateSelf(),不提前杀进程
三、 常见"为什么还不过"差异案例分析
| 情况 | 验收结果 | 原因 |
|---|---|---|
只覆写 onBackPressed 但忘了 return true |
❌ 仍判定秒退 | 返回 undefined/false ≈ 系统默认,依然 terminate |
| 游戏引擎(Cocos/Unity)全屏吞触摸,未透传返回手势 | ❌ 侧滑无反应或秒退 | 引擎需调用 OHOS NAPI 注册返回监听或设置 setAvoidBackGesture(false) 允许透传 |
| 弹了 Dialog 但点确认前进程已被 GC/后台清理 | ❌ 存档丢失 | 应在 Dialog confirm 回调中先调引擎存盘再退出 ,勿依赖 aboutToTerminate 时机 |
配置了 module.json5 中 "abilities":[{"isLauncherAbility":false,...}] 但没覆写 onBackPressed |
❌ 仍不合格 | 配置只影响任务栈展示,不改变侧滑分发逻辑 |
四、 HarmonyOS 6(API 22)适配前瞻与新 API
onBackPressed 覆写是最稳妥做法
1. 新增 setAvoidBackGesture(avoid: boolean) 精细控制(预测)
在 window.Window 上正式标准化该方法(部分预览版已有),让应用显式声明是否拦截/避让系统返回手势:
typescript
// 预期 HarmonyOS 6 API 形态(请以最终官方文档为准)
mainWindow.setAvoidBackGesture(false); // false=允许系统返回手势派发 → 触发 onBackPressed
// true=应用自行在游戏内处理(如虚拟摇柄区域),系统不派发返回事件
游戏启动时设 false 确保手势能透传到 onBackPressed;在需要禁用(如某些内购弹窗层)可动态切 true。
2. onBackPressed 行为规范化 ------ 异步消费支持
高版本系统可能允许 onBackPressed 返回 Promise<boolean>,方便你在里头等存档 IO 完成再决定是否消费。向下兼容写法目前不受影响,但新代码可考虑预留:
typescript
// 未来可能形态(当前仍用同步 return true)
// async onBackPressed(): Promise<boolean> {
// await this.saveProgress();
// return true;
// }
3. 多窗口 / 悬浮窗游戏场景
API 22 对 PiP(画中画)及悬浮游戏窗口的侧滑行为做了区分------悬浮窗模式侧滑默认不 terminate 主 Ability,但仍建议覆写 onBackPressed 中对 window.getWindowProperties().isFloating() 的判断,避免误弹退出框影响体验。
五、 避坑小妙招
return true别漏! 这是九成验收被打回的根源。建议代码中加注释强调。- Cocos Creator / Unity 导出项目 :检查原生层是否有
onBackPressed的 JNI/NAPI 桥接,确认引擎没有return false硬编进去。Cocos 可在native/entry/src/main/ets/EntryAbility.ets覆写并调jsb.reflection.callStaticMethod(...)通知 JS 层弹确认框。 - 存档异步时序 :引擎存档若为异步(写文件/上传云存档),确认在
then/callback内部 调terminateSelf(),勿在onBackPressed末尾直接跟着调------那时候存档可能还没写完。 - 模拟器不出现侧滑 :部分模拟器默认无全面屏手势,用
Ctrl+Backspace或 DevTools 模拟触发onBackPressed测逻辑。
最后唠一下什么问题
侧滑秒退被认定严重问题,说白了就是系统给了你拦截口但你没用 。覆写 UIAbility.onBackPressed() → return true → 弹确认或静默存档后再 terminateSelf(),这三步走完,验收此项必过。