欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net





一、 引言:从世俗模拟器到生命微观的哲学跃迁
在当前的移动端应用生态中,"人生模拟器"类文字互动游戏早已屡见不鲜。从出生的随机属性到升学、职场、财富的积累,传统的模拟器往往聚焦于宏观的社会学与世俗轨迹。然而,站在生命科学与组学(Omics)的宏大视角下,生命的本质远比一张名校录取通知书或一份高薪工作来得更为波澜壮阔。真正的"生存挑战",在受精卵结合、干细胞着床的那一微秒起,便已在一场不见硝烟的分子战场上拉开序幕。
本项目旨在突破传统应用的业务边界,将情景模拟 的载体从宏观人类个体,下潜至微观的全能干细胞(Totipotent Stem Cell) 。在《干细胞分化与表观遗传学生存模拟器》的开发周期中,我们不仅要面临庞大的生化知识图谱映射,更要解决跨平台图形学界面渲染中一个最为臭名昭著的毒瘤------边界溢出(Boundary Overflow,即经典的黄黑条纹 Right/Bottom Overflowed by XX pixels)。
本文将以深度的申论文笔,系统阐述我们如何利用 Flutter 框架构建一套纯前端、零后端依赖的离散状态机(Discrete State Machine),并通过严苛的弹性盒(Flexbox)与可卷轴(Scrollable)边界防御机制,彻底消灭渲染管线中的布局崩溃现象。
二、 领域建模与离散状态机推演
2.1 细胞指标四元矩阵模型
为了量化微观生命体的生存状态,我们建立了一个基于四元向量的生命体征矩阵(Vital Signs Matrix)。每一代细胞的存活与否,取决于该向量在状态空间中的坐标。
我们定义该向量为 V ∈ R 4 V \in \mathbb{R}^4 V∈R4:
V = [ g , v , i , m ] T V = [g, v, i, m]^T V=[g,v,i,m]T
其中:
- g g g 代表 Genomic Stability(基因组稳定性) ,反映了 DNA 双链的完整程度与表观遗传的保真度。一旦 g g g 逼近阈值底线,细胞将面临癌变(Cancerization)或端粒枯竭的终局。
- v v v 代表 Cellular Viability(细胞活力),反映了细胞器(如线粒体、内质网)的健康状态。
- i i i 代表 Immune Competence(免疫机能),表征系统面对外源逆转录病毒或噬菌体攻击时的抗逆水平。
- m m m 代表 Metabolic Efficiency(代谢效能),是细胞内三磷酸腺苷(ATP)供能循环的宏观体现。
2.2 状态机迁移方程
在本作中,玩家(即干细胞本身)在每一个时间纪元(Epoch,记为 t t t)面临特定的微环境情景(Scenario,记为 S t S_t St)。在给定的选项集合 O O O 中选择动作 a t ∈ O ( S t ) a_t \in O(S_t) at∈O(St) 后,生命体征状态的转移方程如下:
V t + 1 = max ( 0 , min ( 100 , V t + Δ V ( a t ) ) ) V_{t+1} = \max(0, \min(100, V_t + \Delta V(a_t))) Vt+1=max(0,min(100,Vt+ΔV(at)))
该函数确保了矩阵元素始终处于 [ 0 , 100 ] [0, 100] [0,100] 的安全边界内。同时,我们通过非线性判定函数 Φ ( V ) \Phi(V) Φ(V) 监控边界崩溃条件:
Φ ( V ) = { Apoptosis (凋亡) , if v ≤ 0 ∨ m ≤ 0 Cancerization (异化) , if g ≤ 20 Survival (存活) , otherwise \Phi(V) = \begin{cases} \text{Apoptosis (凋亡)}, & \text{if } v \le 0 \lor m \le 0 \\ \text{Cancerization (异化)}, & \text{if } g \le 20 \\ \text{Survival (存活)}, & \text{otherwise} \end{cases} Φ(V)=⎩ ⎨ ⎧Apoptosis (凋亡),Cancerization (异化),Survival (存活),if v≤0∨m≤0if g≤20otherwise
图 1:生命状态演化流向图
面临微环境 St
Apoptosis
Cancerization
Survival
干细胞状态 Vt
选择应对策略 at
应用突变向量 ΔV
计算截断值 Vt+1
边界探测 ΦV
细胞凋亡终局
恶性肿瘤终局
进入下一纪元 St+1
三、 核心困局突破:跨端 UI 边界溢出的彻底终结
在处理庞大叙事文字的情景模拟类产品中,Flutter 开发者极易陷入硬编码宽高(Hard-coded Dimensions)的陷阱。由于桌面端与移动端在 DPI 和屏幕物理尺寸上存在数量级的落差,当文本字数超过单行载荷时,若无弹性约束,渲染树(Render Tree)在进行 performLayout() 时将直接向引擎抛出异常,引发令人崩溃的黑屏与黄黑相间的错误警告。
3.1 Right Overflowed 的本质与弹性盒模型重构
"RIGHT OVERFLOWED BY XX PIXELS" 的根源在于父组件为 Row 时,默认的 crossAxisAlignment 与内部 Text 的宽度要求发生冲突。Row 为其子节点提供了无限的水平空间(Unbounded Horizontal Constraints) ,导致 Text 试图将长段落强行绘制在一行。
【防御准则 1】:标量隔离
在任何水平布局中,若存在不可预期的字符流,必须使用 Expanded 或 Flexible 将无界约束转化为有界约束,强制开启文本折行(softWrap: true)。
【防御准则 2】:卷轴隔离
对于不可预测深度的纵向内容块(如情景描述面板),外围容器不可使用基于屏幕高度的相对比例(如单纯的 Expanded 包裹固定容器)。正确的范式应当是:Expanded 包裹 SingleChildScrollView,从而将纵向无界约束转化为内部滚动视差。
3.2 布局决策的类图抽象
如下 UML 类图展示了本系统中的视图组件之间是如何通过弹性隔离层构建稳健的渲染管线的。
mounts
utilizes
encapsulates
CellularLifeSimulatorApp
+build(BuildContext context) : Widget
SimulatorHomeScreen
-currentVitals VitalSigns
-currentNodeId String
-cellEpoch int
+build(BuildContext context) : Widget
<<Widget>>
LayoutBuilderPanel
+constraints BoxConstraints
+responsiveBuild() : Widget
<<Widget>>
FlexibleTextContainer
-text String
-softWrap bool
四、 跨端情景树渲染架构解析 (核心代码点)
本节将逐一拆解本模拟器引擎的四处核心代码,深入剖析其背后的架构思想与溢出防御机制。
4.1 核心源码一:领域模型设计与状态矩阵变迁
领域驱动设计(DDD)要求我们将业务逻辑高度浓缩在充血模型之中。下述代码定义了核心生命体征 VitalSigns 及其应用冲击向量的方法。通过引入 .clamp(0, 100) 的标量截断器,在数学模型的最底层彻底切断了数据越界的可能性。
dart
// -----------------------------------------------------
// 核心源码一:生命体征与突变状态矩阵
// -----------------------------------------------------
class VitalSigns {
double genomics; // 基因组稳定性 0-100
double viability; // 细胞活力 0-100
double immunity; // 免疫机能 0-100
double metabolism; // 代谢效能 0-100
VitalSigns({
required this.genomics,
required this.viability,
required this.immunity,
required this.metabolism,
});
// 状态迁移:冲击向量融合与数学截断
void applyImpact(VitalSigns impact) {
genomics = (genomics + impact.genomics).clamp(0, 100);
viability = (viability + impact.viability).clamp(0, 100);
immunity = (immunity + impact.immunity).clamp(0, 100);
metabolism = (metabolism + impact.metabolism).clamp(0, 100);
}
// 边界探测钩子函数
bool get isDead => viability <= 0 || metabolism <= 0;
bool get isMutated => genomics <= 20; // 发生癌变或变异判定
}
【技术评述】 :这一设计不仅实现了代码的高度内聚,还将后续 UI 层的判定逻辑极度轻量化。所有的生存逻辑交由对象自身决策,避免了在 Widget 树中散落着冗长的 if-else 分支。
4.2 核心源码二:无后端全内存剧本数据库
为了确保该平台能在极端网络环境下(甚至完全离线的局域网医疗设备中)流畅运行,所有节点剧本均被收束于 ScenarioDatabase 的静态内存池中。这种零后端高度解耦的架构极大降低了 I/O 延迟带来的掉帧可能。
dart
// -----------------------------------------------------
// 核心源码二:剧情节点微型数据库
// -----------------------------------------------------
class ScenarioDatabase {
static final Map<String, ScenarioNode> nodes = {
'EPISODE_2': ScenarioNode(
id: 'EPISODE_2',
title: '【入侵】外源逆转录病毒侵袭',
description: '警报!检测到大量外源病毒 RNA 序列正在突破细胞膜,试图利用逆转录酶将自身的基因整合进你的染色体中。一旦整合成功,你的基因组将遭到严重污染。',
options: [
ScenarioOption(
text: '不计代价激活 CRISPR/核酸酶 防卫系统进行定点剪切',
impact: VitalSigns(genomics: 15, viability: -15, immunity: -30, metabolism: -15),
nextNodeId: 'EPISODE_3', // 成功过渡到下一纪元
),
ScenarioOption(
text: '放弃抵抗,任由病毒序列整合(基因组严重破坏,但意外获得某些异源蛋白质存活特性)',
impact: VitalSigns(genomics: -40, viability: 10, immunity: -10, metabolism: 0),
nextNodeId: 'EPISODE_3',
),
],
),
// ... 其他剧本节点定义
};
}
【技术评述】:在选项设计上,我们抛弃了二元对立的"好与坏",而是深度拥抱了生物学中"权衡(Trade-off)"的核心概念。例如,激活 CRISPR 系统固然能维持基因组的纯洁(genomics +15),但这种高耗能反应必定会严重抽调代谢(metabolism -15)与整体活力(viability -15)。
4.3 核心源码三:中台情景描述面板的防溢出纵深设计
在此前的项目历史中,由于文本长度的暴增导致 Bottom Overflow 是最为棘手的问题。为此,我们在剧情展示区(Middle Panel)实施了最为严酷的空间管制:外层用 Expanded 占据剩余弹性空间,次层包裹 SingleChildScrollView,核心层由 Column 展开。
dart
// -----------------------------------------------------
// 核心源码三:防纵向崩塌的剧本渲染管线
// -----------------------------------------------------
Widget _buildScenarioPanel(ScenarioNode node) {
return Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white12),
),
// 核心防御点1:Expanded -> SingleChildScrollView
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.hub_outlined, color: const Color(0xFF00FFCC), size: 28),
const SizedBox(width: 12),
// 核心防御点2:对于横向Row中的长标题,必用Expanded配合softWrap
Expanded(
child: Text(
node.title,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
softWrap: true, // 允许折行
),
),
],
),
const SizedBox(height: 24),
// 多行正文区直接天然折行
Text(
node.description,
style: const TextStyle(fontSize: 16, height: 1.6, color: Color(0xFFC9D1D9)),
softWrap: true,
),
],
),
),
);
}
【技术评述】 :通过 Expanded(child: SingleChildScrollView(...)) 的组合拳,无论传入的 description 是三十字还是三千字,渲染引擎(RenderBox)都能正确地将该区域标记为有限的视口大小,并将滚动偏移量交由内部驱动。这是 Flutter 响应式编程中对抗文本溢出的一项教科书级解法。
4.4 核心源码四:动作面板的自适应与递归生命演进
底部的选项按钮面板(Bottom Panel)往往是容易发生横向挤压与文本截断的重灾区。为了适应不同大小的设备,我们使用 ListView.separated 配合内部 Row 的 Expanded 子级,确保用户能看清选项描述并执行决策。
dart
// -----------------------------------------------------
// 核心源码四:动作突变决策渲染台
// -----------------------------------------------------
Widget _buildOptionButton(ScenarioOption option) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _handleOptionSelected(option), // 触发演进递归
borderRadius: BorderRadius.circular(12),
splashColor: const Color(0xFF00FFCC).withOpacity(0.2),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFF21262D),
border: Border.all(color: const Color(0xFF30363D)),
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, // 关键:顶部对齐
children: [
const Icon(Icons.arrow_right_alt, color: Color(0xFF00FFCC)),
const SizedBox(width: 12),
// 防御点:文字长度决定了按钮高度将被撑开,而不是向右撕裂屏幕
Expanded(
child: Text(
option.text,
style: const TextStyle(fontSize: 15, color: Colors.white, height: 1.4),
softWrap: true,
),
),
],
),
),
),
);
}
【技术评述】 :在处理跨端按钮事件时,使用 Material 和 InkWell 而不是生硬的 GestureDetector,能够提供原生级的涟漪交互反馈(Ripple Effect)。而内部 Expanded 的应用,使得包含诸如"强制启动HSP表达抑制机制并释放胞内信号"这种长达几十个字符的复合型描述能优雅地折断为多行,极大地提升了系统的工程健壮性。
五、 生命轨迹与渲染周期的同步律
在本套《干细胞分化与表观遗传学生存模拟器》的闭环构建中,代码世界的局部重绘(Local Reconstruction via setState)与生命科学层面的细胞迭代(Cellular Iteration)形成了一种奇妙的映射关系。每一次屏幕的微秒级刷新,都对应着一次核酸多聚酶在模板链上惊心动魄的滑移。
我们使用了内置的 FadeTransition 和 AnimationController,使得纪元更替时的页面切换并不突兀,而是如细胞膜融合般顺滑:
当 `_fadeController.forward(from: 0.0)` 被调用时,Opactity 矩阵经历从 0.0 至 1.0 的平滑积分。这不仅掩盖了底层状态树重构瞬间的闪烁,更为玩家(细胞生命体)的每一次生死攸关的抉择平添了一分肃穆的仪式感。
六、 总结与展望
通过对 Flutter 底层 Flex 布局体系的深度解构,我们在没有任何第三方状态管理插件和后端数据库加持的前提下,纯靠内力锻造出了一套坚不可摧、免受任何 Overflow 侵害的情景模拟器引擎。
这部作品从细胞破核分裂的曙光启航,历经高温热休克、病毒逆转录、神经与免疫定向分化的重重磨砺,深刻阐释了生命系统的高容错率与脆弱性的辩证统一。在未来的版本迭代中,我们甚至可以通过解析真实的单细胞测序(scRNA-seq)数据矩阵,利用大模型自动为该模拟器生成无限拓扑演化的命运剧情树(Cell Fate Trajectory Tree)。
在这个被硅基电路与代码统治的世界里,碳基生命的史诗,依然可以藉由这不朽的跨端渲染引擎被生生世世地传唱!
源码
bash
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const CellularLifeSimulatorApp());
}
class CellularLifeSimulatorApp extends StatelessWidget {
const CellularLifeSimulatorApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '细胞演化模拟器',
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0A0E17), // 极客深海暗色
fontFamily: 'Courier', // 选用等宽/极客字体
textTheme: const TextTheme(
bodyMedium: TextStyle(color: Color(0xFFE2E8F0)),
),
),
home: const SimulatorHomeScreen(),
);
}
}
// ==========================================
// 领域模型定义:生命指标与情景树
// ==========================================
class VitalSigns {
double genomics; // 基因组稳定性 0-100
double viability; // 细胞活力 0-100
double immunity; // 免疫机能 0-100
double metabolism; // 代谢效能 0-100
VitalSigns({
required this.genomics,
required this.viability,
required this.immunity,
required this.metabolism,
});
void applyImpact(VitalSigns impact) {
genomics = (genomics + impact.genomics).clamp(0, 100);
viability = (viability + impact.viability).clamp(0, 100);
immunity = (immunity + impact.immunity).clamp(0, 100);
metabolism = (metabolism + impact.metabolism).clamp(0, 100);
}
bool get isDead => viability <= 0 || metabolism <= 0;
bool get isMutated => genomics <= 20; // 发生癌变或变异
}
class ScenarioOption {
final String text;
final VitalSigns impact;
final String nextNodeId;
ScenarioOption({
required this.text,
required this.impact,
required this.nextNodeId,
});
}
class ScenarioNode {
final String id;
final String title;
final String description;
final List<ScenarioOption> options;
final bool isEnding; // 是否为结局节点
ScenarioNode({
required this.id,
required this.title,
required this.description,
required this.options,
this.isEnding = false,
});
}
// ==========================================
// 剧本数据库 (无后端,内存级状态机)
// ==========================================
class ScenarioDatabase {
static final Map<String, ScenarioNode> nodes = {
'START': ScenarioNode(
id: 'START',
title: '【原点】胚胎干细胞着床',
description: '你是一个刚刚完成受精的干细胞。在温暖的母体子宫腔内,充足的营养正在渗透。现在,你必须开启第一轮艰难的细胞增殖有丝分裂。请选择你的增殖策略:',
options: [
ScenarioOption(
text: '加速有丝分裂(消耗大量代谢,但能快速扩增细胞数量,略微增加基因复制出错率)',
impact: VitalSigns(genomics: -10, viability: 20, immunity: 0, metabolism: -20),
nextNodeId: 'EPISODE_1',
),
ScenarioOption(
text: '严谨的DNA校验分裂(维持极高的基因稳定性,但扩增缓慢,可能错过着床最佳窗口)',
impact: VitalSigns(genomics: 10, viability: -10, immunity: 0, metabolism: -5),
nextNodeId: 'EPISODE_1',
),
],
),
'EPISODE_1': ScenarioNode(
id: 'EPISODE_1',
title: '【事件】母体高热危机',
description: '突然,外环境温度飙升至 39.8°C。你的内部蛋白质开始出现折叠异常,溶酶体有破裂的风险。由于代谢速率的异常波动,你感受到了极大的生存压力。',
options: [
ScenarioOption(
text: '启动热休克蛋白(HSP)超表达机制(强行消耗代谢储备以稳定蛋白结构)',
impact: VitalSigns(genomics: 0, viability: 15, immunity: 0, metabolism: -30),
nextNodeId: 'EPISODE_2',
),
ScenarioOption(
text: '降频进入休眠状态(停止生长活动,降低所有机能以苟延残喘)',
impact: VitalSigns(genomics: 5, viability: -25, immunity: -10, metabolism: 10),
nextNodeId: 'EPISODE_2',
),
],
),
'EPISODE_2': ScenarioNode(
id: 'EPISODE_2',
title: '【入侵】外源逆转录病毒侵袭',
description: '警报!检测到大量外源病毒 RNA 序列正在突破细胞膜,试图利用逆转录酶将自身的基因整合进你的染色体中。一旦整合成功,你的基因组将遭到严重污染。',
options: [
ScenarioOption(
text: '不计代价激活 CRISPR/核酸酶 防卫系统进行定点剪切',
impact: VitalSigns(genomics: 15, viability: -15, immunity: -30, metabolism: -15),
nextNodeId: 'EPISODE_3',
),
ScenarioOption(
text: '放弃抵抗,任由病毒序列整合(基因组严重破坏,但意外获得某些异源蛋白质存活特性)',
impact: VitalSigns(genomics: -40, viability: 10, immunity: -10, metabolism: 0),
nextNodeId: 'EPISODE_3',
),
],
),
'EPISODE_3': ScenarioNode(
id: 'EPISODE_3',
title: '【抉择】细胞分化十字路口',
description: '你已经度过了最危险的时期。内胚层信号因子正在向你发送浓度梯度。你面临着生命最终形态的抉择。',
options: [
ScenarioOption(
text: '响应神经元诱导信号,分化为神经细胞(高代谢需求,不再具备分裂能力)',
impact: VitalSigns(genomics: 0, viability: 0, immunity: 0, metabolism: -40),
nextNodeId: 'END_NEURON',
),
ScenarioOption(
text: '响应免疫细胞诱导信号,分化为巨噬细胞(游走于前线,随时准备吞噬病原体)',
impact: VitalSigns(genomics: 0, viability: 0, immunity: 40, metabolism: -10),
nextNodeId: 'END_MACROPHAGE',
),
],
),
'END_NEURON': ScenarioNode(
id: 'END_NEURON',
title: '【终局】中枢神经的基石',
description: '演化完成。你成为了一颗拥有复杂树突结构的神经元细胞。虽然你丧失了继续分裂的能力,但你承载着宿主的记忆与思维电信号。你将在宿主的整个生命周期中坚守岗位。',
options: [
ScenarioOption(text: '重置生命轮回 (Restart)', impact: VitalSigns(genomics: 0, viability: 0, immunity: 0, metabolism: 0), nextNodeId: 'START'),
],
isEnding: true,
),
'END_MACROPHAGE': ScenarioNode(
id: 'END_MACROPHAGE',
title: '【终局】无畏的吞噬者',
description: '演化完成。你成为了免疫系统的精锐------巨噬细胞。你开始在组织间隙中游走,吞噬一切非我异物。你的生命短暂但充满荣耀。',
options: [
ScenarioOption(text: '重置生命轮回 (Restart)', impact: VitalSigns(genomics: 0, viability: 0, immunity: 0, metabolism: 0), nextNodeId: 'START'),
],
isEnding: true,
),
'END_DEAD': ScenarioNode(
id: 'END_DEAD',
title: '【凋亡】细胞崩解',
description: '由于过度消耗代谢或失去了基础活力,你的细胞膜破裂,溶酶体酶外溢,引发了自溶现象。你的生命序列就此终止。',
options: [
ScenarioOption(text: '重新演化 (Restart)', impact: VitalSigns(genomics: 0, viability: 0, immunity: 0, metabolism: 0), nextNodeId: 'START'),
],
isEnding: true,
),
'END_CANCER': ScenarioNode(
id: 'END_CANCER',
title: '【异化】永生与恶性增殖',
description: '警告!由于基因组稳定性降至冰点,抑癌基因发生严重突变!你失去了接触抑制能力,获得了端粒酶的无限活性。你已成为一颗恶性肿瘤细胞。虽然获得了局部的"永生",但你最终将与宿主同归于尽。',
options: [
ScenarioOption(text: '重置生命轮回 (Restart)', impact: VitalSigns(genomics: 0, viability: 0, immunity: 0, metabolism: 0), nextNodeId: 'START'),
],
isEnding: true,
),
};
}
// ==========================================
// 主视图控制器
// ==========================================
class SimulatorHomeScreen extends StatefulWidget {
const SimulatorHomeScreen({Key? key}) : super(key: key);
@override
State<SimulatorHomeScreen> createState() => _SimulatorHomeScreenState();
}
class _SimulatorHomeScreenState extends State<SimulatorHomeScreen> with SingleTickerProviderStateMixin {
late VitalSigns currentVitals;
late String currentNodeId;
int cellEpoch = 1;
// 加入简单的淡入淡出动画,增强生命演进的质感
late AnimationController _fadeController;
@override
void initState() {
super.initState();
_fadeController = AnimationController(vsync: this, duration: const Duration(milliseconds: 600));
_restartGame();
}
@override
void dispose() {
_fadeController.dispose();
super.dispose();
}
void _restartGame() {
setState(() {
currentVitals = VitalSigns(genomics: 80, viability: 80, immunity: 50, metabolism: 80);
currentNodeId = 'START';
cellEpoch = 1;
});
_fadeController.forward(from: 0.0);
}
void _handleOptionSelected(ScenarioOption option) {
setState(() {
// 1. 应用指标影响
currentVitals.applyImpact(option.impact);
cellEpoch++;
// 2. 状态越界判定拦截 (红线检查:死亡或癌变优先触发)
if (currentVitals.isDead && option.nextNodeId != 'START') {
currentNodeId = 'END_DEAD';
} else if (currentVitals.isMutated && option.nextNodeId != 'START') {
currentNodeId = 'END_CANCER';
} else {
currentNodeId = option.nextNodeId;
}
// 如果选了重置,走重置流
if (option.nextNodeId == 'START') {
_restartGame();
return;
}
});
_fadeController.forward(from: 0.0);
}
@override
Widget build(BuildContext context) {
final currentNode = ScenarioDatabase.nodes[currentNodeId]!;
return Scaffold(
// 背景注入
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/explore_ohos.png'),
fit: BoxFit.cover,
opacity: 0.15, // 极低透明度,做暗黑水印背景
),
),
// 外层 SafeArea 保护刘海屏溢出
child: SafeArea(
// 外层 Padding 防护
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
// 使用 Column 时,严格定义内部子元素的弹性比例,这是防止 Overflow 的核心法则!
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 顶部:生命指标监控面板
_buildVitalsMonitor(),
const SizedBox(height: 24),
// 中间:剧情描述面板 (使用 Expanded 强力填充剩余空间,内部允许滚动,杜绝高度越界)
Expanded(
child: FadeTransition(
opacity: _fadeController,
child: _buildScenarioPanel(currentNode),
),
),
const SizedBox(height: 16),
// 底部:动作选项列表 (采用不可滚动但自适应包裹的弹性布局,或者外嵌灵活滚动)
_buildOptionsPanel(currentNode),
],
),
),
),
),
);
}
// 构建生物指标监控台
Widget _buildVitalsMonitor() {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
border: Border.all(color: const Color(0xFF00FFCC).withOpacity(0.3), width: 1),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: const Color(0xFF00FFCC).withOpacity(0.05),
blurRadius: 10,
spreadRadius: 2,
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'CELLULAR VITAL SIGNS // 细胞生命体征',
style: TextStyle(color: Color(0xFF00FFCC), fontWeight: FontWeight.bold, fontSize: 14),
),
Text(
'EPOCH: $cellEpoch',
style: const TextStyle(color: Colors.orangeAccent, fontWeight: FontWeight.bold),
)
],
),
const SizedBox(height: 16),
// 为了防止 Row 溢出,如果屏幕极窄,使用 Wrap 或者强制比例 Expanded
LayoutBuilder(
builder: (context, constraints) {
// 在宽屏用 Row,在窄屏(手机竖屏)用双行分列
if (constraints.maxWidth > 600) {
return Row(
children: [
Expanded(child: _buildSingleVitalBar('基因组稳定', currentVitals.genomics, Colors.blueAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSingleVitalBar('细胞活力', currentVitals.viability, Colors.greenAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSingleVitalBar('免疫机能', currentVitals.immunity, Colors.purpleAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSingleVitalBar('代谢效能', currentVitals.metabolism, Colors.orangeAccent)),
],
);
} else {
// 移动端:网格状排布,避免水平拥挤导致的右侧溢出
return Column(
children: [
Row(
children: [
Expanded(child: _buildSingleVitalBar('基因组稳定', currentVitals.genomics, Colors.blueAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSingleVitalBar('细胞活力', currentVitals.viability, Colors.greenAccent)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(child: _buildSingleVitalBar('免疫机能', currentVitals.immunity, Colors.purpleAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSingleVitalBar('代谢效能', currentVitals.metabolism, Colors.orangeAccent)),
],
),
],
);
}
},
),
],
),
);
}
// 单个指标进度条
Widget _buildSingleVitalBar(String label, double value, Color color) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 防止 label 极长时溢出
Expanded(
child: Text(
label,
style: const TextStyle(fontSize: 12, color: Colors.white70),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Text('${value.toInt()}%', style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 6),
// 进度条本身自带边界自适应特性
LinearProgressIndicator(
value: value / 100.0,
backgroundColor: Colors.white10,
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 6,
borderRadius: BorderRadius.circular(3),
),
],
);
}
// 构建剧本事件展示区
Widget _buildScenarioPanel(ScenarioNode node) {
return Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: const Color(0xFF161B22),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white12),
),
// 外层已经是 Expanded,内部用 SingleChildScrollView 彻底消灭垂直溢出 (BOTTOM OVERFLOWED)
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 剧目标题区
Row(
children: [
Icon(
node.isEnding ? Icons.warning_amber_rounded : Icons.hub_outlined,
color: node.isEnding ? Colors.redAccent : const Color(0xFF00FFCC),
size: 28,
),
const SizedBox(width: 12),
// 使用 Expanded 包裹文本,彻底消灭标题过长时的右侧溢出 (RIGHT OVERFLOWED)
Expanded(
child: Text(
node.title,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: node.isEnding ? Colors.redAccent : Colors.white,
letterSpacing: 1.2,
),
softWrap: true, // 允许折行
),
),
],
),
const SizedBox(height: 24),
// 剧目详细内容描述
Text(
node.description,
style: const TextStyle(
fontSize: 16,
height: 1.6,
color: Color(0xFFC9D1D9),
),
softWrap: true,
),
],
),
),
);
}
// 构建选项动作区
Widget _buildOptionsPanel(ScenarioNode node) {
// 选项列表区可能高度也会超过极限,因此包裹在一层定高的或者具有内在滚动的组件中
// 为了美观,我们使用 ListView.separated 来罗列选项
return Container(
constraints: const BoxConstraints(maxHeight: 280), // 给一个最大高度约束
child: ListView.separated(
shrinkWrap: true, // 根据内容收缩高度
physics: const BouncingScrollPhysics(),
itemCount: node.options.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final option = node.options[index];
return _buildOptionButton(option);
},
),
);
}
// 单个选项按钮
Widget _buildOptionButton(ScenarioOption option) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _handleOptionSelected(option),
borderRadius: BorderRadius.circular(12),
splashColor: const Color(0xFF00FFCC).withOpacity(0.2),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: const Color(0xFF21262D),
border: Border.all(color: const Color(0xFF30363D)),
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.arrow_right_alt, color: Color(0xFF00FFCC)),
const SizedBox(width: 12),
// 再次使用 Expanded 包裹选项文本,这是防 Overflow 的铁律
Expanded(
child: Text(
option.text,
style: const TextStyle(fontSize: 15, color: Colors.white, height: 1.4),
softWrap: true,
),
),
],
),
),
),
);
}
}