15 KiB
15 KiB
2026-10-25
移动端沉浸式全屏(用户诉求:手机端整个游戏应沉浸式全屏显示)
之前只锁了横屏,但系统状态栏/导航栏仍可见,不是真正的沉浸。
改动文件:
app/lib/main.dart:_configureMobile()加SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky)(隐藏状态栏+导航栏;滑出后自动收回,最适合游戏)。app/android/app/src/main/res/values/styles.xml与values-night/styles.xml:LaunchTheme/NormalTheme 加android:windowFullscreen=true(冷启动即隐藏系统 UI,避免首帧闪现)。- 新建
values-v28/styles.xml与values-night-v28/styles.xml:两主题再加android:windowLayoutInDisplayCutoutMode=shortEdges,让画面延伸到刘海/挖孔区真正铺满(该属性仅 API28+,必须放 v28 限定目录否则低版本 aapt 编译报找不到资源)。 app/ios/Runner/Info.plist:加UIViewControllerBasedStatusBarAppearance=false+UIStatusBarHidden=true(launch 阶段即隐藏状态栏);并把UISupportedInterfaceOrientations与~ipad收紧为只横屏(与安卓一致)。app/lib/src/app_root.dart:_AppRootState加WidgetsBindingObserver,didChangeAppLifecycleState在resumed时重设immersiveSticky(切后台再回来不会丢沉浸,常规游戏标准做法)。
验证:flutter analyze 0 error;flutter test 14/14 通过。
说明:信箱(letterbox)黑边保留——游戏按比例完整显示在画面矩形内、外围黑边,这正是常规横版游戏的表现;沉浸仅指隐藏系统 UI、游戏铺满屏幕。iOS 底部 Home 指示条无法用公开 API 完全隐藏,属平台限制。沙箱仍无法跑原生 flutter build,需在真机 flutter pub get && flutter run 验证。
全屏铺满(去信箱黑边)+ 系统返回 = 游戏内上一页(用户二次诉求)
用户不接受信箱黑边("游戏内部显示也要全屏"),且系统返回手势不应退出游戏。
改动:
app/lib/src/ui/game_canvas.dart:letterboxRect改为stageMetrics(Size)—— 视口保证 ≥800×608 完整可见,另一维随屏幕比例加宽铺满全屏(横屏手机:高度定标 608、vw=屏宽/scale≈1318;比游戏窄的窗口则宽度定标 800)。Painter 每帧 paint 前world.setViewport(vw,vh)(只改纯数值,paint 阶段禁止 notifier)。app/lib/src/game/world.dart:新增viewW/viewH字段 +setViewport();startLevel用Camera(viewW,viewH);render/drawTitleBg 全部改用 viewW/viewH(含死亡提示居中、标题星空/刺地带)。摄像机自带关卡边界钳制,关卡比视口窄时自动居中 —— 横向看得多是"PC 游戏支持任意宽高比"的同款做法,不裁内容、不变形。app/lib/src/ui/touch_controls.dart:按钮定位从 letterboxRect 改为整屏 Rect(画面已铺满,无黑边可飘)。app/lib/src/app_root.dart:MaterialApp.home包PopScope(canPop:false, onPopInvokedWithResult)→_handleSystemBack():playing→pause;paused→resume;result→toLevels;editor→toTitle;scr-levels/scr-settings→toTitle;标题页忽略(防误退)。- 测试:
title_render_test.dart重写画布断言(铺满全屏、视口≥800×608)、手柄断言改整屏锚定;新增 2 个返回导航用例(关卡选择→标题、游戏中→暂停→继续,用tester.binding.handlePopRoute()模拟)。
验证:analyze 0 error;flutter test 16/16 通过。
修复「跳跃键经常失灵」(用户反馈)
根因有两处叠加:
- 虚拟键卡住常按(主因):
touch_controls.dart的按钮用Listener(不捕获指针)。手指在键上滑动后于键外松手时,松手事件落到别的控件 →touch['jump']永久卡在true;之后每次按下都因Input.setTouch的「值未变则 return」短路,触发不了新边沿 →hit('jump')恒 false,跳不起来、时好时坏。_TouchButton._release还有if(!_pressed) return进一步放任卡键。- 修:
GestureDetector(behavior: HitTestBehavior.opaque, onPanDown/onPanEnd/onPanCancel)替代Listener(Pan 识别器会认领指针,键外抬起/取消也回传本按钮);_release改为无论如何都强制setTouch(a,false)复位(去掉_pressed早退)。
- 修:
- 按钮重叠误触:旧默认布局大号跳跃键(94px)与射击/下蹲键在纵向像素重叠,重叠区按下会落到上层(射击)键。
BUTTONS重排让jump居末(绘制最上层,重叠区优先归跳跃键),并微调坐标消除重叠。 - 正确性问题(顺带):
world.tick的Input.endFrame()原本整个 tick 调一次;合帧时某帧跑 0 次更新、下一帧跑 2 次,会让一次按键被当两帧 hit → 误触二段跳。改为每个固定步长子步都调一次endFrame()(now 在两子步间保持,下一子步才清 prev,不丢按键)。 新增回归测试:模拟「跳跃键上按下→滑到屏幕外松手」,断言Input.held('jump')最终为 false(验证不再卡键)。
验证:analyze 0 error;flutter test 17/17 通过。注:用户设备已保存旧触屏布局(首跑写入),默认布局改动只影响新装/「恢复默认」;卡键修复与布局无关,对其现设备直接生效。
首页去滚动(改等比缩小)+ 编辑器溢出修复 + 关卡选择去色块(用户反馈)
- 首页滚动被用户否决:
screens.dartTitleScreen 的_FitScroll改为FittedBox(fit: BoxFit.scaleDown)+Column(mainAxisSize: min)—— 放得下按原尺寸、放不下整体等比缩小居中,既不滚动也不溢出。设置屏保留_FitScroll(内容 ~600px 缩到 310px 会小到不可读,滚动是合理方案)。 - 编辑器 RIGHT OVERFLOWED BY 48 PIXELS:根因是
ElevatedButton默认minimumSize(64,36)+MaterialTapTargetSize.padded,属性栏 200px 宽塞不下三个_SmallButton。修复:两个文件的_SmallButton加minimumSize: Size.zero+tapTargetSize: shrinkWrap+visualDensity: compact;顶部工具行 Row→Wrap(spacing:8, runSpacing:6)(窄屏自动换行);面操作行 Row→Wrap;_portalRowRow→Wrap(双下拉可能超宽)。 - 关卡选择去掉蓝绿黄色块:删掉底部
_Tag图例 Wrap 和卡片内的类型_Tag(用户:没什么用;分类已有 Tab 承担);_Tag/_tagColor/typeLabel/typeClass一并删除,import 清理为仅 LEVELS。卡片保留编号/名称/难度星星/通关状态。 - 测试:小高度标题用例从
scrollUntilVisible改为断言 footer 一屏可见;新增「编辑器 800×360 不溢出」回归(EditorController.open(25,19) 真渲染)。flutter test18/18、analyze 0 error。
「自定义按键」独立全屏页(用户需求)
- 新文件
lib/src/ui/touch_editor.dart:TouchLayoutEditor全屏编辑页,作为 AppRoot 的浮层scr-touchedit(透出下方实时游戏画布=标题星空动画,与玩游戏同等沉浸)。按钮用与_TouchButton完全一致的渲染与k=(w/W).clamp(0.6,2.6)缩放(所见即所得);点选后出现右下角黄点手柄拖动改大小(30~180);拖动/缩放即改即存(writeTouchLayout→metaSettings+Save.flush),返回无需确认;顶部「恢复默认/返回」,底部提示。 - 入口:设置页触摸区去掉内嵌
_TouchPreview(连同_DragBtn/_btnLabel/_cloneTouch/_touch工作副本一并删除,touch 布局不再由设置页代管,_confirm 不再写 s['touch']),改为「自定义按键」按钮 →screen.value='scr-touchedit';系统返回手势映射为回设置页。 - 默认布局仿用户截图(touch_layout.dart BUTTONS):◀(12.5,81,62) ▶(22,81,62) 🔫(78.5,90,56) ▼(94.5,89,56) ❚❚(96.5,8,40) ↺(91,56,52) 跳(85,73,78)。注意:老设备存档里已有旧布局,默认值只影响新装/恢复默认。
- 血泪:macOS 沙箱里
sed -i ''静默成功但参数被吞 → python 二次删除同一区间,误删 screens.dart 的_FitScroll/_MenuBg/_MenuButton/_SmallButton/_Tab/_Modal;已从本会话早前读过的完整内容精确重建(_SmallButton 带紧凑样式)。教训:批量删行一律用 python/专用工具,别混用 sed -i。 - 测试坑:widget 测试里
startGesture+单次 moveBy(60)不触发 pan(单大步位移不产出 update),改用tester.dragFrom或小步 move+pump 才有效。新测试:设置→自定义按键→全屏 7 键齐全→拖动跳跃键位置变化→返回设置。 - 验证:analyze 0 error;
flutter test18/18。
结算界面 BOTTOM OVERFLOWED BY 43px + 编辑器调键盘溢出 5.8px(用户反馈)
- 结算
_Modal(暂停/结算共用):外层 Column(min) 里内容区包Flexible(SingleChildScrollView)(min Column + Flexible 的标准做法:内容少时按原尺寸居中、超出时内部滚动)。横屏 ~360 逻辑高下「标题+4 行数据+新纪录+3 按钮」约 403px 必然超。 - 编辑器调键盘溢出:
app_rootScaffold 加resizeToAvoidBottomInset: false—— 游戏类 App 不让软键盘压缩界面(一压缩固定头部就溢出);所有输入框(地图名/宽/高/用户名)都在上半区,键盘不遮挡。 - 顺带:编辑器
_ImportModal地图列表同样包 Flexible+滚动(地图多时也会溢出)。 - 回归测试:800×360 下直泵
ResultScreen(better=true + hasNext=true 最长内容),断言无异常、三按钮可见。flutter test19/19、analyze 0 error。
结算弹窗改回「紧凑固定、不可滚动、不撑满」(用户推翻上一轮方案)
用户明确反对上一轮的可滚动方案:「地图通关不能设计成可滚动的,而且高度不要撑满,太丑了」。
- 根因:
Flexible(SingleChildScrollView)在Center下会把内容区撑到可用全高 → 弹窗高度=全屏高("撑满"),用户觉得丑。 - 新方案:删掉
Flexible/SingleChildScrollView,_Modal改Column(mainAxisSize.min)+Center,高度只包住内容、不撑满。同时整体压紧凑以在 ~360 逻辑高横屏内不溢出:lib/src/ui/screens.dart:_Modalwidth 320→300、padding 20→14、标题 26→20、标题后间距 16→10;_ResultLine行距 3→1、字号 15→14;_MenuButton外距 5→3、内 padding 12→8、字号 16→14;ResultScreen按钮前间距 12→8、新纪录上行距 6→4/字号16→15。lib/src/app_root.dart:三处MaterialApp全部加debugShowCheckedModeBanner:false—— 去掉用户所说的「app右上角的bug标」(即 Flutter 调试模式默认右上角 DEBUG 角标)。
- 验证:800×360 直泵
ResultScreen(better+hasNext 最长内容)不再 OVERFLOW;flutter test19/19、flutter analyze0 error。 - 教训:弹窗类 UI 在横屏小高度下,宁可用「紧凑固定尺寸」而非「可滚动」,用户对滚动版接受度低(觉得丑/撑满)。
关卡选择界面滑动报 Incorrect use of ParentDataWidget(修复)
- 根因:
_LevelCard的「已通关 ✔ 角标」原来是Positioned直接放在内层Column里。但Positioned必须是Stack的直接子级(它外层的 Stack 隔着GestureDetector→Container→Column),一旦某张已通关卡片被构建/滚入视图就抛Incorrect use of ParentDataWidget。GridView 懒构建 → 表现为「滑动到某张卡片时才报错」。 - 修复:把那枚
Positioned角标从内层 Column 删掉,改为外层 Stack 的直接子级(与删除按钮的Positioned并列),只对rec != null显示。 - 回归测试:新增
关卡选择:已通关卡片的 ✔ 角标不报 ParentDataWidget 错误——Save.clear标记首关已通关后直泵LevelSelectScreen,断言无异常且✔正常渲染。 - 验证:
flutter test20/20、flutter analyze0 error。
离开编辑器后再进入报 setState() called after dispose()(修复)
- 根因:
_EditorPanelState.initState把setState闭包直接挂到共享EditorController.onChange(闭包捕获当前 State);但 State 没有dispose()清掉它。world.toEditor()在host.onShowScreen设屏之后同步调host.editorShow()→editor.showOrOpen()→onChange?.call()(见 editor_panel.dartshowOrOpen/open等)。离开编辑器时旧 State 已 dispose、onChange仍指向它,再次进入的那次同步onChange?.call()就对死 State 调setState→ 报错。 - 修复(
lib/src/ui/editor_panel.dart):① 把闭包存为_onChange字段并加if (mounted)守卫;② 新增dispose()——若onChange == _onChange则置null,并dispose()三个TextEditingController(此前一直没 dispose,轻微泄漏);③initState改为赋值_onChange。 - 回归测试:新增
进出编辑器多次不报 setState-after-dispose——_boot后进编辑器→点「返回」离开→再进,断言AppError.current为 null 且EditorPanel正常重建。 - 验证:
flutter test21/21、flutter analyze0 error。
点开第二个地图报 type 'int' is not a subtype of type 'String?'(修复)
- 崩溃栈:
Level.reset (level.dart:257) ← new Level ← World.startLevel ← LevelSelectScreen._play。报错发生在构造含warp实体、但data.doors里找不到对应门的关卡时。 - 根因:web 原版
tmp/js/level.js:133是e.doorId = door ? door.id : e.idx;——JS 弱类型下doorId既可是字符串也可是数字(int),===比较同名类型即匹配。Flutter 移植把Entity.doorId声明成String?(level.dart:62),无匹配门时兜底idx(int)赋给String?即抛 TypeError。内置 23 关无doors/warps故不触发;自制关卡含 warp 且无匹配门即崩("先玩一个再开另一个"只是第二个关卡恰好有该内容)。 - 修复(
lib/src/game/level.dart):doorId字段由String?改为Object?(容纳 String 或 int,与 web 一致);world.dart里两处o.doorId == to['door']比较对 String/String、int/int 走值相等,语义不变。- 顺手加固同行的 null 兜底:
(data['doors'] as List?)?.cast<Map>()在doors为 null 时会因.firstWhere调在 null 上抛NoSuchMethodError,改为?? const <Map>[],消除同类崩溃。
- 回归测试:新增
含 warp 但无匹配门的关卡可正常构建(doorId 类型回归)——构造含W且doors:[]的关卡,Level(data)不再抛 TypeError,且 warp 实体的doorId为 int(回退序号)。 - 验证:
flutter test22/22、flutter analyze0 error。