资讯动态

Android Studio计算器源码解析:双栈引擎与28键布局

发布时间:2026/10/24 5:03:34 来源:尧图企业网站定制
简介一份面向安卓初学者的计算器小程序源代码文档基于Android Studio开发环境编写主要用于解决新手在界面布局、按钮事件监听与基础运算逻辑上的练习需求。文档以docx格式封装仅含1个文件压缩后约34KB核心内容包括网格布局的配置文件其中定义5列8行的网格、表达式与输入输出两个文本框以及清除、退格、内存等运算按键代码部分则对应按钮点击、菜单选择、对话框取消等事件处理并附带通过系统浏览器打开网页的扩展功能。已吸引1518人学习适合作为移动开发课程作业或入门练手项目。通过这份文档读者可以掌握计算器界面从控件排列到右对齐文本显示的搭建思路理解程序运行中的界面交互与事件回调机制并在此基础上自行完善科学计算、异常输入处理与多屏幕适配等高级特性。1. 这个计算器项目到底给了你什么28 键、双 TextView 与双栈引擎用 Android Studio 做计算器的人很多翻开源码却发现一大半只给了布局文件算法要么写死在一长串 if-else 里要么只能算一次就不能继续。这份“Android studio计算器”源码不太一样它把 28 键的计算器拆成了清晰的界面层和表达式解析层界面用 GridLayout 排 5 列 8 行解析用经典的运算符栈 数字栈输入与表达式各占一个 TextViewMC/MR/MS/M/M- 五个内存键也有闭环。适合三类人正在学 Android 事件监听的初学者需要一份能讲明白原理的课程作业源码的人想研究 Stack 和运算符优先级表配合方式的人。下文把它从布局到算法拆开讲容易翻车的位置也会标出来。2. 界面层拆解GridLayout 布局与 28 个按钮的事件绑定拿到一份 Android 项目源码我习惯先看布局再看逻辑。这个项目的界面文件是一个根 GridLayout里面塞了 2 个 TextView 和 28 个 Button没有任何嵌套容器。好处是布局层级浅渲染快坏处是 GridLayout 的排列规则和重量分配不直观抄代码时容易排错位置。这一章先把界面层讲透下一章再进算法。2.1 5 列 × 8 行的 GridLayout 是怎么排布的根布局的关键参数只有四个columnCount5、rowCount8、宽度match_parent、高度wrap_content。GridLayout 的排列规则很简单子控件按在 XML 里的书写顺序从左到右、从上到下自动填格子不想按顺序排的可以用layout_column、layout_row手动指定但这个项目里除了跨行跨列的按钮其余全靠顺序。把控件按行展开是这个结构行序号第 1 列第 2 列第 3 列第 4 列第 5 列0expressionOutputView跨 5 列1input_outputView跨 5 列2MCMRMSMM-3DelCEC/-sqrt4789/%5456*1/x6123-rowSpan270columnSpan2.← 等号延续至此两个 TextView 通过layout_columnSpan5各占一整行fill_parent的宽高配合layout_gravityfill_horizontal|fill_vertical才能撑满格子。这里有个细节GridLayout 里直接写fill_parent不一定能铺满必须让layout_gravity带上fill方向否则控件会按照自身内容尺寸测量看起来右边留白。等号按钮的处理是底部排布的关键它在第 6 行占据第 4 列layout_rowSpan2让高度跨两行再用layout_gravityfill_vertical把纵向填满。第 7 行剩下的位置由 0 和 . 、 填充其中 0 占了columnSpan2于是底部恰好排满 5 列。这个“跨行 跨列”的组合是计算器布局里最容易抄错的地方少写任何一个 span 或 gravity整体就会错位。2.2 两个 TextView表达式框与输入输出框的分工界面里两个 TextView 不是冗余它们各自承担不同的显示职责。expressionOutputView在最上方负责回显用户已经按过的完整表达式它设了singleLinetrue和ellipsizestart意味着文字超出宽度时省略号出现在左边保留右边最新输入的运算符和数字。下方的input_outputView是当前输入值和计算结果的共用框初始文本是 0maxLines3允许多行显示同样是右对齐。这个双框设计在手机计算器上很常见优点是用户在按长表达式时能同时看到“刚才按了什么”和“正在输入什么”。两个框都是textAlignmenttextEnd也就是右对齐这符合计算器显示习惯数字从右往左增长不会因为长度变化而跳动。字号统一用了 50sp在模拟器上很合适但真机窄屏上会放大溢出这个坑后面避坑章节会专门讲。2.3 一个监听器管 28 个按钮内部类的设计Java 端的按钮绑定没有写 28 行setOnClickListener而是用了一个内部类统一处理public class MainActivity extends AppCompatActivity { private Button[] btn; private TextView ioView; private TextView expressionOutputView; private String expression ; private String input 0; private void initial() { btn new Button[28]; ioView (TextView) findViewById(R.id.input_outputView); expressionOutputView (TextView) findViewById(R.id.expressionOutputView); ioView.setText(0); btn[0] (Button) findViewById(R.id.btn_0); // ... 依次绑定 28 个按钮 btn[27] (Button) findViewById(R.id.btn_msub); MyOnClickHandler myOnClickHandler new MyOnClickHandler(); for (Button i : btn) i.setOnClickListener(myOnClickHandler); } }MyOnClickHandler是 MainActivity 的内部类能直接读写外部类的expression和input成员变量。这样设计的好处很明显28 个按钮的事件处理逻辑共享同一组状态不需要在 Activity 和 Handler 之间来回传值而且键盘上的每个按钮只是在同一个状态机上触发不同的动作写成一个类更便于统一收口。内部类里还声明了几个关键的工具方法getRet负责根据运算符计算两个数的结果someOfStr_in_anotherStr负责从指定位置开始找最近的运算符calculate是表达式求值入口stripTrailingZeros负责清理结果里多余的尾零。从命名上能看出这个作者是按“工具方法 核心算法”来组织的而不是把所有逻辑堆在onClick里。3. 双栈计算引擎表达式求值里最关键的三件事如果说布局是皮那这个项目的计算引擎是骨。它没有用 Android 自带的表达式求值库也没有用 JavaScript 引擎桥接而是纯靠 Java 的Stack类实现了一个经典的中缀表达式求值器。核心代码不算长但包含了三个必须理解清楚的点双栈模型、优先级表的语义、负数识别分支。3.1 运算符栈 数字栈中缀表达式求值的基本模型calculate()的第一步是建立两个栈private void calculate() throws Exception { StackCharacter opStack new Stack(); StackDouble numStack new Stack(); opStack.add(#); String tempExpression expression #; int start 0; int end; while (start tempExpression.length()) { end someOfStr_in_anotherStr(operator, tempExpression, start); // 解析操作数这里省略了负数分支 numStack.add(Double.parseDouble(tempExpression.substring(start, end))); // 比较运算符优先级决定是压栈还是弹栈计算 // ... 循环内核心逻辑 } }两个栈的分工很朴素numStack存数字opStack存运算符。表达式的处理方式是扫描字符串每遇到一个运算符就说明它前面夹着一段数字把数字解析出来压进数字栈然后再决定这个运算符是直接压栈还是先把栈顶的运算符弹出来算一步。为了处理边界在栈底和表达式末尾各放了一个#哨兵字符。哨兵不进计算流程只在两两相遇时表示表达式读完了整个循环自然结束。这种方法比递归下降解析更容易读也比“每次从头遍历找最高优先级运算符”的方式高效。后半段循环原文没完整给出但按这个结构常见的补全逻辑是这样一段char nowOption tempExpression.charAt(end); char topOption opStack.peek(); int compareResult compare[topOption][nowOption]; if (compareResult 1) { opStack.add(nowOption); start end 1; } else if (compareResult 0) { double b numStack.pop(); double a numStack.pop(); char op opStack.pop(); numStack.add(getRet(a, b, op)); // 注意没有移动 start继续处理当前运算符 }这里的核心在于compareResult 1时当前运算符压栈并读下一个字符compareResult 0时先弹栈计算但start不前进让循环继续拿同一个运算符和新栈顶比较。当两个#相遇返回 0循环结束numStack栈顶就是最终结果。3.2 compare 矩阵6×6 优先级表怎么读才是对的优先级比较不是用一堆 if 判断而是查一张 6×6 的表。运算符集合是-*/%#矩阵定义如下private int[][] compare { {-1, -1, 1, 1, 1, -1}, // 栈顶为 {-1, -1, 1, 1, 1, -1}, // 栈顶为 - {-1, -1, -1, -1, -1, -1}, // 栈顶为 * {-1, -1, -1, -1, -1, -1}, // 栈顶为 / {-1, -1, -1, -1, -1, -1}, // 栈顶为 % {1, 1, 1, 1, 1, 0} // 栈顶为 # };行表示当前栈顶运算符列表示当前读入的运算符。拿3 4 * 5举例读入*时栈顶是查表compare[][*] 1表示当前读入的*优先级更高先把*压栈等后面#到来时再逐层弹出先算4 * 5再算3 20。这里有个非常值得注意的细节源码注释写的是“-1 代表低于1 代表高于”字面上容易理解成“返回值表示栈顶运算符的优先级关系”。但按实际运行行为反推1的实际动作是“当前运算符压栈”-1的实际动作是“弹出栈顶参与计算”。也就是说表是站在当前运算符的角度描述的。如果照注释直译一个优先级比较器整个计算顺序会被带偏2 3 * 4会算出错误结果。同优先级运算符之间取-1比如遇、遇-都会先弹栈计算这正好实现从左到右的结合方向。只有栈顶和当前都是#时返回0表示表达式扫描结束。这张表是理解整个计算器行为的关键建议到手源码后先在纸上手推三四个表达式再对照行为理解注释能省不少绕弯的功夫。3.3 负数识别end - start 1 分支到底在判断什么扫描表达式时someOfStr_in_anotherStr找到的end是最近一个运算符的下标start到end之间应该是数字。但负数会使这个逻辑出现边界情况比如-5 3一开头就是一个负号这时end - start 1两个运算符之间没有数字。源码的处理是if (end - start 1) { if (tempExpression.charAt(end) -) { start end; end someOfStr_in_anotherStr(operator, tempExpression, start 1); } else { throw new Exception(ExpressionError); } }这段代码解决的是两个问题一是一元负号二是连续运算符场景比如3 * -4 5。扫描到*之后下一次从*后面找运算符找到的是-此时end - start为 0发现紧邻的是-就把它当作数字的符号重新从-的下一个位置找真正的运算符最终截取-4解析为数字。这个分支只放行-遇到3 * 4这类一元正号会直接抛“表达式错误”。这是设计取舍不支持一元正号但支持一元负号。实际使用中够用因为正常用户很少在运算中间输入4这种写法。3.4 一元运算怎么落sqrt、1/x、/- 的处理思路布局里有sqrt、1/x、/-三个一元运算键它们的处理方式和二元运算符完全不同二元运算符要进表达式参与解析一元运算更像是对“当前输入值”的即时变换。源码里calculate()只负责四则运算加取模一元运算应该在onClick里走独立分支。常见做法是这样点击/-时把input取出转成 double取负后再写回文本框点击1/x时判断当前值是否为 0不为 0 则求倒数并写回点击sqrt时先判断负数负数弹错误提示非负数开方后写回。private void handleUnary(char op) { double cur Double.parseDouble(input); switch (op) { case s: if (cur 0) throw new IllegalArgumentException(SqrtByNegative); input stripTrailingZeros(String.valueOf(Math.sqrt(cur))); break; case r: if (cur 0) throw new ArithmeticException(DivByZeroError); input stripTrailingZeros(String.valueOf(1.0 / cur)); break; case n: input stripTrailingZeros(String.valueOf(-cur)); break; } ioView.setText(input); }这里容易忽略的是精度显示。Math.sqrt(2)算出来是1.4142135623730951如果不做尾零清理sqrt(4)会显示2.0而不是2。源码里出现的stripTrailingZeros就是为了处理这个问题它把 double 转成字符串后去除末尾无意义的 0让显示更干净。一元运算执行后通常还要把本次操作追加到表达式框比如表达式框显示√(9)输出框显示3。这样用户能看到完整的操作历史而不是莫名其妙蹦出一个数。4. 内存键、菜单与对话框把计算器从“能算”做到“能维护”四则运算能跑通只是第一步。这份源码相比很多教学 demo 多了一排内存键 MC/MR/MS/M/M-还带了菜单和关于对话框的骨架。这一章讲功能闭环也顺便指出代码里几个需要注意的细节。4.1 saveNum 与 MC/MR/MS/M/M- 的语义闭环内存键的变量是内部类里的private double saveNum 0.0初始值为 0。五个键的语义按标准计算器定义如下按键语义对应操作MC清除内存saveNum 0.0MR读取内存把saveNum显示到输入输出框MS保存到内存用当前输入值覆盖saveNumM累加进内存saveNum 当前输入值M-从内存减去saveNum - 当前输入值按这份源码 28 键的下标映射btn[23]到btn[27]依次是 MC、MR、MS、M、M-。常见的处理写法是这样private void handleMemoryKey(int index) { double cur Double.parseDouble(input); switch (index) { case 23: // MC saveNum 0.0; break; case 24: // MR input stripTrailingZeros(String.valueOf(saveNum)); ioView.setText(input); break; case 25: // MS saveNum cur; break; case 26: // M saveNum cur; break; case 27: // M- saveNum - cur; break; } }这里有个浮点上的老问题saveNum是 double连续几次 M 操作后显示值可能带上浮点尾数。比如 0.1 加三次saveNum内部是0.30000000000000004MR 显示出来就露馅。所以 MR 读取时一定要走stripTrailingZeros或 BigDecimal 格式化不能直接String.valueOf(saveNum)。双击 M 或连续 M 的场景是测内存功能最容易暴露问题的地方。4.2 一处注释与实现不符的按钮初始化源码里初始化部分有一段btn[23].setClickable(false);//M btn[24].setClickable(false);//M-按钮下标映射在前文已经列过btn[23]是btn_mcbtn[24]是btn_mr。也就是说这段代码实际禁用的是 MC 和 MR 键但注释写的是禁用 M 和 M-。这是明显的复制粘贴问题。从业务语义讲初始状态只有 MS 是明确可用的因为内存里还没有任何值MC清内存和 MR读内存没有意义M/M- 也没有累加基础。所以更合理的初始化应该是禁用btn[23]、btn[24]、btn[26]、btn[27]只放行btn[25]MS。等到用户第一次执行 MS 或 M/M- 之后再动态把 MC/MR 置为可点。如果只是照抄源码运行后会发现 MR、MC 是灰的而 M、M- 能点这和你预期的内存闭环是反的。具体修法放在避坑章节。4.3 菜单与关于对话框onOptionsItemSelected 的扩展点源码里还留了一段onOptionsItemSelected用Menu.FIRST系列常量区分菜单项其中一个分支弹出一个“关于”对话框确认后跳转外部页面另一个分支把某个名为isPaused的字段设为 false。从字段命名看这段逻辑大概率是从别的项目移植过来的在这个计算器上下文里没有对应的 view 对象。菜单的编程方式用的是旧式的Menu.FIRST n索引而不是菜单资源 ID。这种方式在现在的新项目里不推荐但它展示了一个扩展点如果要在计算器里加“帮助”“关于”“换肤”等功能在onOptionsItemSelected里增加分支就可以。把这段代码接到界面上需要先在 Activity 里重写onCreateOptionsMenu给菜单添加三个条目分别对应Menu.FIRST、Menu.FIRST 1、Menu.FIRST 2。Menu.FIRST 2那段把isPaused置 false逻辑上是“恢复”对应的Menu.FIRST 1预计是“暂停”但源码没把前面的分支写全。这个代码块的实用价值是演示了AlertDialog的链式写法setTitle、setMessage、setPositiveButton依次设置最后create().show()。对话框通过setCanceledOnTouchOutside(true)允许点击外部关闭这是移动端对话框很常见的用户习惯值得保留。5. 常见问题与避坑跑通这份源码最容易翻车的 5 个地方部署一份别人的源码最花时间的不是读代码而是踩坑。这里把我在跑这个项目过程中真实遇到的几个问题按“现象 → 原因 → 解决”写出来。5.1 现象MC、MR 初始变灰M、M- 反而能点运行后界面上 MC、MR 两个键是灰色不可点的M、M- 却能正常点击和预期完全相反。查看代码发现initial()里写了btn[23].setClickable(false)和btn[24].setClickable(false)注释却写着 M 和 M-。原因就是按钮下标映射和注释对不上btn[23]是btn_mcbtn[24]是btn_mr作者想禁用 M/M- 却写错了下标。解决方法是按下标映射修正同时考虑内存键的完整状态逻辑btn[23].setClickable(false); // MC btn[24].setClickable(false); // MR btn[26].setClickable(false); // M btn[27].setClickable(false); // M- // btn[25] MS 保持可用作为内存功能的入口如果只是想让初始状态符合直觉这一处修正就够了。更严谨的做法是在每次内存操作后同步更新按钮可用状态执行过 MS 或 M/M- 后把 MC、MR、M/M- 全部置为可点。5.2 现象3*4 报“表达式错误”3*-4 却正常输入3*4再按等号直接弹出“表达式错误!”但同样结构换成3*-4就能算出 -12。原因是calculate()的负数识别分支只放行-遇到、*、/等连续运算符会认为表达式不合法抛出异常。从设计角度讲这是可接受的行为因为“一元正号”在数学上等价于省略3*4完全可以写成3*4。真正需要注意的是如果你改代码支持一元正号必须在if (tempExpression.charAt(end) -)的判断里把也放进去并且把start移到end 1否则4里的 4 会丢失。这个分支很微妙建议改完后把3*4、3*-4、-53、3--4四组用例全部跑一遍。5.3 现象5.5 % 2 被拒绝提示“不能对小数或负数进行取余数操作!”取模键%是布局里的特色功能但随便拿小数试一下就报错。原因在getRet里的取模分支if ((a ! (int) a || b ! (int) b) || (a 0 || b 0)) throw new Exception(ModByInt); ret a - ((int) (a / b)) * b;作者故意把取模限制在“非负整数”范围内。原因是 Java 的%运算符对 double 的操作结果是浮点余数对负数的处理又遵循向零取整规则容易让初学者困惑。这里直接限制输入类型把边界问题通过异常挡在门外。如果要用原生 Java 取模直接把ret a % b即可但要注意结果可能带小数尾数比如5.5 % 2的结果是1.5。如果产品上允许小数取模建议用BigDecimal.remainder()而不是 double 的%能避免大部分精度问题。否则就要像源码一样在 UI 上给用户明确提示而不是抛一个笼统的异常文本。5.4 现象0.10.2 显示一长串浮点尾数输入0.10.2输出框显示0.30000000000000004虽然数学上不算错但用户观感极差。原因不难猜表达式里的数字都用Double.parseDouble解析计算结果也直接用 double 参与运算而 IEEE 754 浮点数无法精确表示 0.1 和 0.2。源码里有一个stripTrailingZeros方法它的作用是清理 double 转字符串后末尾的 0比如让2.0显示成2。但它解决不了0.30000000000000004这种尾数问题因为尾数不是 0。常见做法有两种一是用BigDecimal.valueOf(ret).stripTrailingZeros().toPlainString()再转回字符串显示二是限制小数位数保留精度到一定位数后四舍五入。需要注意BigDecimal的构造方法用new BigDecimal(0.1)会把 double 的二进制近似值原样带进来越转越乱得用BigDecimal.valueOf(0.1)或字符串构造。这个项目如果只是交作业用String.format(%.10f, ret)再清尾零是最省事的方案。5.5 现象50sp 大字号在窄屏上溢出表达式框从头部省略模拟器上界面显示良好换到小屏真机数字一长就会溢出到边界。原因有两个input_outputView的textSize50sp是固定值不随屏幕宽度缩放expressionOutputView的ellipsizestart在超长时会吞掉表达式开头的字符。50sp 在 1080p 模拟器上能显示约 8 个字符在 720p 屏上只能显示约 5 个用户按一个较长的表达式根本看不全。解决思路有两种一是改用 AndroidX 的 AutoSizeTextView设置最小文本大小和最大文本大小让字号随控件宽度自适应二是在代码里动态计算把 dp 转成 px 后按屏幕宽度等比缩放。DisplayMetrics dm getResources().getDisplayMetrics(); float textSize Math.min(50f, dm.widthPixels / 20f); ioView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);表达式框的头部省略其实是可以接受的因为用户关心的是最近输入的内容最左边是已经确认过的运算符和数字。但如果你希望用户能回看完整表达式可以把ellipsize改为marquee并开启滚动或者把singleLine改成maxLines2换行显示。6. 验证与进阶用一张自测表把这套源码跑服代码能编译不等于能交付。计算器这种工具类应用边界情况多到足以让你怀疑人生。我的习惯是拿到任何计算器 demo先建一张冒烟测试表把常规运算、异常场景、内存功能分开跑一遍跑过再谈优化。编号输入操作期望结果实际关注点1123基本加法22*3410乘法优先级高于加法334*211反序验证优先级不依赖输入顺序49/0除数不能为零!除零异常拦截59%21整数取模6-53-2负数开头73*-4-12运算符后紧跟负数87 按 /--7正负号切换99 按 sqrt3开方且无尾零1010 按 1/x0.1倒数1112 按 M再按 3 MMR15内存累加120.10.20.3浮点显示是否干净131÷30.3333333333结果位数是否可接受其中第 12 和第 13 条是浮点相关的硬关卡。很多计算器 demo 在整数运算里一切正常一到小数就露馅。如果你拿到手的是原始 double 实现建议先跑这两条不合格就按前面 5.4 的 BigDecimal 方案改造。验证环境上模拟器和真机有差异。模拟器普遍分辨率高、密度大50sp 的文字看起来刚好真机如果是小屏同样 sp 会显得偏大。所以布局相关的几条一定要在真机上过一遍尤其是底部按钮宽度 80dp 是否会被系统缩放。还有等号按钮的rowSpan2在部分国产 ROM 上可能因为 GridLayout 版本差异出现高度测量不准这时可以手动给等号按钮设一个最小高度兜底。进阶方向上这套双栈引擎最值得改的是支持括号。当前compare表只覆盖-*/%#六种字符要加括号得扩成 8×8 矩阵读入(时无条件压栈读入)时不断弹栈计算直到弹出匹配的(。改的时候注意)之后的数字解析逻辑要跟着变否则(12)*3这类表达式会扫描出“括号后紧跟数字”的异常情况。另一个方向是把 double 换成 BigDecimal。改动面不小StackDouble要变StackBigDecimalgetRet的四则运算全部换成 BigDecimal 方法解析数字的Double.parseDouble对应换成new BigDecimal(String)。但换来的是浮点问题彻底消失加法和乘法都能精确落笔。我第一次拿这套源码跑真机时就是按着这张表敲到第 12 条在 0.10.2 上被一长串浮点尾数糊了一脸才意识到“能算”和“能交付”之间差着多少边界处理。从那以后我拿到任何计算器 demo第一件事都是先过一遍小数精度和内存键状态这两关没过就坚决不上线。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑