Android期末大作业:简易计算器完整实现与避坑指南

发布时间:2026/10/29 14:16:13
Android期末大作业:简易计算器完整实现与避坑指南

简介:这份资源是面向K12阶段学习者与Android初学者的期末大作业项目包,基于Android Studio实现一个简易计算器应用,帮助读者在完整项目中掌握界面设计、事件监听、计算逻辑与发布流程等入门技能。压缩包共48个文件,约180KB,以12个png界面截图、11个xml布局与配置、7个properties属性文件、3个java源码及gradle构建脚本、jar依赖等为主,覆盖从工程结构到核心代码的完整项目骨架。目前已有4339人学习下载,适合作为课程作业参考。项目围绕数字与运算符按钮、TextView显示屏展开,通过OnClickListener响应点击,并借助CalculatorEngine封装运算、用栈处理运算符优先级,还涉及模拟器调试与APK打包发布,读者可据此理解Android应用从布局到逻辑再到测试的完整开发链路,提升编程与问题解决能力。

1. 从一份“能跑起来”的安卓期末大作业说起

每年学期末,总有人抱着 Android Studio 的报错截图到处问:明明照着教程敲了,为什么一运行就闪退?这份简易计算器项目就是为这类场景准备的——它把安卓入门阶段最该练的东西压缩进一个能编译、能安装、能点出结果的完整工程里。适合谁?刚学完 Java 语法、想拿一个真实可运行的 Android 工程练手的同学,以及需要一份结构清晰、方便改造成自己作业底稿的开发者。它不追求功能堆砌,核心就是四则运算加一个干净的界面,但恰恰是这种小工程,最能暴露你对 Activity 生命周期、布局约束和事件绑定的理解到底扎不扎实。下面我按“它长什么样、怎么跑、怎么改”的顺序,把这个工程拆开讲。

2. 工程结构与技术选型:一个计算器背后的最小闭环

2.1 目录结构:先认清每个文件在干什么

拿到一个 Android Studio 工程,别急着点运行,先把目录扫一遍。这个计算器项目的结构是标准的单模块 App 工程,没有引入任何第三方依赖,全部基于 Android SDK 原生能力。常见做法是下面这套骨架:

CalculatorApp/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/calculator/ │ │ │ └── MainActivity.java # 唯一的 Activity,承载全部交互逻辑 │ │ ├── res/ │ │ │ ├── layout/ │ │ │ │ └── activity_main.xml # 计算器界面布局 │ │ │ ├── values/ │ │ │ │ ├── colors.xml # 按钮与背景配色 │ │ │ │ ├── strings.xml # 文案资源 │ │ │ │ └── themes.xml # 主题 │ │ │ └── mipmap-*/ # 启动图标 │ │ └── AndroidManifest.xml # 声明 Activity 与启动入口 │ └── build.gradle # 模块级构建配置 └── build.gradle # 工程级构建配置

这个结构里真正需要你动手的只有三处:MainActivity.java写逻辑,activity_main.xml画界面,build.gradle管编译版本。其余文件基本是模板生成的,改不改都不影响跑起来。很多新手翻车就翻在把注意力全放在build.gradle的版本号上,结果界面和逻辑一个都没写对。

2.2 技术栈:为什么用原生 View 而不是 Compose

这个工程用的是传统 View 体系加 Java,没有用 Kotlin,也没有用 Jetpack Compose。原因很实际:期末大作业的评分环境往往要求工程能在较老版本的 Android Studio 上打开,而 Compose 对 AGP 和 Kotlin 版本有硬性要求,版本一错就是一片红。原生 View 加 Java 的组合兼容性最好,minSdk设到 21 就能覆盖绝大多数教学用模拟器。

布局上用的是LinearLayout嵌套GridLayout的思路:外层垂直排列“显示区”和“键盘区”,键盘区用GridLayout按 4 列排布数字与运算符按钮。这种写法比ConstraintLayout更好理解,虽然灵活性差一点,但对一个 4×5 的按钮矩阵来说完全够用。事件绑定用的是OnClickListener接口,所有按钮共用一个监听器,通过view.getId()区分按的是哪个键——这是这个工程里最值得学的一处设计,后面会展开讲。

2.3 构建配置:版本号对齐是跑通的第一道关

build.gradle里最容易出问题的是compileSdk、targetSdk和 AGP 版本的匹配。这个工程按常见教学环境配置,模块级文件大致如下:

android { namespace 'com.example.calculator' compileSdk 33 defaultConfig { applicationId "com.example.calculator" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }

compileSdk 33意味着你需要本机装好对应平台的 SDK;minSdk 21决定了最低能装在 Android 5.0 的设备上。如果你本机的 Android Studio 版本较新,打开时提示 AGP 版本不兼容,改工程级build.gradle里的com.android.tools.build:gradle版本即可,不要动compileSdk,否则可能牵出一串 API 变更。这一步是纯环境问题,和代码逻辑无关,先把它解决掉再谈功能。

3. 核心逻辑拆解:一个按键从点击到出结果的完整链路

3.1 布局文件:显示区与键盘区怎么摆

界面分上下两块。上面是一个TextView负责显示输入和结果,下面是一个GridLayout放 20 个按钮。关键点在于TextView要设置android:gravity="end|bottom",让数字从右往左堆叠,符合计算器的视觉习惯;同时给它一个固定高度和背景,避免输入变长时布局跳动。

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/tvDisplay" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="end|bottom" android:textSize="48sp" android:maxLines="2" android:text="0" /> <GridLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:columnCount="4" android:rowCount="5"> <!-- 按钮在此逐个声明,id 统一命名 btn0~btn9、btnAdd 等 --> </GridLayout> </LinearLayout>

layout_weight把屏幕按 1:2 分给显示区和键盘区,这样在不同尺寸的设备上比例保持一致。maxLines="2"是防止超长输入把显示区撑爆,配合textSize调小可以进一步兜底。按钮的layout_columnWeight和layout_rowWeight都设为 1,保证每个键等宽等高,不会出现某个键被挤扁的情况。

3.2 事件绑定:一个监听器管二十个按钮

如果给每个按钮单独写setOnClickListener,代码会膨胀到没法看。这个工程的做法是让MainActivity实现View.OnClickListener,然后在onCreate里遍历键盘区的所有子 View 统一注册:

public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView tvDisplay; private StringBuilder currentInput = new StringBuilder("0"); private double operandA = 0; private String pendingOp = null; private boolean resetOnNextDigit = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvDisplay = findViewById(R.id.tvDisplay); GridLayout keypad = findViewById(R.id.gridKeypad); for (int i = 0; i < keypad.getChildCount(); i++) { keypad.getChildAt(i).setOnClickListener(this); } } @Override public void onClick(View v) { int id = v.getId(); if (id == R.id.btnClear) { clearAll(); return; } if (id == R.id.btnEquals) { evaluate(); return; } if (id == R.id.btnAdd || id == R.id.btnSub || id == R.id.btnMul || id == R.id.btnDiv) { handleOperator(((Button) v).getText().toString()); return; } appendDigit(((Button) v).getText().toString()); } }

这段代码的核心是onClick里的分发逻辑:先判断是不是功能键(清除、等号),再判断是不是运算符,剩下的都当数字处理。((Button) v).getText()直接取按钮上的文字作为输入,省去了为每个数字键写映射表的麻烦。参数上要注意currentInput用StringBuilder而不是String,因为数字是逐个追加的,用String每次拼接都会产生新对象,虽然在这个量级无所谓,但养成习惯没坏处。

3.3 运算状态机:三个变量撑起四则运算

计算器最容易写错的地方不是加减乘除本身,而是“什么时候该清空输入、什么时候该保留”。这个工程用三个状态变量解决:operandA存第一个操作数,pendingOp存待执行的运算符,resetOnNextDigit标记下一次按数字时是否要清空显示。

private void appendDigit(String digit) { if (resetOnNextDigit) { currentInput.setLength(0); resetOnNextDigit = false; } if (currentInput.toString().equals("0")) { currentInput.setLength(0); } currentInput.append(digit); tvDisplay.setText(currentInput.toString()); } private void handleOperator(String op) { if (pendingOp != null) { evaluate(); // 连续运算时先把上一步算掉 } operandA = Double.parseDouble(currentInput.toString()); pendingOp = op; resetOnNextDigit = true; // 下次按数字重新开始 } private void evaluate() { if (pendingOp == null) return; double operandB = Double.parseDouble(currentInput.toString()); double result = 0; switch (pendingOp) { case "+": result = operandA + operandB; break; case "-": result = operandA - operandB; break; case "×": result = operandA * operandB; break; case "÷": if (operandB == 0) { tvDisplay.setText("错误"); clearAll(); return; } result = operandA / operandB; break; } currentInput.setLength(0); currentInput.append(formatResult(result)); tvDisplay.setText(currentInput.toString()); pendingOp = null; resetOnNextDigit = true; }

appendDigit里先判断resetOnNextDigit,再判断当前是不是 "0",这两层判断的顺序不能反:如果先判 "0" 再判重置标记,那么按完运算符后第一次输入会被错误地当成“替换 0”而不是“重新开始”。handleOperator里先调evaluate是为了支持2 + 3 + 4这种连续运算,否则第二个加号会把operandA覆盖掉。除零判断放在evaluate里而不是onClick里,是因为只有真正执行除法时才知道除数是多少。formatResult是个小工具方法,用DecimalFormat去掉多余的小数位,避免显示成3.0000000001这种。

4. 跑起来与改起来:从导入工程到换一套皮肤

4.1 导入与首次运行:三步走

第一步,用 Android Studio 的 “Open” 选择工程根目录,不要选app子目录,否则 Gradle 同步会找不到工程级配置。第二步,等待 Gradle Sync 完成,如果卡在下载依赖,检查settings.gradle里的仓库配置是否指向可访问的镜像。第三步,创建一个 API 30 左右的模拟器,点绿色三角运行。

首次运行最常见的现象是编译通过但安装失败,提示INSTALL_FAILED_OLDER_SDK,这说明模拟器的 API 等级低于minSdk 21,换一个高版本模拟器即可。另一个高频问题是界面出来了但按钮点不动,八成是GridLayout的id写错,导致findViewById(R.id.gridKeypad)返回 null,遍历子 View 的循环直接空指针。跑之前先在布局文件里确认这个 id 存在且拼写一致。

4.2 换配色与改文案:不动逻辑的改造

想把这个工程改成自己的作业,最省力的切入点是换皮肤。颜色全部集中在colors.xml,把主色、按钮背景色、文字色改掉,界面立刻变样。文案在strings.xml,把按钮上的+、-换成中文的“加”“减”也可以,但要注意handleOperator里的switch分支匹配的是按钮文字,改了文案就得同步改分支条件,否则运算符识别会失效。

<resources> <color name="colorPrimary">#2E7D32</color> <color name="keypadBg">#F5F5F5</color> <color name="keyText">#212121</color> </resources>

改完颜色后记得在activity_main.xml里把按钮的android:backgroundTint或android:background引用到新颜色上,光改colors.xml不会自动生效。这一步是纯资源替换,不涉及 Java 代码,适合在答辩前快速做出差异化。

4.3 加功能:从四则运算到带括号的表达式

如果想把简易计算器升级成能处理(1+2)×3的版本,改动集中在输入解析层。当前实现是“即时求值”,每按一个运算符就算一次,这种模型天然不支持括号优先级。常见做法是引入一个表达式字符串缓冲区,把所有按键都追加进去,等按等号时再用调度场算法(Shunting-yard)转成后缀表达式求值。

// 思路示意:把按键输入累积成表达式,等号时统一解析 private StringBuilder expression = new StringBuilder(); private void onEqualsPressed() { String expr = expression.toString(); if (expr.isEmpty()) return; try { double result = ExpressionEvaluator.evaluate(expr); // 自行实现的解析器 tvDisplay.setText(formatResult(result)); expression.setLength(0); expression.append(formatResult(result)); } catch (Exception e) { tvDisplay.setText("表达式错误"); expression.setLength(0); } }

ExpressionEvaluator需要你自己写,核心是维护一个运算符栈和一个输出队列,遇到数字直接输出,遇到运算符按优先级弹栈。这个改造的工作量比换皮肤大得多,但它是从“期末作业”迈向“能用的计算器”的关键一步。改的时候建议保留原来的即时求值逻辑作为降级方案,新解析器单独放一个类,出问题能快速切回去。

5. 避坑与排查:那些让工程跑不起来的细节

5.1 现象:一运行就闪退,Logcat 报 NullPointerException

原因通常是findViewById拿到的 View 为 null。这个工程里最可能出错的是tvDisplay或gridKeypad的 id 在布局里拼错,或者setContentView加载的布局文件根本不是你以为的那个。解决方法是打开 Logcat 看崩溃堆栈的第一行,定位到具体行号,再回布局文件核对 id。养成习惯:findViewById之后立刻用一下这个 View(比如设个文本),如果这里就崩,说明 id 有问题,比等到后面逻辑里崩更好排查。

5.2 现象:按钮有按压效果但数字不显示

原因多半是appendDigit里的resetOnNextDigit状态没复位,或者currentInput被意外清空。检查handleOperator里是否在设置resetOnNextDigit = true之后又调了appendDigit,导致刚清空又被重置逻辑覆盖。另一个可能是tvDisplay.setText被调用但传入的是空串,用Log.d在appendDigit入口打印currentInput和resetOnNextDigit的值,一眼就能看出状态对不对。

5.3 现象:连续运算结果不对,比如 2+3×4 算出 20

这是即时求值模型的固有局限,不是 bug。当前实现每遇到运算符就结算前一步,所以2+3×4会先算2+3=5,再算5×4=20。如果作业要求支持优先级,必须换成第 4.3 节说的表达式解析方案。如果只是演示四则运算,在说明文档里写清楚“按顺序即时计算”即可,别硬扛。

5.4 现象:Gradle Sync 失败,提示找不到 SDK 或版本不匹配

先看报错里提到的具体版本号,再去 SDK Manager 里确认对应平台是否已安装。如果是 AGP 版本和 Android Studio 版本不匹配,改工程级build.gradle的插件版本,不要盲目升级compileSdk。改完执行一次 “Clean Project” 再 Sync,缓存残留也会导致改了配置不生效。

5.5 现象:模拟器上显示正常,真机上按钮错位

GridLayout在不同厂商的 ROM 上对layout_columnWeight的支持有差异,部分设备会忽略权重导致按钮挤在一起。稳妥做法是给每个按钮显式设置layout_width="0dp"配合layout_columnWeight="1",并在GridLayout上设android:useDefaultMargins="false"。如果还不行,退回到LinearLayout嵌套四行水平布局,兼容性最好,代价是代码稍微啰嗦一点。

6. 二次开发的方向与验证习惯

这个工程的改造空间比它看起来大。最直接的一条路是把MainActivity里的运算逻辑抽成一个独立的CalculatorEngine类,Activity 只负责收按键、调引擎、刷显示。抽出来之后,引擎可以脱离 Android 环境用纯 JUnit 测试,2+3、除零、连续运算这些用例跑一遍,比在模拟器上手动点一百次靠谱得多。我一般会先写三五个断言把边界情况钉死,再动界面,这样改布局的时候心里有底。

第二条路是加历史记录。用一个RecyclerView或者简单的ScrollView加TextView堆叠,每次evaluate成功后把表达式和结果追加进去。数据不用持久化,放一个ArrayList<String>里就够,进程被杀就清空,符合计算器的使用预期。真要持久化,SharedPreferences存个 JSON 字符串即可,别上数据库,杀鸡用牛刀。

第三条路是适配深色模式。在values-night目录下放一套colors.xml,把背景和文字色反过来,系统切换主题时自动生效。这一步几乎不用改 Java 代码,但能让作业在演示时多一个亮点。注意深色模式下按钮的backgroundTint要单独调,直接复用浅色值会显得刺眼。

验证改造是否成功,我的习惯是固定跑一套手动用例:0开头输入、连续按运算符、除零、超长数字、旋转屏幕。旋转屏幕这一项特别容易被忽略——默认情况下 Activity 重建会丢失currentInput和pendingOp,表现就是转到横屏后显示归零。要修的话在onSaveInstanceState里存这几个状态,onCreate里取回来,代码不到十行,但能省掉答辩时被问住的尴尬。

从那以后我每次交 Android 作业前,都强制自己在真机上把旋转、切后台、快速连点这三件事各走一遍,很多在模拟器上看不出来的状态问题都是这么暴露的。希望帮到你。

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