Flutter for OpenHarmony 实现 iOS 风格科学计算器:从 UI 到表达式求值的完整解析

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 ';
  • 无缝衔接 :若刚计算完,直接用结果作为新表达式起点:

    dart 复制代码
    if (_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.0000003):

dart 复制代码
_result = tempResult
    .toStringAsFixed(6)           // 保留6位小数
    .replaceAll(RegExp(r'0+$'), '') // 去除末尾零
    .replaceAll(RegExp(r'\.$'), ''); // 去除孤立小数点

3. 支持的科学函数

按钮 功能 Dart 方法
平方根 sqrt()
平方 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_evalmath_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 确保按钮呼吸感。

六、潜在优化方向

  1. 增强表达式引擎

    支持括号、阶乘、π/e 常量,例如:

    dart 复制代码
    // 添加常量替换
    expression = expression.replaceAll('π', pi.toString());
  2. 历史记录功能

    存储最近 10 次计算,通过上滑手势查看。

  3. 横屏扩展模式

    横屏时显示更多科学函数(tan, ln, x³ 等)。

  4. 震动反馈

    关键操作(如按"=")触发 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,
        ),
      ),
    );
  }
}
相关推荐
陈希瑞3 小时前
OpenClaw Chrome扩展使用教程 - 浏览器中继控制
前端·chrome
雨季6663 小时前
Flutter 三端应用实战:OpenHarmony “呼吸灯”——在焦虑时代守护每一次呼吸的数字禅修
开发语言·前端·flutter·ui·交互
切糕师学AI3 小时前
Vue 中如何修改地址栏参数并重新加载?
前端·javascript·vue.js
软弹3 小时前
Vue3如何融合TS
前端·javascript·vue.js
2601_949543013 小时前
Flutter for OpenHarmony垃圾分类指南App实战:资讯详情实现
android·java·flutter
0思必得012 小时前
[Web自动化] Selenium处理动态网页
前端·爬虫·python·selenium·自动化
向哆哆12 小时前
打造高校四六级报名管理系统:基于 Flutter × OpenHarmony 的跨端开发实践
flutter·开源·鸿蒙·openharmony·开源鸿蒙
2501_9400078912 小时前
Flutter for OpenHarmony三国杀攻略App实战 - 设置功能实现
flutter
东东51612 小时前
智能社区管理系统的设计与实现ssm+vue
前端·javascript·vue.js·毕业设计·毕设