Flutter for OpenHarmony 实现 iOS 风格科学计算器:从 UI 到表达式求值的完整解析
在移动应用开发中,计算器是一个看似简单却极具挑战性的功能模块。它不仅要求界面美观、交互流畅,还需要具备强大的数学运算能力。本文将深入剖析一段完整的
Flutter 代码,展示如何构建一个兼具 iOS 设计风格与科学计算能力的高级计算器应用。
完整效果展示


一、整体架构设计
1. 三层结构清晰分离
- UI 层 :
CalculatorScreen负责界面布局和用户交互; - 逻辑层 :状态管理(
_expression,_result,_shouldReset)处理数据流转; - 计算层 :自研
_evaluateExpression方法实现基础四则运算,配合 Dart 内置math库完成科学函数。
💡 这种分层使得代码易于维护和扩展------未来若需支持括号或更复杂表达式,只需修改计算层即可。
2. 按钮配置驱动 UI
通过 _buttonRows 二维列表定义所有按钮:
dart
final List<List<Map<String, dynamic>>> _buttonRows = [
[{'label': 'AC', 'type': 'command', 'color': Colors.grey}, ...],
...
];

- 类型区分 :
digit(数字)、operator(运算符)、command(命令)、scientific(科学函数);- 颜色编码:灰色(数字/命令)、橙色(运算符)、蓝色(科学函数),符合 iOS 视觉规范;
- 动态渲染 :
GridView.builder根据配置自动生成 5×5 网格布局。
二、核心交互逻辑详解
1. 智能结果显示机制
通过 _shouldReset 标志位控制输入行为:
dart
bool _shouldReset = false;
void _handleDigit(String label) {
if (_result == '0' || _shouldReset) {
_result = label; // 替换而非追加
_shouldReset = false;
} else {
_result += label;
}
}

- 场景1:刚计算完(如按"="后),输入新数字会清空旧结果;
- 场景2:按下运算符后,下一次数字输入自动重置显示区;
- 防错设计 :禁止连续输入多个小数点(
if (_result.contains('.')) return;)。
2. 表达式构建策略
-
实时记录 :每次按下运算符时,将当前结果追加到
_expression:dart_expression += '$_result $label '; -
无缝衔接 :若刚计算完,直接用结果作为新表达式起点:
dartif (_shouldReset) { _expression = '$_result $label '; }
3. 命令按钮精准控制
| 按钮 | 功能 | 实现细节 |
|---|---|---|
AC |
全清 | 重置 _expression 和 _result |
C |
清当前 | 仅重置 _result = '0' |
± |
正负切换 | 字符串前缀操作('-$_result') |
% |
百分比 | 数值除以 100 |
✅ 特别注意:
%按钮直接作用于当前显示值,而非整个表达式,符合用户直觉。
三、科学计算功能实现
1. 三角函数角度转换
Dart 的 sin()/cos() 使用弧度制,但用户习惯输入角度:
dart
tempResult = sin(value * pi / 180); // 角度 → 弧度
2. 结果格式化技巧
避免显示冗余零(如 3.000000 → 3):
dart
_result = tempResult
.toStringAsFixed(6) // 保留6位小数
.replaceAll(RegExp(r'0+$'), '') // 去除末尾零
.replaceAll(RegExp(r'\.$'), ''); // 去除孤立小数点

3. 支持的科学函数
| 按钮 | 功能 | Dart 方法 |
|---|---|---|
√ |
平方根 | sqrt() |
x² |
平方 | pow(x, 2) |
sin/cos |
三角函数 | sin(), cos() |
log |
自然对数 | log() |
⚠️ 注意:
log在此实现为自然对数(ln),若需常用对数(log₁₀)应使用log(value) / ln(10)。
四、自研表达式求值器深度解析
由于未引入第三方库(如 math_expression),作者实现了轻量级求值器:
1. 分步解析策略
-
第一步:词法分析
将字符串拆分为数字和运算符令牌(Tokens):dart// "3 + 4 × 2" → [3, '+', 4, '×', 2] -
第二步:优先级处理
先遍历处理×/÷(高优先级),再处理+/−(低优先级); -
第三步:顺序计算
从左到右执行加减运算。
2. 关键代码片段
dart
// 处理乘除(从左到右)
for (int i = 0; i < tokens.length; i++) {
if (tokens[i] == '×') {
tokens[i-1] = tokens[i-1] * tokens[i+1]; // 计算结果存回前一位
tokens.removeAt(i); // 移除运算符
tokens.removeAt(i); // 移除后一位数字
i--; // 保持索引正确
}
// ... 除法类似
}
// 处理加减
double result = tokens[0];
for (int i = 1; i < tokens.length; i += 2) {
if (tokens[i] == '+') result += tokens[i+1];
else if (tokens[i] == '-') result -= tokens[i+1];
}

3. 局限性说明
- 不支持括号 :无法处理
(1+2)×3; - 无错误恢复:除零等异常直接抛出"错误";
- 生产建议 :实际项目应使用
dart_eval或math_expressions库。
五、UI/UX 设计亮点
1. iOS 风格还原
- 深色背景 :
Colors.black主色调; - 圆角按钮 :
BorderRadius.circular(50)模拟 iOS 按钮; - 字体层级 :
- 表达式:32px 灰色(次要信息);
- 结果:48px 加粗(核心焦点)。
2. 按钮尺寸差异化
dart
if (label == '0') fontSize = 32; // 数字0更大
else if (['1','2',...].contains(label)) fontSize = 24;
else fontSize = 28; // 运算符/科学函数
- 视觉引导 :重要操作(如
0、=)通过尺寸强化; - 一致性:同类型按钮保持相同字号。
3. 响应式布局
- 弹性分区:顶部公式(1份)、结果(2份)、按钮(7份);
- 网格间距 :
crossAxisSpacing: 8, mainAxisSpacing: 8确保按钮呼吸感。
六、潜在优化方向
-
增强表达式引擎
支持括号、阶乘、π/e 常量,例如:
dart// 添加常量替换 expression = expression.replaceAll('π', pi.toString()); -
历史记录功能
存储最近 10 次计算,通过上滑手势查看。
-
横屏扩展模式
横屏时显示更多科学函数(tan, ln, x³ 等)。
-
震动反馈
关键操作(如按"=")触发
HapticFeedback提升触感。
结语:小工具中的大智慧
这个计算器项目虽小,却凝聚了状态管理、算法设计、UI 实现三大核心能力。它证明了 Flutter 不仅能构建精美界面,更能处理复杂的业务逻辑。对于初学者,它是绝佳的练手项目;对于资深开发者,其自研求值器的设计思路也值得借鉴。
🌐 加入社区
欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持:
👉 开源鸿蒙跨平台开发者社区
完整代码
bash
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const CalculatorApp());
}
class CalculatorApp extends StatelessWidget {
const CalculatorApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'iOS 计算器',
theme: ThemeData.dark(),
home: const CalculatorScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class CalculatorScreen extends StatefulWidget {
const CalculatorScreen({super.key});
@override
State<CalculatorScreen> createState() => _CalculatorScreenState();
}
// 按钮类型枚举
enum ButtonType { digit, operator, command, scientific }
class _CalculatorScreenState extends State<CalculatorScreen> {
// 当前显示的数字和公式
String _expression = '';
String _result = '0';
// 按钮配置数据 (label, type, color)
final List<List<Map<String, dynamic>>> _buttonRows = [
[
{'label': 'AC', 'type': 'command', 'color': Colors.grey},
{'label': '±', 'type': 'command', 'color': Colors.grey},
{'label': '%', 'type': 'command', 'color': Colors.grey},
{'label': '÷', 'type': 'operator', 'color': Colors.orange},
{'label': '√', 'type': 'scientific', 'color': Colors.blue},
],
[
{'label': '7', 'type': 'digit', 'color': Colors.grey},
{'label': '8', 'type': 'digit', 'color': Colors.grey},
{'label': '9', 'type': 'digit', 'color': Colors.grey},
{'label': '×', 'type': 'operator', 'color': Colors.orange},
{'label': 'x²', 'type': 'scientific', 'color': Colors.blue},
],
[
{'label': '4', 'type': 'digit', 'color': Colors.grey},
{'label': '5', 'type': 'digit', 'color': Colors.grey},
{'label': '6', 'type': 'digit', 'color': Colors.grey},
{'label': '−', 'type': 'operator', 'color': Colors.orange},
{'label': 'sin', 'type': 'scientific', 'color': Colors.blue},
],
[
{'label': '1', 'type': 'digit', 'color': Colors.grey},
{'label': '2', 'type': 'digit', 'color': Colors.grey},
{'label': '3', 'type': 'digit', 'color': Colors.grey},
{'label': '+', 'type': 'operator', 'color': Colors.orange},
{'label': 'cos', 'type': 'scientific', 'color': Colors.blue},
],
[
{'label': '0', 'type': 'digit', 'color': Colors.grey},
{'label': '.', 'type': 'digit', 'color': Colors.grey},
{'label': '=', 'type': 'operator', 'color': Colors.orange},
{'label': 'log', 'type': 'scientific', 'color': Colors.blue},
{'label': 'C', 'type': 'command', 'color': Colors.grey},
],
];
// 处理按钮点击
void _buttonPressed(String label, String type) {
setState(() {
if (type == 'digit') {
_handleDigit(label);
} else if (type == 'operator') {
_handleOperator(label);
} else if (type == 'command') {
_handleCommand(label);
} else if (type == 'scientific') {
_handleScientific(label);
}
});
}
void _handleDigit(String label) {
// 防止连续输入多个小数点
if (label == '.' && _result.contains('.')) return;
// 如果当前显示的是 0 或者刚计算完,直接替换
if (_result == '0' || _shouldReset) {
_result = label;
_shouldReset = false;
} else {
_result += label;
}
}
void _handleOperator(String label) {
if (label == '=') {
_calculateResult();
} else {
// 如果刚计算完,把结果作为新的表达式开始
if (_shouldReset) {
_expression = '$_result $label ';
} else {
_expression += '$_result $label ';
}
_shouldReset = true;
}
}
void _handleCommand(String label) {
switch (label) {
case 'AC':
_expression = '';
_result = '0';
break;
case 'C': // 清除当前数字
_result = '0';
break;
case '±':
if (_result != '0') {
_result =
_result.startsWith('-') ? _result.substring(1) : '-$_result';
}
break;
case '%':
double value = double.tryParse(_result) ?? 0;
_result = (value / 100).toString();
break;
}
_shouldReset = false;
}
// 标记是否需要重置结果区(例如按下运算符后)
bool _shouldReset = false;
void _handleScientific(String label) {
double value = double.tryParse(_result) ?? 0;
double tempResult = 0;
switch (label) {
case '√':
tempResult = sqrt(value);
break;
case 'x²':
tempResult = pow(value, 2).toDouble();
break;
case 'sin':
tempResult = sin(value * pi / 180); // 转换为角度
break;
case 'cos':
tempResult = cos(value * pi / 180);
break;
case 'log':
tempResult = log(value);
break;
}
_result = tempResult
.toStringAsFixed(6)
.replaceAll(RegExp(r'0+$'), '')
.replaceAll(RegExp(r'\.$'), '');
_shouldReset = true;
}
void _calculateResult() {
try {
// 构建完整的表达式字符串
String fullExpression = '$_expression$_result';
// 替换符号为 Dart 可识别的运算符
fullExpression = fullExpression.replaceAll('×', '*').replaceAll('÷', '/');
// 使用简单的 eval 逻辑 (这里使用 Function 代替第三方库)
// 注意:在生产环境中,建议使用 math_expression 或 dart_eval 库
final result = _evaluateExpression(fullExpression);
_result = result.toString();
_expression = '';
_shouldReset = true;
} catch (e) {
_result = '错误';
}
}
// 简单的表达式求值器 (不使用外部库)
double _evaluateExpression(String expression) {
// 这是一个非常简化的版本,仅支持基础四则运算
// 将字符串转换为 List<Token>
final tokens = <dynamic>[];
String currentNumber = '';
for (var char in expression.split('')) {
if (RegExp(r'[0-9.]').hasMatch(char)) {
currentNumber += char;
} else {
if (currentNumber.isNotEmpty) {
tokens.add(double.parse(currentNumber));
currentNumber = '';
}
if (char != ' ') {
tokens.add(char);
}
}
}
if (currentNumber.isNotEmpty) {
tokens.add(double.parse(currentNumber));
}
// 先处理乘除
for (int i = 0; i < tokens.length; i++) {
if (tokens[i] == '×' || tokens[i] == '*') {
tokens[i - 1] = tokens[i - 1] * tokens[i + 1];
tokens.removeAt(i);
tokens.removeAt(i);
i--;
} else if (tokens[i] == '÷' || tokens[i] == '/') {
tokens[i - 1] = tokens[i - 1] / tokens[i + 1];
tokens.removeAt(i);
tokens.removeAt(i);
i--;
}
}
// 再处理加减
double result = tokens[0];
for (int i = 1; i < tokens.length; i += 2) {
if (tokens[i] == '+') {
result += tokens[i + 1];
} else if (tokens[i] == '-') {
result -= tokens[i + 1];
}
}
return result;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
// 顶部显示区域 (公式)
Expanded(
flex: 1,
child: Container(
alignment: Alignment.bottomRight,
padding: const EdgeInsets.all(20),
child: Text(
_expression,
style: const TextStyle(fontSize: 32, color: Colors.grey),
),
),
),
// 底部显示区域 (结果)
Expanded(
flex: 2,
child: Container(
alignment: Alignment.bottomRight,
padding: const EdgeInsets.all(20),
child: Text(
_result,
style:
const TextStyle(fontSize: 48, fontWeight: FontWeight.w600),
),
),
),
// 按钮区域
Expanded(
flex: 7,
child: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _buttonRows.expand((row) => row).length,
itemBuilder: (context, index) {
final row = (index / 5).floor();
final col = index % 5;
final button = _buttonRows[row][col];
return CalculatorButton(
label: button['label'],
color: button['color'],
onPressed: () =>
_buttonPressed(button['label'], button['type']),
);
},
),
),
],
),
);
}
}
// 自定义按钮组件
class CalculatorButton extends StatelessWidget {
final String label;
final Color color;
final VoidCallback onPressed;
const CalculatorButton({
super.key,
required this.label,
required this.color,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
// 根据按钮位置决定大小
double fontSize;
if (label == '0') {
fontSize = 32; // 数字0更大
} else if ([
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'.',
'AC',
'±',
'%',
'C'
].contains(label)) {
fontSize = 24; // 普通数字和命令
} else {
fontSize = 28; // 运算符和科学函数
}
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
padding: EdgeInsets.zero,
),
onPressed: onPressed,
child: Text(
label,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w500,
),
),
);
}
}