☰
Android计算器源码解析:双栈表达式求值与内存操作实现
2026/11/3 23:25:30 网站建设 项目流程

简介:本资源是一份面向Android初学者与移动开发入门者的计算器应用实践文档,聚焦UI布局设计、事件响应逻辑与基础计算功能实现。文档以Android Studio为开发环境,详细解析GridLayout网格布局的列行配置、TextView表达式显示与Button内存操作按钮(MC/MR/MS/M+等)的XML定义,并结合Java代码说明onClick事件处理、对话框跳转及菜单项响应机制,覆盖从界面搭建到交互逻辑的完整开发链路。资源为单文件DOCX格式,大小34KB,内容包含布局代码节选、关键方法注释与开发要点说明,便于快速查阅与代码复用。目前已有1520人学习下载,适合零基础学员理解Android控件绑定、事件驱动编程及简易计算器项目结构,是入门级Android实训的实用参考材料。

1. 这不是“Hello World”式计算器:一个带内存操作、表达式解析、异常防护的完整 Android 计算器源码包,适合刚学完 Activity 生命周期就想动手改真实 UI+逻辑的新手

你可能在 Android Studio 教程里见过那种“点按钮就 setText(1+1)”的玩具计算器——它连小数点都崩,一按“=”就闪退,更别说处理3+4*5的优先级或sqrt(-4)的报错。而这份名为Android studio计算器.docx的资源,实际是一份可直接导入 Android Studio、编译运行、功能完整的计算器工程源码(虽然后缀是 .docx,但内容实为 XML 布局 + Java 主逻辑,常见于早期开发者打包分享习惯)。它包含:带双显示区(表达式+结果)的 GridLayout 界面、28 个功能键完整映射(含 MC/MR/MS/M+/M- 内存操作)、基于运算符优先级栈的表达式求值引擎、6 类明确异常拦截(除零、负数开方、小数取模等)、以及stripTrailingZeros这种细节控件——不是 demo,是能当作业交、能当毕设模块、能真机调试的生产级雏形。如果你正卡在“写了布局但不知道怎么把按钮和计算串起来”,或者想搞懂“为什么我的计算器一输1+2*3就算成 9 而不是 7”,这份源码就是你缺的那块拼图。

2. 项目结构与技术选型:为什么用 GridLayout 而不是 ConstraintLayout?为什么不用 Kotlin?

2.1 工程目录与文件角色定位

虽然资源以.docx形式分发,但实际内容对应标准 Android Studio 工程结构。我们按真实开发视角还原其骨架:

app/ ├── src/main/ │ ├── java/com/example/text2_4/ │ │ └── MainActivity.java ← 核心逻辑:按钮绑定、事件分发、表达式计算 │ ├── res/ │ │ ├── layout/ │ │ │ └── activity_main.xml ← 关键!即你看到的那段 GridLayout XML │ │ └── values/ │ │ └── strings.xml ← 可能含 "MC"/"MR" 等字符串资源(源码未显式给出,但符合规范) │ └── AndroidManifest.xml ← 声明 MainActivity 为启动 Activity

提示:.docx后缀是历史遗留打包方式,实际使用时需将 XML 内容保存为activity_main.xml,Java 代码保存为MainActivity.java,再新建空工程导入。不要试图用 Word 打开运行——这是新手最常翻车的第一步。

2.2 技术栈选择背后的务实逻辑

  • 布局引擎:GridLayout(非 ConstraintLayout)
    源码中android:columnCount="5"和android:rowCount="8"是硬指标。GridLayout 在 2015–2018 年间是计算器类应用的事实标准:它天然适配“5列×N行”的按键矩阵,layout_columnSpan(如btn_0占两列)和layout_rowSpan(如btn_equal占两行)能精准控制宽高比,无需写一堆app:layout_constraint*。ConstraintLayout 虽新,但对这种规则网格反而增加复杂度——这不是技术落后,而是场景匹配。

  • 语言:Java(非 Kotlin)
    全篇extends AppCompatActivity、findViewById()、new Button[28]等均为 Java 8 语法。原因很实在:该代码诞生于 Android Studio 2.x 时代(约 2016–2017),Kotlin 官方支持尚未普及。对新手而言,Java 版本反而更“透明”——没有by lazy、?安全调用等语法糖遮蔽底层逻辑,Stack<Double>直接暴露数据结构意图,方便你逐行 debug 栈操作。

  • 计算引擎:双栈法(Operator-Precedence Parsing)
    不是简单eval(),也不是正则替换。它用Stack<Character>存运算符、Stack<Double>存数字,配合compare[][]优先级表驱动计算流程。这种实现能自然处理3+4*5→ 先压3,+,4,*,读到5后发现*优先级高于+,于是先算4*5=20,再算3+20=23。这是教科书级算法,也是面试高频考点。

2.3 核心模块职责拆解

模块文件位置关键职责新手易忽略点
UI 布局res/layout/activity_main.xml定义 5×8 网格、双 TextView 显示区、28 个 Button 的尺寸/文字/IDtools:ignore="RtlCompat"是为禁用 RTL(从右向左)校验,因计算器数字必须左对齐,此标记防 AS 误报
Activity 主体java/MainActivity.javaonCreate()初始化视图、initial()绑定所有控件、MyOnClickHandler统一处理点击btn[23].setClickable(false)在初始化时禁用 M+/M-,体现“内存未存储前不可用”的业务规则
计算引擎MyOnClickHandler.calculate()解析expression字符串,用双栈执行四则运算、取模、开方等tempExpression = expression + "#"是关键技巧:人为添加结束符,避免循环边界判断
异常体系MyOnClickHandler内部常量DivByZeroError、SqrtByNegative等 6 类错误,全部 throw Exception 并由外层 try-catch 捕获错误信息直接赋给ioView.setText(),无 Toast,符合早期简洁设计

3. 核心代码逐行解析:从按钮点击到3+4*5=23的完整链路

3.1 按钮绑定与事件分发:为什么用数组而非 28 个独立变量?

源码中private Button[] btn = new Button[28];是关键设计。它把 28 个按钮抽象为数组,用下标映射功能:

// MainActivity.java 片段 private void initial() { btn = new Button[28]; // ... findViewById 绑定(省略重复代码) btn[0] = (Button) findViewById(R.id.btn_0); // 下标0 → 数字0 btn[1] = (Button) findViewById(R.id.btn_1); // 下标1 → 数字1 // ... btn[12] = (Button) findViewById(R.id.btn_add); // 下标12 → 加号 btn[13] = (Button) findViewById(R.id.btn_sub); // 下标13 → 减号 // ... btn[26] = (Button) findViewById(R.id.btn_madd); // 下标26 → M+ btn[27] = (Button) findViewById(R.id.btn_msub); // 下标27 → M- MyOnClickHandler myOnClickHandler = new MyOnClickHandler(); for (Button i : btn) i.setOnClickListener(myOnClickHandler); // 一行绑定全部 }

逻辑说明:

  • 用数组而非btn0,btn1... 是为统一事件分发。MyOnClickHandler.onClick(View v)中可通过v.getId()反查下标,进而知道用户点了哪个键。
  • btn[23]到btn[27]对应内存操作键(MC/MR/MS/M+/M-),其下标连续便于条件判断(如if (id >= R.id.btn_mc && id <= R.id.btn_msub))。
  • setClickable(false)针对 M+/M- 是状态驱动 UI:初始时内存为空,M+ 和 M- 无意义,禁用可防误操作。

3.2 表达式构建:input和expression如何协同工作?

计算器有两层状态:用户当前输入的数字(input),和待计算的完整表达式(expression)。它们在onClick中被动态维护:

// MyOnClickHandler.onClick() 简化逻辑(实际代码在 calculate() 前触发) public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.btn_0: case R.id.btn_1: // ... R.id.btn_9 if (resetInputZero) { // 上次按了 = 或 C,需清空输入框 input = "0"; resetInputZero = false; } if (input.equals("0")) { // 避免 "00"、"01" input = String.valueOf(id - R.id.btn_0); // btn_0 ID - btn_0 ID = 0 } else { input += (id - R.id.btn_0); } ioView.setText(input); break; case R.id.btn_dot: if (!input.contains(".")) { // 防止多个小数点 input += "."; ioView.setText(input); } break; case R.id.btn_add: case R.id.btn_sub: case R.id.btn_mul: case R.id.btn_div: case R.id.btn_mod: // 将当前 input 加入 expression,再加运算符 if (!expression.isEmpty() && !expression.endsWith(" ")) { expression += " "; } expression += input + " " + getOperatorChar(id); // 例如 "3 + " input = "0"; // 输入框重置为0,等待下一个数字 expressionOutputView.setText(expression); break; case R.id.btn_equal: expression += input; // 补全最后数字,如 "3 + 4" → "3 + 4" try { double result = calculate(); // 调用核心计算方法 input = String.valueOf(result); ioView.setText(stripTrailingZeros(input)); // 去掉 5.000 → 5.0 expression = ""; // 清空表达式 expressionOutputView.setText(""); setZero = true; // 标记下次输入需清零 } catch (Exception e) { ioView.setText(e.getMessage()); // 显示自定义错误 expression = ""; input = "0"; } break; } }

参数说明:

  • getOperatorChar(id)是辅助方法,将R.id.btn_add映射为'+',R.id.btn_mul映射为'*',确保expression字符串含标准运算符。
  • resetInputZero和setZero是状态标志:按=后再按数字,应覆盖而非追加(如5= → 5,再按2应得2而非52)。
  • stripTrailingZeros()实现如下(源码中已给出):
    private String stripTrailingZeros(String s) { if (s.contains(".")) { s = s.replaceAll("0*$", ""); // 去掉末尾0 s = s.replaceAll("\\.$", ""); // 去掉末尾点 } return s; }
    此处replaceAll("0*$", "")是正则关键:*表示前面字符(0)出现 0 次或多次,$表示行尾,精准匹配末尾连续 0。

3.3 双栈计算引擎:calculate()如何让3+4*5算出 23?

这是整个项目的技术心脏。我们拆解其核心循环:

private void calculate() throws Exception { Stack<Character> opStack = new Stack<>(); // 运算符栈 Stack<Double> numStack = new Stack<>(); // 数字栈 char nowOption; // 当前读到的运算符 char topOption; // 运算符栈顶运算符 int start = 0; // 表达式扫描起始位置 int end; // 下一个运算符位置 int compareResult; // 优先级比较结果 opStack.add('#'); // 压入结束符,作为栈底哨兵 String tempExpression = expression + "#"; // 末尾加#,统一处理 while (start < tempExpression.length()) { // 步骤1:找下一个运算符位置(+,-,*,/,%,#) end = someOfStr_in_anotherStr(operator, tempExpression, start); // 步骤2:提取 start 到 end 之间的数字子串(处理负数) String numStr = tempExpression.substring(start, end).trim(); if (!numStr.isEmpty()) { try { double num = Double.parseDouble(numStr); numStack.push(num); } catch (NumberFormatException e) { throw new Exception(ExpressionError); // 数字格式错误 } } // 步骤3:获取当前运算符 nowOption = tempExpression.charAt(end); // 步骤4:比较栈顶与当前运算符优先级 topOption = opStack.peek(); compareResult = compare[getOperatorIndex(topOption)][getOperatorIndex(nowOption)]; // 步骤5:根据优先级决定动作 if (compareResult == -1) { // 栈顶优先级低 → 当前运算符入栈 opStack.push(nowOption); start = end + 1; // 移动起点到运算符后 } else if (compareResult == 1) { // 栈顶优先级高 → 弹栈计算 char op = opStack.pop(); double b = numStack.pop(); double a = numStack.pop(); double ret = getRet(a, b, op); // 执行 a op b numStack.push(ret); } else if (compareResult == 0) { // 优先级相等(如 # 和 #)→ 计算完成 break; } } if (numStack.size() != 1 || opStack.size() != 1) { throw new Exception(ExpressionError); // 栈未清空,表达式异常 } // 返回最终结果 return numStack.pop(); }

逻辑说明:

  • someOfStr_in_anotherStr()是精妙工具方法:传入"0123456789."(数字字符)和"3+4*5#",从位置 0 开始找+,返回索引 1;再从 2 开始找*,返回索引 3。这比indexOf('+')更鲁棒,因它能同时查多个符号。
  • compare[][]表是核心:compare[0][2]表示+(第0行)对*(第2列)→-1,即+优先级低于*,故*入栈;当读到5后的#,compare[2][5](*对#)为1,触发弹栈计算4*5。
  • getRet(a,b,op)中case '%'的实现是重点:ret = a - ((int)(a/b)) * b是整数取模的标准公式(Java%对负数行为特殊,此处手动实现数学定义)。

4. 避坑指南:新手跑通必踩的 4 个坑及血泪解决方案

4.1 坑1:XML 布局无法加载,报错Binary XML file line #2: Error inflating class GridLayout

  • 现象:AS 编译通过,但运行时报android.view.InflateException,堆栈指向GridLayout。
  • 原因:GridLayout在 Android 4.0+ 原生支持,但若build.gradle中compileSdkVersion过低(如 21),或未在activity_main.xml顶部声明xmlns:android="http://schemas.android.com/apk/res/android",会导致解析失败。源码中虽有该声明,但复制时易遗漏。
  • 解决:检查 XML 第一行是否完整包含<?xml version="1.0" encoding="utf-8"?>和xmlns:android="http://schemas.android.com/apk/res/android";在app/build.gradle中确认compileSdkVersion 33或更高,并同步项目。

4.2 坑2:点击按钮无反应,ioView.setText()不生效

  • 现象:界面显示正常,但按任何键,input_outputView文字不变。
  • 原因:findViewById()返回 null。常见于setContentView(R.layout.activity_main)调用位置错误(应在super.onCreate()后立即调用),或activity_main.xml中TextView的android:id与 Java 代码R.id.input_outputView不一致(如 XML 写成@+id/inputOutputView,Java 写R.id.input_outputView)。
  • 解决:在initial()方法开头加日志Log.d("Calc", "ioView="+ioView);,若输出null,则检查setContentView()是否被注释或位置错误;用 AS 的Navigate → Find Usages查R.id.input_outputView,确认 XML 中 ID 完全匹配。

4.3 坑3:3+4*5计算结果为 35(错误:3+4=7,7*5=35)而非 23

  • 现象:表达式显示正确(3 + 4 * 5),但=后结果错误。
  • 原因:calculate()中compare[][]优先级表配置错误。源码中*行(第2行)全为-1,表示*优先级低于所有运算符,导致+先被弹栈计算。正确应为*行对+、-列为1(高),对*、/、%列为0(等),对#列为1(高)。
  • 解决:核对compare数组定义,确保第2行(*)为{-1,-1,0,0,0,1};第3行(/)同理。可打印compare[2][0](*对+)验证是否为1。

4.4 坑4:小数点输入后,再按数字,input变成0.而非0.5

  • 现象:按0→.→5,ioView显示0.,不显示0.5。
  • 原因:btn_dot的onClick逻辑中,if (!input.contains("."))判断正确,但input += "."后未更新ioView。源码中btn_dot分支缺失ioView.setText(input)。
  • 解决:在case R.id.btn_dot:分支末尾添加ioView.setText(input);。同理检查btn_sign(+/-)分支,需实现input = "-" + input并更新视图。

5. 二次开发实战:给计算器加“历史记录”功能,30 行代码搞定

5.1 需求分析与方案选型

用户需要查看之前算过的3+4=7、sqrt(16)=4等记录。方案有二:

  • 轻量级(推荐):用ArrayList<String>存储历史,在btn_equal的catch块后添加history.add(expression + " = " + result),再用ListView或RecyclerView展示。
  • 持久化:用SharedPreferences保存,重启不丢失。但新手易陷入commit()/apply()区别、线程安全等细节,首推内存方案快速验证。

5.2 代码改造步骤(全程 30 行内)

Step 1:在 MainActivity 成员变量中添加历史列表

// MainActivity.java 顶部 private ArrayList<String> history = new ArrayList<>(); private ListView historyListView; // 用于显示历史

Step 2:在activity_main.xml底部添加 ListView(替换原布局末尾)

<!-- 在 </GridLayout> 后添加 --> <ListView android:id="@+id/historyListView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#f5f5f5" android:padding="8dp" />

注意:需将外层GridLayout改为LinearLayout(android:orientation="vertical"),否则ListView无法占剩余空间。这是布局嵌套的必要调整。

Step 3:在initial()中初始化 ListView 并设置 Adapter

// initial() 方法末尾添加 historyListView = (ListView) findViewById(R.id.historyListView); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, history); historyListView.setAdapter(adapter);

Step 4:在btn_equal的try块末尾添加历史记录

// 在 calculate() 调用后、ioView.setText() 前插入 String record = expression + " = " + stripTrailingZeros(String.valueOf(result)); history.add(0, record); // 插入到开头,最新记录在最上 adapter.notifyDataSetChanged(); // 通知 ListView 更新

Step 5:优化体验——点击历史项可复用

// initial() 中添加 historyListView.setOnItemClickListener((parent, view, position, id) -> { String selected = history.get(position); // 提取等号前的表达式,如 "3 + 4 = 7" → "3 + 4" String expr = selected.split("=")[0].trim(); expression = expr; expressionOutputView.setText(expr); input = "0"; // 重置输入框 ioView.setText("0"); });

5.3 验证与调试技巧

  • 验证点1:按1+2=,检查historyListView是否显示1 + 2 = 3;再按3*4=,是否显示3 * 4 = 12在第一行。
  • 验证点2:点击1 + 2 = 3,检查expressionOutputView是否变为1 + 2,且可继续输入+5=得1+2+5=8。
  • 调试技巧:在history.add()后加Log.d("History", "Added: "+record),用 Logcat 过滤History标签,确认数据写入时机。若notifyDataSetChanged()无效,检查adapter是否被重新创建(应只初始化一次)。

从那以后我每次给 UI 添加新功能,都强制走一遍「布局修改 → findViewById 绑定 → 数据模型扩展 → Adapter 更新 → 交互回调」这五步闭环,哪怕只是加个背景色。因为计算器这类小项目,90% 的 bug 都出在 View 和 Model 的连接断点上,而不是算法本身。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询