安卓期末大作业:Android Studio简易计算器从布局到求值全指南 简介面向K12阶段Android开发初学者的期末大作业资源包以简易计算器为完整项目载体帮助解决编程入门课缺少完整实践项目的难题。压缩包共48个文件总大小180KB其中包含12个png界面截图、11个xml布局资源、3个java业务源码、7个properties配置以及gradle构建脚本等目录结构完整可直接导入Android Studio研读运行。项目覆盖从界面设计到发布的完整流程在activity_main.xml中定义数字与运算符按钮及显示框在MainActivity.java里为按钮逐一注册OnClickListener点击后更新显示屏并暂存操作数与运算符将计算逻辑单独封装为CalculatorEngine借助栈处理运算符优先级体现LIFO原则支持连续运算最终通过setText展示结果模拟器与Logcat辅助定位错误。另附proguard-rules.pro、gradlew等构建文件支持快速打包APK。已有4331人学习下载适合初学者把零散知识点串成可运行应用也可作为期末作业或课程设计的参考模板。1. 安卓期末大作业Android Studio 简易计算器别以为它只是一排按钮选“Android Studio 简易计算器”做期末大作业的同学大多以为这是所有题目里最省事的一个几个按钮加一个显示框照着课本敲一遍就交差。实际动起手来你会发现这玩意儿麻雀虽小五脏俱全Activity生命周期、布局约束、事件分发、字符串解析全都要碰一遍踩坑密度比做一个普通的列表页高出不少。我用这个项目带过不少初学者也帮人改过不少答辩前翻车的代码。今天这篇就按我平时自己搭这类项目的顺序把界面布局、按键分发、表达式求值和答辩前必查的坑一次讲透。看完你不仅能交出一个能跑的工程还能在老师追问“为什么这么写”的时候说出个一二三来。2. 项目骨架与按键布局先用 GridLayout 把面板钉死2.1 用 GridLayout 搭计算器面板XML 全量清单计算器最合适的布局不是 LinearLayout 一层层嵌套而是 GridLayout。网格布局天然支持几行几列的均匀排布代码量最少也最好调整。我常用的面板结构是顶部一个显示用的 TextView下面一个 4 列 5 行的 GridLayout按钮按标准计算器顺序排开。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding12dp TextView android:idid/tvDisplay android:layout_widthmatch_parent android:layout_height80dp android:gravityend|center_vertical android:maxLines1 android:text0 android:textSize40sp android:textStylebold / GridLayout android:idid/gridLayout android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:columnCount4 android:rowCount5 Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:textC android:textSize22sp android:tagclear / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text⌫ android:textSize22sp android:tagbackspace / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text÷ android:textSize22sp android:tagdiv / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text× android:textSize22sp android:tagmul / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text7 android:tag7 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text8 android:tag8 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text9 android:tag9 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text- android:textSize22sp android:tagsub / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text4 android:tag4 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text5 android:tag5 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text6 android:tag6 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text android:textSize22sp android:tagadd / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text1 android:tag1 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text2 android:tag2 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text3 android:tag3 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_margin4dp android:text android:textSize22sp android:tagequals / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_columnSpan2 android:layout_margin4dp android:text0 android:tag0 / Button android:layout_width0dp android:layout_heightmatch_parent android:layout_columnWeight1 android:layout_columnSpan2 android:layout_margin4dp android:text. android:textSize22sp android:tagdot / /GridLayout /LinearLayout这段布局里有两个关键点。第一按钮的layout_width0dp配合layout_columnWeight1让四个按钮等分每一行的宽度不会因为文字长短出现某个按钮特别宽的情况。第二最下面一行的“0”和“.”都设置了layout_columnSpan2各自占两列面板底部的视觉重心是稳的不会有空一格的突兀感。layout_margin4dp是给按钮之间留缝隙手指点击时不容易误触相邻键。tag属性现在不起作用但它是下一步事件分发的地基。所有按钮共用一套点击逻辑靠 tag 区分身份这里先埋好。2.2 给每个按钮贴上 tag事件派发的前提计算器有 18 个按钮新手最常见的写法是给每个按钮写一个findViewById再写一个setOnClickListener。一个按钮三行18 个按钮就是 54 行样板代码。我一般不会这么干而是在布局阶段就用android:tag给每个按钮一个字符串标识在 Java 代码里统一读取 tag再根据 tag 分发到不同的处理分支。这里给出一份 tag 规划表写代码之前先对着表检查一遍能少踩很多漏标签的坑。按钮文字tag 值类型作用0 - 90 到 9数字追加到表达式末尾.dot小数点追加小数点add运算符追加加号-sub运算符追加减号×mul运算符追加乘号÷div运算符追加除号equals功能触发求值Cclear功能清空所有输入⌫backspace功能删除最后一个字符注意数字按钮的 tag 我直接用了数字本身这样解析时不需要再做一次字母到数字的映射。运算符按钮没有直接用符号做 tag是因为java:switch对字符串做匹配没问题但符号加上中文注释在团队项目里容易让人看花眼用add、sub这类简短英文更清晰。2.3 用 dimens 和 style 统一按钮尺寸布局文件里每个按钮都写layout_heightmatch_parent和layout_width0dp重复性太高。实际工程里我会把按钮公共属性抽到一个样式里布局文件只保留文本和 tag这样谁改样式都只动一处。先建res/values/dimens.xmlresources dimen namebtn_margin4dp/dimen dimen namebtn_text_size22sp/dimen dimen namedisplay_height80dp/dimen dimen namedisplay_text_size40sp/dimen /resources再建res/values/styles.xmlresources style nameCalcButton parentWidget.AppCompat.Button item nameandroid:layout_width0dp/item item nameandroid:layout_heightmatch_parent/item item nameandroid:layout_columnWeight1/item item nameandroid:layout_margindimen/btn_margin/item item nameandroid:textSizedimen/btn_text_size/item /style /resources然后把布局里每个按钮冗长的属性删掉只留stylestyle/CalcButton、android:text和android:tag。比如数字键 7 就变成Button stylestyle/CalcButton android:text7 android:tag7 /这里要提醒一句android:layout_columnWeight写在 style 里是有效的但前提是你用的是原生 GridLayout 且 minSdk 不低于 21。现在 Android Studio 新建项目的 minSdk 基本都是 24 以上可以直接用。如果还在用兼容库那就只能老老实实把权重写在布局里别硬抽样式。3. 事件监听与输入状态一个监听器管全部按键3.1 遍历 GridLayout 一次性绑定监听器布局文件已经用 GridLayout 包住了所有按钮Java 侧就可以直接遍历 GridLayout 的子 View一次性给所有按钮绑定同一个点击监听器。这是替代 18 个setOnClickListener的标准做法代码量从几十行压到不到十行。public class MainActivity extends AppCompatActivity { private TextView tvDisplay; private StringBuilder expression new StringBuilder(); private boolean justCalculated false; private String lastExpression; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvDisplay findViewById(R.id.tvDisplay); setupCalculatorButtons(); } private void setupCalculatorButtons() { GridLayout gridLayout findViewById(R.id.gridLayout); View.OnClickListener listener new View.OnClickListener() { Override public void onClick(View v) { Object tag v.getTag(); if (tag ! null) { handleInput(tag.toString()); } } }; for (int i 0; i gridLayout.getChildCount(); i) { View child gridLayout.getChildAt(i); if (child instanceof Button) { child.setOnClickListener(listener); } } } }getChildCount()拿到 GridLayout 里所有直接子 View 的数量遍历后判断类型只给 Button 绑定监听。这一步很关键因为 GridLayout 里没有别的东西但如果以后你往里面塞 TextView 或者其他控件这个instanceof Button能挡住误绑。所有按钮共用一个监听器实例内存上只创建了一个对象代码上想给某个按钮加个点击音效或者震动只需要在handleInput里加一个分支不用再回头找那个按钮的监听器。3.2 tag 解析把按键意图转成动作监听器拿到 tag 后就要根据 tag 决定按键行为。这里我直接写一个handleInput(String tag)方法整个计算器的按键逻辑全部收敛在这个方法里。private void handleInput(String tag) { switch (tag) { case clear: expression.setLength(0); tvDisplay.setText(0); justCalculated false; break; case backspace: if (expression.length() 0) { expression.deleteCharAt(expression.length() - 1); } tvDisplay.setText(expression.length() 0 ? 0 : expression.toString()); break; case equals: handleEquals(); break; case dot: appendDot(); break; default: // tag 是数字字符串或者 add/sub/mul/div if (tag.matches(\\d)) { appendDigit(tag); } else { appendOperator(tag); } break; } }clear直接清空 StringBuilder 并让显示区回 “0”。backspace删掉表达式最后一个字符这里简单粗暴地删一个字符但真实场景里如果用户输入的是1234按一下退格只会把4删掉这是符合直觉的。equals走独立的求值逻辑避免和普通按键混在一起。注意tag.matches(\\d)这个正则它只匹配单个数字字符。我们的数字按钮 tag 恰好都是单个数字所以能正确区分数字和运算符。如果你把 tag 改成num_7这种形式这里就要改成tag.startsWith(num_)再截取逻辑会更绕这也是当初直接把数字当 tag 的好处。3.3 输入状态机刚算完和正在输入是两回事计算器最容易出 bug 的状态是“按了等号之后再按数字”。按正常逻辑125算出 17 后如果再按一个 8应该把 17 清掉重新开始而不是在 17 后面拼出 178。这里需要一个布尔变量justCalculated来标记当前是否处于“刚算完”的状态。private void appendDigit(String digit) { if (justCalculated) { expression.setLength(0); tvDisplay.setText(0); justCalculated false; } String current tvDisplay.getText().toString(); if (current.equals(0)) { tvDisplay.setText(digit); expression.append(digit); } else { tvDisplay.append(digit); expression.append(digit); } }justCalculated为 true 时说明上一次按了等号此时来一个数字先把表达式和显示区全部重置再开始输入新数字。为 false 时当前是正常输入过程只需要判断显示区是不是初始的 “0”是就替换掉不是就追加。那为什么这里还要一个expression和一个tvDisplay两套数据事实上显示区显示的就是当前表达式本身两个数据始终是一致的区分开是为了后续扩展方便。比如以后要在显示区做高亮、要限制显示长度或者要做历史记录光靠 TextView 的文本拿不到可靠的结构化数据。现在这版项目里你完全可以理解为expression是对显示文本的引用二者同步更新。运算符的追加逻辑也是一样的套路只是要多一层“尾部运算符替换”的保护private void appendOperator(String tag) { if (justCalculated) { // 刚算完先把结果作为新一轮计算的开头 justCalculated false; } if (expression.length() 0) { // 表达式为空时不允许以运算符开头直接忽略 return; } char lastChar expression.charAt(expression.length() - 1); if (isOperator(lastChar)) { // 表达式尾部已经是运算符用新运算符替换掉它 expression.setCharAt(expression.length() - 1, operatorSymbol(tag)); } else { expression.append(operatorSymbol(tag)); } tvDisplay.setText(expression.toString()); }isOperator判断最后一位是不是 - × ÷如果是就把末尾替换成新运算符否则正常追加。这一步拦截了12、12×3这类脏表达式求值器不需要再处理连续运算符的情况。实际用下来这比在表达式解析器里做防御要清爽得多。4. 计算核心自己写一个支持优先级的求值器4.1 用 StringBuilder 攒表达式不让计算器现算现卖有些同学会把计算器做成“每按一次运算键就立刻算一次”比如按1 2显示区立刻变成 3再按× 4又立刻算出 12。这种方案表面看也能跑但有个致命问题1 2 × 3这种多级运算按到中间那步你根本没法确定后面还有没有更高优先级的运算所以必须等用户按等号时一次性把整个表达式算完。正确的做法是按键阶段只负责拼字符串把完整表达式攒在 StringBuilder 里最后按等号时才交给求值器。这也是为什么显示区在按键过程中会显示12×3而不是3×3。private void handleEquals() { if (expression.length() 0) { return; } String expr expression.toString(); String result ExpressionEvaluator.evaluate(expr); tvDisplay.setText(result); expression.setLength(0); expression.append(result); justCalculated true; lastExpression expr; }handleEquals里先把当前表达式存进lastExpression这是为了最后一章的“再次等号重复上一步运算”功能做准备。表达式求完值后把结果重新放回expression这样用户可以直接按运算符接续运算比如123之后按5就能基于 3 继续算。4.2 双栈求值加减乘除的优先级一次讲透表达式的核心求值器我单独抽成一个类不在 Activity 里堆逻辑。这里用最经典的双栈法一个操作数栈一个运算符栈× ÷的优先级高于 -。遇到运算符时反复弹出栈顶高优先级运算保证12×3先算乘法。import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Stack; public class ExpressionEvaluator { public static String evaluate(String expr) { if (expr null || expr.trim().isEmpty()) { return 0; } String trimmed expr.trim(); char first trimmed.charAt(0); char last trimmed.charAt(trimmed.length() - 1); if (!Character.isDigit(first)) { return 表达式不完整; } if (!Character.isDigit(last) last ! .) { return 表达式不完整; } StackDouble numbers new Stack(); StackCharacter operators new Stack(); StringBuilder numBuffer new StringBuilder(); for (int i 0; i trimmed.length(); i) { char c trimmed.charAt(i); if (Character.isDigit(c) || c .) { numBuffer.append(c); } else { numbers.push(Double.parseDouble(numBuffer.toString())); numBuffer.setLength(0); while (!operators.isEmpty() priority(operators.peek()) priority(c)) { double b numbers.pop(); double a numbers.pop(); numbers.push(apply(a, b, operators.pop())); } operators.push(c); } } if (numBuffer.length() 0) { numbers.push(Double.parseDouble(numBuffer.toString())); } while (!operators.isEmpty()) { double b numbers.pop(); double a numbers.pop(); numbers.push(apply(a, b, operators.pop())); } double result numbers.pop(); return formatResult(result); } private static int priority(char op) { if (op || op -) { return 1; } if (op * || op /) { return 2; } return 0; } private static double apply(double a, double b, char op) { switch (op) { case : return a b; case -: return a - b; case *: return a * b; case /: return a / b; default: return 0; } } private static String formatResult(double value) { if (Double.isNaN(value) || Double.isInfinite(value)) { return 不能除以0; } if (value Math.floor(value) Math.abs(value) 1e15) { return String.valueOf((long) value); } BigDecimal rounded new BigDecimal(value) .setScale(8, RoundingMode.HALF_UP); return rounded.stripTrailingZeros().toPlainString(); } }建议把头文件里的 import 写完整BigDecimal和RoundingMode都在java.math包下模拟器上跑没问题。整个求值器的流程是从左到右扫描表达式遇到数字就收集进numBuffer遇到运算符就把缓冲区里的数字压栈然后根据优先级决定是立即运算还是留到后面。以12×3为例扫描到时数字栈压入 1运算符栈压入扫描到×时数字栈压入 2此时×的优先级高于栈顶的所以不弹栈直接压入×扫描到末尾后数字栈压入 3最后统一结算先弹×算出 2×36再弹算出 167。这个顺序和手算完全一致。formatResult里做了两层修整。第一层判断NaN和Infinity这是除零导致的结果直接显示中文提示。第二层把可能是整数的结果转成不带小数的形式比如2显示成2而不是2.0。非整数则用 BigDecimal 保留 8 位小数并去掉末尾多余的零0.30000000000000004在这里会被修整成0.3。4.3 计算前的边界检查除零、空表达式、尾运算符这个求值器能处理正常四则运算但期末答辩老师最喜欢问“如果用户乱按怎么办”。求值器入口处我做了三个显式检查对应三个最容易出现的脏输入。第一空表达式。用户在什么都没输入的情况下直接按等号expression长度为 0handleEquals里已经做了保护但求值器层再守一道防止将来有其他调用方。第二表达式以运算符开头。比如用户连按 5 6显示区是56这种表达式在我设计的输入状态机里根本不会产生因为appendOperator里expression.length() 0会直接忽略运算符。但求值器还是加了!Character.isDigit(first)的判断双保险。第三表达式以运算符结尾。比如用户按1 就急着按等号尾部检查发现last不是数字也不是小数点直接返回“表达式不完整”。这里没有抛异常因为期末项目需要的是可见的友好反馈而不是让 App 崩掉。注意这个求值器不支持括号也不支持负数开头比如-53。如果你的按钮没有做正负号切换功能这两个限制完全够用如果以后加了括号双栈逻辑就得大改。5. 计算器避坑清单期末答辩前必过的 5 道坎5.1 旋转屏幕后输入全丢Activity 重建的锅现象模拟器上算到一半旋转一下屏幕所有输入清零回到初始状态。原因默认情况下旋转屏幕会触发 Activity 销毁重建所有成员变量全部重新初始化。解决用onSaveInstanceState保存关键状态在onCreate里恢复。Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(expression, expression.toString()); outState.putString(display, tvDisplay.getText().toString()); outState.putBoolean(just_calculated, justCalculated); } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvDisplay findViewById(R.id.tvDisplay); if (savedInstanceState ! null) { String savedExpr savedInstanceState.getString(expression, ); expression new StringBuilder(savedExpr); tvDisplay.setText(savedInstanceState.getString(display, 0)); justCalculated savedInstanceState.getBoolean(just_calculated, false); } setupCalculatorButtons(); }注意expression不能在声明时就用new StringBuilder()赋值否则恢复阶段重新赋值会失效。我习惯把初始化放到onCreate里统一处理声明处只留空引用。5.2 显示框用了 EditText一点就弹输入法现象计算器点击显示区域底部弹出系统输入法把按钮挡住一半整个界面变得没法操作。原因新手为了显示文本方便把显示区做成 EditText而 EditText 天生抢焦点、弹键盘。解决显示区一律用 TextView不接受任何输入自然不会有键盘。如果你的项目已经用了 EditText两个方向一是把控件类型改成 TextView二是给 EditText 设置android:focusablefalse和android:inputTypenone。但治本还是换成 TextView别在错误的选择上打补丁。5.3 0.1 0.2 算出 0.30000000000000004现象计算0.1 0.2结果不是 0.3而是一长串浮点数尾巴。原因Java 的 double 类型遵循 IEEE 754 标准二进制无法精确表示 0.1计算时产生误差外露。解决不参与运算逻辑在显示结果时用 BigDecimal 修整小数位。这也是我formatResult里用setScale(8, RoundingMode.HALF_UP)的原因。注意 BigDecimal 只在最终显示时使用如果每一步都用 BigDecimal 做四则运算性能会差一截期末项目完全没必要。5.4 连按运算符表达式变成垃圾现象快速点击、-、×显示区出现1-×3这种乱码按等号直接报错。原因运算符追加逻辑没有检查表达式尾部是否已经是运算符。解决在appendOperator里判断最后一个字符如果是运算符就替换而不是追加。这个分支我前面已经给过代码本质是“输入阶段的脏数据拦截”。我们宁可在按键阶段多花几个 if 判断也不要在求值阶段写一堆容错逻辑——容错越多越容易埋雷。5.5 为每个按钮单独写监听器代码量爆炸现象MainActivity 里从findViewById(R.id.btn0)一路写到btnDot每个按钮后面跟着三行setOnClickListenerActivity 接近两百行答辩老师问一个按钮逻辑要翻半天。原因不了解 ViewGroup 的子控件遍历方案。解决用 GridLayout 的getChildCount()和getChildAt()统一绑定这个做法在 3.1 已经实现。可能有人担心性能18 个按钮的遍历耗时可忽略关键是代码可维护性提升了一个量级。6. 答辩加分项把“再次等号”和状态恢复做成亮点6.1 再按一次等号重复上一步运算很多基础版计算器只实现了“按一次等号出结果”不支持连续按等号重复运算。如果你在答辩前把这个功能补上属于典型的低成本高展示度改进。思路是等号求值时把表达式存入lastExpression下次再按等号时如果当前处于justCalculated状态就用lastExpression重新求值。private void handleEquals() { if (expression.length() 0) { return; } if (justCalculated lastExpression ! null) { String resultAgain ExpressionEvaluator.evaluate(lastExpression); tvDisplay.setText(resultAgain); expression.setLength(0); expression.append(resultAgain); return; } String expr expression.toString(); String result ExpressionEvaluator.evaluate(expr); tvDisplay.setText(result); expression.setLength(0); expression.append(result); justCalculated true; lastExpression expr; }第一次按等号会走下面的逻辑把12存入lastExpression。结果3写进expressionjustCalculated置为 true。第二次按等号时因为justCalculated为 true直接拿lastExpression再算一遍结果依然是 3。配合前面appendDigit里“刚算完就清空”的逻辑用户按数字时又自动离开这个状态逻辑是闭环的。6.2 让 lastExpression 跟着 Bundle 一起恢复状态恢复不能漏掉新变量否则旋转屏幕再按等号lastExpression是 null点击等号直接跳过重复运算。把 5.1 的onSaveInstanceState补一行outState.putString(last_expression, lastExpression);恢复时同理lastExpression savedInstanceState.getString(last_expression, null);这几行代码讲不出太多花哨道理但“新增状态就要同步做序列化”是个好习惯很多项目 bug 都出在加了新字段忘了存。我做这类期末小项目有个习惯写完求值器一定先把12×3、6÷0、5、空表达式、0.10.2这几组数据在模拟器上跑一遍再考虑加功能。状态恢复、重复等号这些加分项优先级排在基础稳定性之后。基础算不对功能再多也是花架子。希望这篇笔记能帮你在期末前少踩几个坑交一份自己心里有底的大作业。本文还有配套的精品资源点击获取