news 2026/10/23 17:20:00

Android计算器开发实战:从状态机设计到Activity生命周期管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android计算器开发实战:从状态机设计到Activity生命周期管理

很多刚接触Android开发的朋友问我的第一个问题,往往是“我该写个什么项目练手”。我一般都会推荐:用AS写一个简单计算器。AS就是Android Studio,做Android开发最常用的IDE。这个项目看着不起眼,但Activity生命周期、布局编写、事件绑定、状态管理这几块基础能力,它全都覆盖到了,而且难度刚好卡在一个晚上能写完、又不至于毫无挑战的位置。适合完全零基础、刚学完Java或者Kotlin语法、想完整走一遍Android工程流程的人。做个能算数的App不算本事,把这个项目里藏着的状态机思路理清楚,才是真正的收获。

1. 动手前先想清楚:语言与工程基础

1.1 用Java还是Kotlin

现在的Android Studio新建项目时,默认推荐的是Kotlin,不少教程也直接拿Kotlin写Demo。但我的建议很直接:如果你已经有明确的主力语言,就用那个;如果还没想好,Kotlin是趋势,但计算器这点业务逻辑用Java写也完全没有障碍。

本文的示例代码我用Java来写,理由很朴素——Java的语法对新手来说更直白,声明变量、调用方法、switch分支,每一步都能跟教科书对上。Kotlin写起来更简洁,但初学阶段“少写几行”并不是最重要的,重要的是每一步都看得懂。等逻辑全部理清楚了,再改成Kotlin其实就是换个语法外壳,花不了半个小时。工程创建时选Empty Views Activity就行,包名随意,minSdk建议选23以上,targetSdk用你本机SDK里最新的稳定版。编译版本这件事不用纠结,AS默认给的配置就够用,不需要手动改Gradle文件里的任何参数。

1.2 界面方案与布局选型

动手写代码之前,先把界面在脑子里过一遍。一个最简单的计算器,上面是一块显示屏,下面是若干排按钮。显示屏用TextView就够了,不涉及输入法,也不用处理焦点问题。按钮区最省事的方案是用LinearLayout嵌套——竖向排五排,每排横向放四五个按钮,每排占一行高度。

为什么不推荐直接用ConstraintLayout?不是它不行,而是对于这种规则网格按钮,LinearLayout配合weight权重分配反而是最直观的解法。每个按钮说好占多少比例,布局文件写完,界面长什么样心里基本有数。ConstraintLayout当然也能做,但约束条件写起来比LinearLayout啰嗦,新手容易漏约束然后出现布局飘来飘去的问题。另外一个思路是用GridLayout,网格布局天然适合计算器,但GridLayout的索引管理对新手不太友好。权衡下来,LinearLayout嵌套是我带过这么多项目后觉得最稳的入门方案。

2. 搭界面:一个XML搞定所有按钮

2.1 LinearLayout和weight的配合

先解释一个很多新手栽过跟头的概念:weight权重。LinearLayout里的按钮,如果设置了layout_width="0dp"和layout_weight="1",就表示它参与“剩余空间”的按比例分配。比如一行里有四个按钮,每个权重都是1,总权重是4,那么每个按钮拿到1/4的宽度。关键在于layout_width必须写0dp,如果写成wrap_content,按钮会先按内容占一块地方,剩下的空间才按权重分,最后比例完全不是你想的那样。

下面是一行按钮的完整写法,以数字1、2、3和加号这一行为例:

<LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <Button android:id="@+id/btn1" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="4dp" android:text="1" /> <Button android:id="@+id/btn2" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="4dp" android:text="2" /> <Button android:id="@+id/btn3" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="4dp" android:text="3" /> <Button android:id="@+id/btnAdd" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="4dp" android:text="+" /> </LinearLayout>

其他几行照抄这个结构,改一下id和text就行:清空、删除、百分比、除号一行,7、8、9、乘号一行,4、5、6、减号一行,最后0、小数点、等号一行。注意最外层LinearLayout的方向是vertical,每一行的layout_height都设为0dp并且权重为1,这样五排按钮会均分竖直方向的空间,不管什么屏幕尺寸都不会挤压重叠。

2.2 显示区为什么要用TextView

很多人习惯性地把显示结果的控件选成EditText,理由是“看着像输入框”,但这会给自己挖坑。EditText默认可以聚焦、弹输入法、支持粘贴文本,你辛辛苦苦写完运算逻辑,用户长按粘贴一串乱七八糟的字符串进来,程序直接就崩了。TextView没有这些问题,它天生就是用来展示内容的。

显示区的布局我建议这样写:

<TextView android:id="@+id/tvResult" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="0.3" android:gravity="end|center_vertical" android:maxLines="1" android:text="0" android:textSize="36sp" />

几个细节说明一下。gravity设为end,表示文字靠右显示,这是计算器的通用习惯。maxLines设为1,防止数字太长时把按钮挤得走样。textSize用36sp起步,算出来的结果很长时会显得有点挤,但不影响功能。最外层LinearLayout的高度分配上,给显示区块一个0.3的权重,五排按钮共占1个权重,整体比例就是上面留一小块、下面按钮区占大头,视觉比例比较接近真机上的计算器。

3. 写逻辑:状态机才是计算器的灵魂

3.1 五个状态变量,理清所有输入路径

界面只是壳,计算器真正的核心是“状态”。很多新手写计算器是先拿一个字符串存所有输入,等按等号时再用表达式求值器去算,这个方向一上来就复杂了。正确做法是维护几个状态变量,让每次按键都走明确的分支。

我建议用下面这几个变量:

private TextView tvResult; // 当前屏幕上显示的数字,也就是用户正在输入的内容 private StringBuilder currentInput = new StringBuilder("0"); // 参与运算的第一个操作数,比如按下“2+3”里的2 private double firstOperand = 0; // 当前挂起的运算符,比如“+”或者“×” private String pendingOperator = ""; // 用户刚按过运算符,下一个数字键应该重新开始输入 private boolean isOperatorPressed = false; // 已经按过等号并显示了结果,下一个数字键应该清空重来 private boolean isResultShown = false;

这五个变量各管一摊事。currentInput管的是屏幕显示,firstOperand管的是暂时寄存的数字,pendingOperator管的是下一步要做的运算,两个布尔变量管的是“用户输入节奏”。为什么要单独用一个布尔变量记录是否刚按过运算符?因为用户按完“2 +”,再按“3”的时候,程序需要知道这个3是新的输入,而不是接着2后面变成23。同理,按完等号后屏幕显示8,这时再按数字5,应该从5重新开始,而不是在8后面补一位变成85。

拿“1 + 2 =”这个操作序列走一遍:按1,currentInput变成“1”。按加号,此时firstOperand记下1,pendingOperator记下“+”,isOperatorPressed置为true。按2,因为isOperatorPressed是true,currentInput先清空再append“2”。按等号,程序取出firstOperand和currentInput里的2做加法,结果写回currentInput。每一步都有明确的归宿,不会出现歧义。这就是状态机设计的意义——用几个标志位把用户所有可能的操作路径都约束住。

3.2 数字键和小数点键的逻辑

数字键的点击事件其实只有几行代码,但分支条件特别容易写漏。我建议把所有数字键还有小数点键都路由到同一个方法里,用按钮的tag区分具体是哪个字符:

private void onNumberPressed(String tag) { // 刚算完结果,再按数字就重新开始 if (isResultShown) { currentInput.setLength(0); isResultShown = false; } if (tag.equals(".")) { // 小数点不能出现两次 if (currentInput.indexOf(".") >= 0) { return; } // 直接按小数点,补一个0在前面,显示“0.” if (currentInput.length() == 0) { currentInput.append("0"); } } else { // 开头是0时,直接按数字要把0替换掉 if (currentInput.toString().equals("0")) { currentInput.setLength(0); } // 限制最长输入15位,防止溢出和显示溢出 if (currentInput.length() >= 15) { return; } } currentInput.append(tag); updateDisplay(); }

这里面有两个容易被忽略的边界。一是“0开头”的处理,用户先按了个0,接着按5,屏幕应该显示5而不是05,所以要先清空再append。二是小数点只允许出现一次,用户按“1.2.3”这种输入没有意义,必须拦下来。另外,输入长度限制十五位这个判断,很多入门教程里都没有,但不加的话,用户连续按几十位数字,double类型解析时就会出问题,显示上也会变成科学计数法。

updateDisplay方法也很简单,统一出口,所有修改currentInput的地方最后都调它刷新屏幕:

private void updateDisplay() { tvResult.setText(currentInput.toString()); }

3.3 运算符、等号和清空键的逻辑

运算符键的处理是计算器状态机的精华部分,也是新手写起来最容易乱的地方。核心逻辑是:如果当前已经有一个挂起的运算符,而且用户没有刚按过运算符,那说明输入格式是“数字 运算符 数字 运算符”,可以先把前面的运算做掉,把结果作为新的firstOperand;如果用户连续按了两个运算符,比如“5 + ×”,那直接用新的运算符覆盖旧的,不进行运算。

private void onOperatorPressed(String op) { // 有挂起的运算符,且不是刚按完运算符的状态,就先把之前的运算执行掉 if (pendingOperator.length() > 0 && !isOperatorPressed) { performCalculation(); } else { // 没有挂起运算符,或者用户刚按过运算符,直接取当前输入作为第一个操作数 firstOperand = parseInput(); } pendingOperator = op; isOperatorPressed = true; isResultShown = false; } private void onEqualsPressed() { if (pendingOperator.length() == 0) { return; } performCalculation(); pendingOperator = ""; isOperatorPressed = false; isResultShown = true; } private void onClear() { currentInput.setLength(0); currentInput.append("0"); firstOperand = 0; pendingOperator = ""; isOperatorPressed = false; isResultShown = false; updateDisplay(); }

performCalculation是真正做加减乘除的地方:

private void performCalculation() { double secondOperand = parseInput(); switch (pendingOperator) { case "+": firstOperand = firstOperand + secondOperand; break; case "-": firstOperand = firstOperand - secondOperand; break; case "*": firstOperand = firstOperand * secondOperand; break; case "/": if (secondOperand == 0) { showError(); return; } firstOperand = firstOperand / secondOperand; break; } currentInput.setLength(0); currentInput.append(formatResult(firstOperand)); updateDisplay(); } private double parseInput() { if (currentInput.length() == 0) { return 0; } return Double.parseDouble(currentInput.toString()); }

这里有个取舍要说明:系统计算器支持“连按等号”继续运算,比如“5 + = =”会得到25。我们这个入门版本故意不做这个功能,因为要实现它需要额外保存“上一次的运算符”和“上一次的操作数”,状态变量会从五个变成七个八个,对新手来说认知负担一下子大很多。先把基本运算链路跑通,连按等号可以在后续扩展阶段再加。

3.4 浮点精度和结果显示

写计算器绕不开浮点精度问题。0.1加0.2,数学上等于0.3,但double计算出来的结果很可能是0.30000000000000004。这不是Android的问题,是二进制浮点表示的天生缺陷。如果不做任何处理,用户会在屏幕上看到一长串莫名其妙的小数尾巴,体验很差。

解决办法是在显示层面做格式化。我的formatResult方法是这样写的:

private String formatResult(double value) { if (Double.isNaN(value) || Double.isInfinite(value)) { return "错误"; } // 结果是整数时,去掉小数点,直接显示整数 if (value == Math.floor(value) && Math.abs(value) < 1e15) { return String.valueOf((long) value); } // 用字符串转换,避免多余尾零 return String.valueOf(value); }

整数直接显示整数,这个判断很重要。否则按“2 + 2”,结果可能显示成“4.0”或者“4.000000000000001”,很不好看。非整数情况直接用String.valueOf转换,虽然某些极端场景下还是会带一长串小数,但对一个入门计算器来说够用了。真想彻底解决精度问题,可以引入BigDecimal做运算,但除法除不尽时还需要设计舍入规则,复杂度上来了。我对初学者的建议是:运算先用double跑通,显示层做好格式化,等以后做更复杂的项目再上BigDecimal,没必要一开始就被精度问题绊住。

4. 绑事件:批量绑定与交互细节

4.1 用数组和tag批量绑定,别写二十遍findViewById

计算器有将近二十个按钮,如果每个按钮都写一遍findViewById和setOnClickListener,代码又长又容易写错。我习惯的做法是把按钮id放进数组,循环绑定,再用tag区分具体按键。

数字键和小数点键的绑定:

private void bindNumberKeys() { int[] ids = { R.id.btn0, R.id.btn1, R.id.btn2, R.id.btn3, R.id.btn4, R.id.btn5, R.id.btn6, R.id.btn7, R.id.btn8, R.id.btn9, R.id.btnDot }; View.OnClickListener listener = v -> { String tag = (String) v.getTag(); onNumberPressed(tag); }; for (int id : ids) { Button btn = findViewById(id); btn.setOnClickListener(listener); btn.setTag(btn.getText().toString()); } }

注意我在绑定监听器时顺手把按钮显示的文字设置成了tag。这样数字键的tag就是“0”“1”这些字符,小数点键的tag是“.”,之后在onNumberPressed里直接用tag判断,不用再根据id去switch。运算符按钮和功能键按钮同理,把所有运算符按钮放进一个数组,统一绑定到onOperatorPressed,tag设成“+”“-”“*”“/”。

代码干净了,后面加按钮也方便。以后想加个“平方”按钮,只需要在布局里加一个控件,id加进数组,tag设置成对应符号,逻辑方法里加一个分支,不需要动其他任何代码。

4.2 清空、删除和按键防抖

删掉最后一个输入字符,这个功能看着简单,但有几个边界要想清楚。当前输入只有一位数字时,删除之后应该显示0而不是显示空字符串;当前处于“刚算完结果”的状态时,按删除键我建议直接清空整个算式,因为用户这时候大概率是想开启新的一次计算,而不是删结果里的某一位数字。

private void onDelete() { if (isResultShown) { onClear(); return; } if (currentInput.length() > 1) { currentInput.deleteCharAt(currentInput.length() - 1); } else { currentInput.setLength(0); currentInput.append("0"); } updateDisplay(); }

关于“防抖”,很多新手一上来就给按钮加各种防抖逻辑。其实计算器场景里,按钮点击事件的处理是幂等的——不管用户多快连按多少次,每次点击的逻辑都独立完整,不会出现累加错误的状态。真正需要防的,反而是某些教程里写的“点击后清零再赋值”这种写法,快速点击时会互相覆盖出问题。所以我的建议是:不要画蛇添足加防抖,把每个点击分支写清楚,比什么都管用。

5. 抗风险:状态保存与异常兜底

5.1 旋转屏幕为什么丢结果

新手最容易遇到的诡异问题:算到一半,旋转一下手机,屏幕上的结果变成0了。原因很简单——旋转屏幕会让Activity销毁并重新创建,所有内存里的变量全部重置。firstOperand、pendingOperator、currentInput全都回到初始值,你的计算结果自然就消失了。

解决办法是实现状态保存和恢复。Activity在即将被销毁时会调用onSaveInstanceState,我们可以把关键状态存进去,重建后再读出来:

@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString("currentInput", currentInput.toString()); outState.putDouble("firstOperand", firstOperand); outState.putString("pendingOperator", pendingOperator); outState.putBoolean("isOperatorPressed", isOperatorPressed); outState.putBoolean("isResultShown", isResultShown); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); currentInput = new StringBuilder(savedInstanceState.getString("currentInput", "0")); firstOperand = savedInstanceState.getDouble("firstOperand", 0); pendingOperator = savedInstanceState.getString("pendingOperator", ""); isOperatorPressed = savedInstanceState.getBoolean("isOperatorPressed", false); isResultShown = savedInstanceState.getBoolean("isResultShown", false); updateDisplay(); }

有人会说,那干脆在AndroidManifest.xml里给Activity加一行android:screenOrientation="portrait",把屏幕锁死成竖屏,不就一了百了了吗?这个做法能用,但不推荐作为默认方案。锁死方向等于把问题藏起来了,并没有真正理解Activity生命周期。而且以后做平板适配、做横屏布局时,锁方向的方案会反咬你一口。既然计算器项目已经帮你遇到了状态保存这个问题,就花十分钟把它解决掉,这笔账怎么算都划算。

5.2 除零、溢出与输入保护

除零是所有计算器都必须处理的情况。在我的performCalculation里,已经加了secondOperand == 0的判断,会调用showError方法:

private void showError() { currentInput.setLength(0); currentInput.append("错误"); isResultShown = true; updateDisplay(); }

把“错误”直接显示在屏幕上,同时把isResultShown置为true。这样用户按任意数字键时,会命中onNumberPressed里isResultShown的判断分支,自动清空错误提示,开启新的输入。不需要用户手动先按一次清空键,体验会顺手很多。

溢出判断也别忘了。double类型在数值过大的时候会变成Infinity,这种情况下继续运算没有任何意义。我在formatResult里已经用Double.isInfinite和Double.isNaN做了拦截,任何NaN或者Infinity的结果都会统一显示成“错误”。配合前面说的十五位输入长度限制,实际使用中基本不会出现溢出场景,但程序里留着这层保护,心里踏实。

6. 常见问题速查与避坑记录

6.1 问题与原因对照表

下面这个表格里的问题,是我这些年看别人做计算器项目时反复出现的,整理出来可以做自查清单:

问题现象根本原因解决方案
按钮挤成一团,比例不对layout_width没设0dp,weight没生效按钮宽度统一设0dp,用weight分配
旋转屏幕后计算结果丢失Activity重建,内存变量重置实现onSaveInstanceState和onRestoreInstanceState
0.1+0.2显示一长串小数double浮点精度误差显示层做格式化,整数直接显示long
连续按等号没反应没有保存上一次运算符入门版本可接受,后续扩展时加lastOperator
输入超长变成科学计数法没有限制输入长度数字输入长度限制15位以内
点击删除后屏幕空了删除逻辑没处理最后一位删除到只剩一位时显示0
按完等号再按数字变成拼接isResultShown状态没处理结果状态后按数字先清空currentInput

6.2 我建议新手养成的好习惯

代码写完之后,我强烈建议你在真机上完整跑一遍这些操作序列:连续输入多位数字、连续按多个运算符、小数和0开头的输入、快速连续按等号、旋转屏幕后再运算、除以0、输入超长数字。这几个动作覆盖了计算器绝大部分边界情况,任何一个出问题,都能对照上面的表格快速定位。

另外一个习惯是命名要表意。我见过有人把状态变量命名为a、b、c,结果代码写出来第二周自己都看不懂。firstOperand、pendingOperator这种冗长一点的名字,比a、b、c值钱得多。再有就是每个方法只做一件事,onNumberPressed就只管数字输入,performCalculation就只管运算,别把清空逻辑写进数字处理里,代码会越写越乱。

最后再分享一点我的体会:计算器虽然小,但它是少有的能把Android基础能力完整串起来的项目。做完它,你不只是得到一个能算数的App,更重要的是建立了“写代码前先把状态想清楚”的习惯。后面做表单、做播放器、做购物车,核心还是这套状态管理的思路。想进阶的话,可以试着加运算符优先级、括号、历史记录,或者用Kotlin重新写一遍,都会很有收获。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 0:29:52

Modbus报文结构详解:从RTU到TCP,功能码与CRC校验一次讲清

1. 为什么干了这么多年工控&#xff0c;还得回头抠报文先从一个很典型的场景说起。现场设备通信异常&#xff0c;PLC显示“通信超时”&#xff0c;触摸屏上数据跳成灰色。你用万用表量了线&#xff0c;没问题&#xff1b;换了个终端电阻&#xff0c;还是不行&#xff1b;把波特…

作者头像 李华
网站建设 2026/10/19 13:12:51

哈工大版理论力学学习指南:从静力学到动力学的完整路线

简介&#xff1a;这套理论力学&#xff08;哈工大版&#xff09;课件匹配高校力学课程&#xff0c;面向土木、机械、航空航天等专业本科生及考研复习者&#xff0c;系统讲解质点动力学、静力学与运动学的核心概念&#xff0c;重点基于牛顿三大定律建立质点运动微分方程&#xf…

作者头像 李华
网站建设 2026/10/19 8:16:50

Vue与Node.js构建机器人健康预警系统实践

1. 项目概述"vue基于nodejs机器人健康预警系统"是一个典型的物联网健康监测应用场景。这个系统通过Vue.js构建前端界面&#xff0c;Node.js处理后端逻辑&#xff0c;实现对机器人设备的实时健康状态监控和预警功能。在实际工业应用中&#xff0c;这类系统可以显著降低…

作者头像 李华
网站建设 2026/10/21 13:41:36

ETERM指令培训全攻略:从黑屏终端到民航票务实操

简介&#xff1a;面向航空公司运营人员与票务操作员的ETERM指令培训文档&#xff0c;系统梳理航班查询、座位预订、PNR生成与出票等核心操作。内容详解AV、SD、NM、TKTL、封口、RT提取、XEPNR取消、DETR电子客票提取等高频指令&#xff0c;并演示记录分离&#xff08;SP&#x…

作者头像 李华
网站建设 2026/10/20 23:21:12

PFC单轴压缩模拟全程自动化:非均质模型、声发射记录与裂纹触发截图

简介&#xff1a;一套面向岩石力学、土木工程与材料科学研究者的PFC单轴压缩模拟资料包。资源围绕非均质模型构建与声发射分析展开&#xff0c;提供完整的PFC代码和配套文档&#xff0c;可模拟岩石在单轴压缩下的裂纹萌生、扩展与贯通全过程&#xff0c;并同步输出裂纹数截图、…

作者头像 李华