flutter_for_openharmony逆向思维训练app实战+镜像识别实现

这一页在产品里的定位更像“思维热身”:
- 核心设计目标:降低用户操作门槛,无需复杂交互即可理解核心概念
- 视觉表达逻辑:通过极简的图形对照,替代文字说教
- 训练核心目标:让用户下意识形成“换位思考、反向观察”的思维习惯
它的实现也非常干净:一个 StatelessWidget,用 Row + Column 组织左右两块图形,然后用圆角位置对调来表达镜像。
本文涉及文件
lib/feature_pages.dart:核心实现页,承载镜像识别的UI与布局lib/app.dart:应用入口配置,关联路由与主题lib/main.dart:程序启动入口,初始化依赖与页面路由
1. 入口在哪里:从模式识别列表进入
镜像识别属于 PatternRecognitionPage(模式识别)里的一个功能入口。
入口写法和其他特性页一致:点击卡片后 push 新页面。
这种“聚合+导航”的分层结构有明确的工程价值:
- 入口页职责单一:仅负责功能分类与路由跳转,不掺杂业务逻辑
- 子页聚焦核心:每个训练页只关注自身的展示/交互逻辑,降低耦合
- 扩展成本低:新增训练页只需在入口页加卡片,无需改动其他子页
核心入口代码示例:
ListTile(
title: Text('镜像识别训练'),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => MirrorRecognitionPage()),
),
)
- 路由跳转使用MaterialPageRoute,保持与App全局导航风格一致
- 点击事件直接指向目标页,无中间态,符合“训练页快速访问”的产品设计
2. MirrorRecognitionPage
下面分段拆解项目 lib/feature_pages.dart 中的核心实现,每段代码配套详细设计思路:
2.1 页面类定义:选择StatelessWidget的核心逻辑
class MirrorRecognitionPage extends StatelessWidget {
const MirrorRecognitionPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('镜像识别')),
- 类修饰符:使用
const构造方法,减少Widget重建开销 - 继承选择:无状态组件适配“纯展示型”页面,避免State冗余
- 基础结构:Scaffold作为页面容器,提供标准的AppBar+Body结构
- 标题设计:AppBar标题直接明确训练主题,无多余装饰,符合训练页简洁风格
2.2 页面边距与整体布局:统一视觉规范
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Text('镜像思维训练',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold
)
),
- 边距设计:16.w的全局统一边距,适配不同屏幕尺寸(基于flutter_screenutil)
- 布局容器:Column垂直排布,符合“标题-内容-提示”的阅读流
- 标题样式:24.sp加粗字体,视觉层级优先,快速传递页面核心主题
- 单位规范:全量使用.w/.h/.sp,保证多终端视觉一致性
2.3 间距控制:提升视觉呼吸感
SizedBox(height: 24.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Text('原图', style: TextStyle(fontSize: 16.sp)),
- 间距设计:24.h的垂直间距,分隔标题与核心内容区,避免视觉拥挤
- 行布局:Row+spaceEvenly,让左右两组图形均匀分布,强化“对照”属性
- 子列结构:每个图形块用Column包裹,实现“文字说明+图形”的垂直组合
- 文字尺寸:16.sp的说明文字,层级低于标题,高于提示文字,主次分明
2.4 原图图形实现:核心视觉元素
Container(
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40.r),
bottomRight: Radius.circular(40.r),
),
),
),
- 尺寸设计:80.w正方形,兼顾辨识度与屏幕占比,适配手机/平板等终端
- 背景色:纯蓝色填充,无渐变/纹理,避免视觉干扰,聚焦形状差异
- 圆角逻辑:40.r圆角半径(边长的50%),形成强视觉特征的对角圆角
- 圆角位置:左上+右下,构建“方向性”视觉标识,为镜像对比做铺垫
2.5 镜像图形实现:对称设计的核心
],
),
Column(
children: [
Text('镜像', style: TextStyle(fontSize: 16.sp)),
Container(
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
color: Colors.blue,
- 视觉一致性:与原图保持相同尺寸/颜色,仅修改圆角位置,强化“镜像”而非“新图形”
- 文字标识:明确标注“镜像”,降低用户理解成本
- 容器复用:完全复用Container结构,仅调整 borderRadius 参数,符合“最小变更表达核心差异”的设计原则
2.6 镜像圆角的反向设计:核心差异点
borderRadius: BorderRadius.only(
topRight: Radius.circular(40.r),
bottomLeft: Radius.circular(40.r),
),
),
),
],
),
],
),
- 圆角反转:左上↔右上、右下↔左下,精准表达“水平镜像”的核心逻辑
- 无其他修改:仅调整圆角位置,避免多余视觉元素干扰用户对“镜像”的理解
- 布局闭合:Row包裹的左右Column完整闭合,布局层级清晰,无冗余嵌套
2.7 底部提示文案:强化训练核心
SizedBox(height: 32.h),
Text('镜像思维:从不同角度观察问题',
style: TextStyle(
fontSize: 16.sp,
color: Colors.purple
)
),
],
),
),
);
}
}
- 间距设计:32.h的垂直间距,分隔图形区与提示区,视觉上形成“观察-总结”的节奏
- 文案设计:直接点出训练核心,将视觉差异升维到思维方法
- 颜色区分:紫色文字,与蓝色图形形成温和对比,吸引注意力但不刺眼
- 布局闭合:Column/Row/Scaffold层级完整闭合,符合Flutter Widget树规范
3. 为什么用 StatelessWidget:这是一页“展示型训练”
你把页面定义为 StatelessWidget,原因可拆解为以下核心点:
- 状态维度:页面无用户输入(按钮点击/输入框/滑动等),无需维护state
- 数据维度:展示内容固定,无动态数据源(网络/本地存储/状态管理)
- 性能维度:StatelessWidget重建开销更低,适合纯展示场景
- 维护维度:减少State类的冗余代码,降低后期维护成本
反例警示:若强行使用StatefulWidget,会出现以下问题:
- 多余的createState/setState方法,增加代码量
- 无意义的状态生命周期,增加调试复杂度
- 违背“最小功能匹配”的工程原则,降低代码可读性
4. 页面骨架:Scaffold + Padding + Column
这页的骨架是Flutter中“展示型页面”的典型范式,拆解如下:
4.1 Scaffold:页面基础容器
return Scaffold(
appBar: AppBar(title: const Text('镜像识别')),
body: Padding(...),
);
- 职责:提供AppBar、Body、FloatingActionButton等标准页面组件
- 适配性:自动适配系统状态栏/导航栏,无需手动处理屏幕适配
- 扩展性:如需新增操作按钮(如“重置”“下一题”),可直接在Scaffold中添加
4.2 Padding:统一页边距
padding: EdgeInsets.all(16.w),
- 设计原则:全局统一16.w边距,保证App内所有页面边距视觉一致
- 适配逻辑:.w单位基于屏幕宽度适配,不同尺寸设备边距比例统一
- 体验优化:避免内容贴边,提升阅读舒适度,符合移动端设计规范
4.3 Column:垂直内容组织
child: Column(
children: [
// 标题
// 图形对照区
// 提示文案
],
)
- 布局逻辑:垂直方向线性排布,符合用户“从上到下”的阅读习惯
- 灵活性:可通过mainAxisAlignment/crossAxisAlignment调整整体对齐方式
- 局限性:内容超出屏幕时需配合SingleChildScrollView,当前页面内容固定无需滚动
5. 核心布局:Row 的两个 Column,左右对照
镜像识别的核心是“视觉对照”,Row+双Column的布局设计有以下优势:
5.1 Row的对齐策略
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [/* 左列:原图 | 右列:镜像 */],
)
- 对齐方式:spaceEvenly让左右内容均匀分布,视觉上形成“对称对照”
- 空间分配:自动平分Row的可用空间,无需手动计算宽度
- 响应式:适配不同屏幕宽度,左右列始终保持等距,不会挤压/重叠
5.2 左右Column的结构一致性
// 左列:原图
Column(
children: [Text('原图'), Container(/* 原图图形 */)],
)
// 右列:镜像
Column(
children: [Text('镜像'), Container(/* 镜像图形 */)],
)
- 结构复用:左右列完全相同的层级结构,仅修改文字和圆角参数
- 视觉对称:保证用户注意力集中在“差异点(圆角)”,而非“结构差异”
- 可维护性:后续修改样式时,只需统一调整Column结构,无需分别修改
5.3 布局层级的工程价值
- 扁平化:Row直接包裹两个Column,无多余嵌套,Widget树层级浅
- 可读性:代码结构与视觉结构完全一致,新人可快速理解布局逻辑
- 扩展性:如需新增“旋转对照”“缩放对照”,可直接在Row中新增Column
6. 用 BorderRadius.only 表达镜像:这是这页最巧的地方
没有使用图片/自定义绘制,而是用“单色方块+定向圆角”表达镜像,设计巧思拆解:
6.1 原图圆角的设计逻辑
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40.r),
bottomRight: Radius.circular(40.r),
),
- 对角圆角:选择左上+右下,形成“对角线”视觉特征,易识别
- 圆角半径:40.r=80.w/2,形成半圆角,强化视觉特征
- 无其他装饰:纯色块+圆角,避免纹理/渐变干扰核心差异
6.2 镜像圆角的反向设计
borderRadius: BorderRadius.only(
topRight: Radius.circular(40.r),
bottomLeft: Radius.circular(40.r),
),
- 精准反转:仅交换左右方向的圆角位置,严格匹配“水平镜像”的物理逻辑
- 最小变更:仅修改两个圆角的位置参数,其余属性完全复用,符合“DRY”原则
- 直观易懂:即使无文字标注,用户也能通过圆角位置感知“镜像”关系
6.3 该设计的产品价值
- 开发成本:无需设计/切图,纯代码实现,降低跨团队协作成本
- 性能:纯Widget渲染,比图片/自定义绘制更高效,无内存占用问题
- 适配性:矢量级别的图形,缩放/适配不同屏幕无模糊问题
- 可扩展性:如需调整圆角大小/颜色,只需修改参数,无需重新设计
7. 为什么圆角半径是 40.r
方块尺寸为80.w×80.w,圆角半径40.r的设计逻辑:
7.1 尺寸比例的视觉效果
// 图形尺寸
width: 80.w,
height: 80.w,
// 圆角半径
Radius.circular(40.r),
- 比例关系:圆角半径=边长的50%,形成“半圆角”,而非“轻微圆角”
- 视觉特征:半圆角的辨识度远高于小圆角,用户可快速捕捉形状差异
- 边界清晰:80.w的正方形+40.r的半圆角,形状轮廓明确,无模糊地带
7.2 训练场景的适配性
- 辨识度优先:训练页需要用户快速感知差异,40.r的圆角让差异最大化
- 屏幕适配:80.w的尺寸在手机端约占屏幕宽度的20%-30%,不会过大/过小
- 交互友好:图形尺寸足够大,即使是老年用户/小屏设备也能清晰识别
7.3 反例对比:若使用20.r圆角
- 视觉特征弱化:圆角不明显,用户可能无法快速感知镜像差异
- 训练效果降低:需要更多时间观察,违背“快速建立镜像思维”的设计目标
- 一致性破坏:与80.w的尺寸比例不协调,视觉上显得“小气”
8. 为什么两边的颜色一样
两个图形均使用color: Colors.blue,设计决策的核心逻辑:
8.1 颜色选择的原则
decoration: BoxDecoration(
color: Colors.blue, // 左右图形统一颜色
borderRadius: ...,
),
- 无干扰原则:统一颜色避免用户将注意力集中在“颜色差异”,而非“形状差异”
- 品牌一致性:蓝色为App主色调,符合全局视觉规范
- 视觉舒适度:蓝色为冷色调,长时间观察不易疲劳,适合训练场景
8.2 颜色差异化的弊端
- 注意力分散:若左右颜色不同,用户会先关注颜色,而非圆角位置
- 训练目标偏离:镜像识别的核心是“形状反转”,而非“颜色区分”
- 认知负荷增加:多维度差异会提升用户的认知成本,违背“轻量化训练”的定位
8.3 颜色的可扩展设计
若未来需强化提示,可保留颜色统一,仅在“交互反馈”时调整:
Container(
color: userIsCorrect ? Colors.green : Colors.blue,
...
)
- 反馈时机:仅在用户交互后调整颜色,不影响初始的“纯形状对照”
- 情绪引导:绿色=正确、红色=错误,符合用户的认知习惯
- 最小变更:仅修改颜色参数,保持布局/形状不变
9. 文案的位置:标题在上、提示在下
文案布局遵循“先告知、再展示、最后总结”的认知逻辑:
9.1 标题文案:明确训练主题
Text('镜像思维训练',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold
)
)
- 位置:页面顶部,第一眼可见,快速传递页面核心
- 样式:24.sp加粗,视觉层级最高,与其他文案形成明显区分
- 内容:简洁直接,无冗余修饰,符合训练页“高效传递信息”的目标
9.2 说明文案:图形标注
Text('原图', style: TextStyle(fontSize: 16.sp)),
Text('镜像', style: TextStyle(fontSize: 16.sp)),
- 位置:图形正上方,与图形一一对应,视觉关联紧密
- 样式:16.sp常规字体,层级低于标题,高于底部提示
- 内容:单字标注,无多余文字,降低阅读成本
9.3 提示文案:升华训练意义
Text('镜像思维:从不同角度观察问题',
style: TextStyle(
fontSize: 16.sp,
color: Colors.purple
)
)
- 位置:页面底部,图形对照区下方,用户观察完图形后自然阅读
- 样式:紫色文字,与蓝色图形形成温和对比,吸引注意力但不刺眼
- 内容:从“图形镜像”升维到“思维镜像”,点明训练的核心价值
9.4 文案布局的阅读节奏
- 顶部标题:建立认知预期(“这是镜像思维训练”)
- 中间图形+标注:提供视觉素材(“观察原图和镜像的差异”)
- 底部提示:总结核心价值(“镜像思维是从不同角度看问题”)
10. 这页如何扩展成更像“训练”
当前页面为纯展示型,扩展为“用户参与型训练”的两种核心方向:
10.1 方向1:用户判断型训练
核心思路:随机生成图形,让用户判断“哪个是镜像”,需升级为StatefulWidget:
class MirrorRecognitionPage extends StatefulWidget {
const MirrorRecognitionPage({super.key});
State<MirrorRecognitionPage> createState() => _MirrorRecognitionPageState();
}
class _MirrorRecognitionPageState extends State<MirrorRecognitionPage> {
final Map<String, double> _originalCorners = {
'topLeft': 40.r,
'bottomRight': 40.r,
};
int? _userAnswer;
int _correctAnswer = 0;
- 状态管理:新增_originalCorners存储随机图形配置,_userAnswer记录用户选择
- 随机逻辑:初始化时随机生成正确答案索引,增加训练的随机性
- 组件升级:从Stateless→Stateful,适配交互状态的维护
Map<String, double> _getDistractorCorners() {
return {
'topLeft': 40.r,
'bottomLeft': 40.r,
};
}
void _onAnswerSelected(int index) {
setState(() {
_userAnswer = index;
});
}
- 干扰项生成:通过修改部分圆角位置,制造“似是而非”的错误选项
- 状态更新:用户选择后调用setState,更新UI展示结果
- 交互闭环:从“展示”到“选择”再到“反馈”,形成完整训练闭环
10.2 方向2:多样本题库化
核心思路:抽离图形配置为数据,通过列表渲染多组对照样本:
final List<Map<String, Map<String, double>>> _mirrorQuestions = [
{
'original': {'topLeft': 40.r, 'bottomRight': 40.r},
'mirror': {'topRight': 40.r, 'bottomLeft': 40.r},
},
{
'original': {'topRight': 30.r, 'bottomLeft': 30.r},
'mirror': {'topLeft': 30.r, 'bottomRight': 30.r},
},
{
'original': {'topLeft': 50.r, 'bottomLeft': 50.r},
'mirror': {'topRight': 50.r, 'bottomRight': 50.r},
},
];
- 数据结构:统一的图形配置格式,包含原图和镜像的圆角参数
- 题库扩展:新增样本只需添加数据,无需修改UI逻辑
- 渲染逻辑:通过ListView.builder遍历题库,动态生成对照样本
ListView.builder(
itemCount: _mirrorQuestions.length,
itemBuilder: (context, index) {
final question = _mirrorQuestions[index];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildShape(question['original']!),
_buildShape(question['mirror']!),
],
);
},
)
Widget _buildShape(Map<String, double> corners) {
return Container(
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(corners['topLeft'] ?? 0),
topRight: Radius.circular(corners['topRight'] ?? 0),
bottomLeft: Radius.circular(corners['bottomLeft'] ?? 0),
bottomRight: Radius.circular(corners['bottomRight'] ?? 0),
),
),
);
}
- 复用性:_buildShape方法统一构建图形,避免重复代码
- 动态渲染:ListView.builder按需加载,适配大量题库数据
- 扩展性:可新增圆角半径/颜色等参数,支持更多样的图形配置
11. 小结:镜像识别实现的关键点
- 页面足够轻:用
StatelessWidget直接展示,无冗余状态管理 - 对照结构清晰:Row 左右两列对比,spaceEvenly保证均匀分布
- 镜像表达成本低:用
BorderRadius.only对调角位,无需图片/绘制 - 视觉聚焦正确:同色块避免干扰,把注意力留给形状差异
- 布局规范统一:全量使用flutter_screenutil的.w/.h/.sp,保证多端适配
- 文案节奏合理:标题-图形-提示的层级,符合用户认知习惯
12. 这页为什么“看起来简单”但很有价值
镜像识别页没有按钮、没有输入框、没有复杂状态,但核心价值体现在:
- 训练路径起点:作为模式识别的基础训练,为后续复杂训练铺路
- 体验一致性:与其他训练页的设计风格统一,提升产品整体感
- 轻量化设计:符合“思维热身”的定位,不占用用户过多时间
13. 为什么用 Column 而不是 ListView
当前页面选择Column而非ListView的核心原因:
13.1 内容特性适配
Column(
children: [
Text('镜像思维训练'),
SizedBox(height: 24.h),
Row(/* 图形对照区 */),
SizedBox(height: 32.h),
Text('镜像思维:从不同角度观察问题'),
],
)
- 内容高度固定:所有元素高度均可预测,无动态内容/长文本
- 无需滚动:整体高度远低于屏幕高度,Column可完全展示
- 视觉完整性:所有内容在一屏内展示,用户可同时看到标题/图形/提示
13.2 ListView的副作用
- 视觉割裂:滚动可能导致图形对照区离开视野,破坏“对照”的核心逻辑
- 交互冗余:用户无需滚动即可看完内容,ListView的滚动功能无意义
- 性能损耗:ListView的复用逻辑对固定短内容来说是性能浪费
13.3 特殊场景的折中方案
若未来内容扩展,可使用Column+SingleChildScrollView的组合:
SingleChildScrollView(
child: Column(/* 扩展后的内容 */),
)
- 保留Column的布局逻辑,新增滚动能力
- 避免ListView的复用逻辑,降低复杂度
- 保证内容超出屏幕时的可访问性
14. 这页的可维护性:你已经避免了重复代码吗
当前页面左右两块Column存在重复代码,不同规模下的优化策略:
14.1 当前规模(无优化):直接编写重复代码
Column(
children: [
Text('原图', style: TextStyle(fontSize: 16.sp)),
Container(/* 原图图形 */),
],
),
Column(
children: [
Text('镜像', style: TextStyle(fontSize: 16.sp)),
Container(/* 镜像图形 */),
],
),
- 合理性:页面元素少,重复代码量小,抽方法的收益低于成本
- 可读性:代码平铺直叙,新人可直接理解左右列的差异
- 维护成本:修改样式时需同时调整两处,但频率低
14.2 抽离_buildShapeColumn方法
Widget _buildShapeColumn(String title, Map<String, double> corners) {
return Column(
children: [
Text(title, style: TextStyle(fontSize: 16.sp)),
Container(
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(corners['topLeft'] ?? 0),
topRight: Radius.circular(corners['topRight'] ?? 0),
bottomLeft: Radius.circular(corners['bottomLeft'] ?? 0),
bottomRight: Radius.circular(corners['bottomRight'] ?? 0),
),
),
),
],
);
}
- 复用性:通过参数传递标题和圆角配置,适配原图/镜像/扩展图形
- 可维护性:修改样式只需调整方法内部,无需修改调用处
- 扩展性:新增图形类型(如旋转图形),只需新增参数/逻辑
14.3 调用优化后的代码
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildShapeColumn('原图', {'topLeft': 40.r, 'bottomRight': 40.r}),
_buildShapeColumn('镜像', {'topRight': 40.r, 'bottomLeft': 40.r}),
],
)
- 代码量减少:从重复的20+行缩减为2行调用
- 逻辑清晰:调用处仅关注“参数差异”,无需关注布局细节
- 错误减少:避免修改一处忘改另一处的问题
15. 一条很实用的工程细节:16.w 的 padding
全局统一使用EdgeInsets.all(16.w)的工程价值:
15.1 单位规范的重要性
padding: EdgeInsets.all(16.w),
padding: EdgeInsets.all(16),
- 适配性:.w单位基于屏幕宽度的比例,不同设备边距视觉一致
- 一致性:全局统一的边距值,保证所有页面的视觉风格统一
- 可维护性:修改全局边距时,只需调整基础配置,无需逐页修改
15.2 16.w的设计依据
- 移动端设计规范:16dp是移动端最常用的边距值,符合用户阅读习惯
- 视觉舒适度:16.w的边距让内容与屏幕边缘保持足够距离,不拥挤
- 交互友好:避免内容贴边,提升点击/阅读的舒适度
15.3 特殊场景的边距调整
若需局部调整边距,建议基于16.w做增量修改:
SizedBox(height: 16.w * 1.5),
SizedBox(height: 16.w * 2),
- 比例调整:保持与基础边距的比例关系,视觉更协调
- 可维护性:修改基础边距时,相关间距自动同步调整
- 一致性:避免出现无规律的数值(如23.h/31.h),降低调试成本
16. 如果用户看不出镜像差异,怎么增强提示
在不改变核心布局的前提下,新增视觉提示元素:
16.1 箭头标注提示
Widget _buildShapeWithArrow(String title, Map<String, double> corners) {
return Column(
children: [
Text(title, style: TextStyle(fontSize: 16.sp)),
Stack(
alignment: Alignment.topLeft,
children: [
Container(/* 原有图形 */),
// 箭头标注
if (title == '原图')
Icon(Icons.arrow_upward, size: 20.sp, color: Colors.white),
if (title == '镜像')
Icon(Icons.arrow_upward, size: 20.sp, color: Colors.white)
..transform = Matrix4.identity()..translate(60.w, 0),
],
),
],
);
}
- 视觉强化:通过箭头明确指向圆角位置,用户可快速定位差异点
- 不破坏布局:使用Stack叠加箭头,不改变原有图形的尺寸/位置
- 适配性:箭头尺寸使用.sp单位,与图形适配不同屏幕
16.2 动态提示
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('镜像图形的圆角位置与原图左右相反')),
);
},
child: Container(/* 镜像图形 */),
)
- 交互提示:用户点击图形时触发提示,主动探索时获得反馈
- 轻量化:使用SnackBar提示,不占用页面空间,自动消失
- 友好性:避免强制弹窗,尊重用户的阅读节奏
16.3 颜色渐变提示
decoration: BoxDecoration(
gradient: LinearGradient(
begin: title == '原图' ? Alignment.topLeft : Alignment.topRight,
end: title == '原图' ? Alignment.bottomRight : Alignment.bottomLeft,
colors: [Colors.blue, Colors.blue.withOpacity(0.7)],
),
borderRadius: ...,
),
- 渐变方向:与圆角位置一致,强化“方向性”视觉特征
- 不改变颜色:保持蓝色系,避免引入新颜色干扰
- 视觉层次:渐变让形状差异更明显,同时保持简洁
17. 把它做成训练题:最小的交互闭环
最小化改造为“选择-反馈”的交互闭环,新增核心代码:
17.1 状态管理
class _MirrorRecognitionPageState extends State<MirrorRecognitionPage> {
final List<Map<String, dynamic>> _questions = [
{
'original': {'topLeft': 40.r, 'bottomRight': 40.r},
'options': [
{'topRight': 40.r, 'bottomLeft': 40.r},
{'topLeft': 40.r, 'bottomLeft': 40.r},
{'topRight': 40.r, 'bottomRight': 40.r},
],
'correctIndex': 0,
}
];
int _currentQuestion = 0;
int? _selectedOption;
bool _showResult = false;
- 题目配置:包含原图、选项、正确答案索引,数据驱动题目逻辑
- 状态变量:管理当前题目、用户选择、结果展示,覆盖核心交互逻辑
17.2 选项渲染
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(3, (index) {
final option = _questions[_currentQuestion]['options'][index];
return GestureDetector(
onTap: () => _onSelectOption(index),
child: Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: _selectedOption == index ? Colors.grey : Colors.blue,
borderRadius: _getBorderRadius(option),
border: _showResult && index == _questions[_currentQuestion]['correctIndex']
? Border.all(color: Colors.green, width: 2.w)
: null,
),
),
);
}),
)
- 选项样式:选中状态变灰色,正确答案显示绿色边框,反馈直观
- 尺寸调整:选项图形略小于原图(60.w),突出原图的核心地位
- 交互逻辑:点击选项触发选择,更新状态并展示结果
17.3 结果反馈
void _onSelectOption(int index) {
setState(() {
_selectedOption = index;
_showResult = true;
});
final isCorrect = index == _questions[_currentQuestion]['correctIndex'];
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isCorrect ? '回答正确!' : '再试试~'),
backgroundColor: isCorrect ? Colors.green : Colors.red,
),
);
}
ElevatedButton(
onPressed: _showResult
? () => setState(() {
_currentQuestion = (_currentQuestion + 1) % _questions.length;
_selectedOption = null;
_showResult = false;
})
: null,
child: Text('下一题'),
)
- 即时反馈:选择后通过SnackBar提示正确/错误,情绪引导
- 下一题逻辑:结果展示后激活按钮,重置状态进入下一题
- 循环题库:通过取模实现题库循环,无需额外逻辑
18. 从 UI 角度看:为什么 Container 80.w 是合适的
80.w×80.w的Container尺寸设计,符合移动端训练页的UI设计原则:
18.1 尺寸与屏幕的比例
width: 80.w,
height: 80.w,
- 手机端适配:以375w(iPhone 14)为例,80.w约占屏幕宽度的21%,左右并排后占比约42%,预留足够留白
- 平板端适配:比例不变,图形尺寸随屏幕宽度等比放大,保持视觉一致性
- 触控友好:80.w的尺寸足够大,若后续添加点击交互,触控区域充足
18.2 尺寸与圆角的配合
- 80.w的边长 + 40.r的圆角 = 半圆角,视觉特征最大化
- 尺寸过小(如40.w):圆角不明显,差异难以识别
- 尺寸过大(如120.w):左右并排挤压,留白不足,视觉拥挤
18.3 尺寸与布局的协调
- Row+spaceEvenly布局下,80.w的两个图形之间的间距约为屏幕宽度的16%,视觉呼吸感充足
- 与文字的比例:图形尺寸是标题文字(24.sp)的3倍多,视觉上以图形为核心,符合“视觉训练”的定位
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)