首页 / 资讯中心 / 文章详情

Android Studio计算器开发:从布局到表达式解析完整实战

Android Studio计算器开发:从布局到表达式解析完整实战 ★ FEATURED ARTICLE
简介面向南邮安卓课程设计的计算器App实现资料包涵盖从安卓Studio项目搭建、界面布局到功能验证的完整流程适合初学安卓开发、需要完成课程作业或掌握常用控件用法的读者。压缩包共2910个文件包含XML布局与资源配置、Java源码、Gradle构建配置、依赖JAR包、编译生成的class与dex文件以及APK成品等项目结构完整可直接导入安卓Studio运行调试。资源包大小26.19MB已有8303人学习下载。该工程实现了加、减、乘、除、回退、清空及小数的连续运算并扩展了sin、cos、tan、lg、ln与括号功能运算结果有验证可借此学习按钮事件绑定、布局组织、表达式解析、运算优先级处理等核心思路对理解安卓计算器开发中的界面与逻辑分层有直接参考价值无论用于课程设计还是自学上手都很实用。1. 为什么拿计算器练手Android Studio 里最容易被低估的入门项目Android Studio 实现计算器看似是每个初学者都会碰到的经典练手项目但我见过太多人在这个题目上翻车界面拖一拖能跑按几个数字也能算可一旦连续按运算符、输入负数、旋转屏幕结果就开始“玄学”了。这个项目的价值恰恰在于此——它把 Android 的界面搭建、事件分发、状态保存和表达式解析四块基本功全部串在了一起而每一块又都有足够深的细节值得打磨。这篇文章写给两类人一类是刚装好 Android Studio、想通过完整项目把开发流程走通的新手另一类是已经能写界面、但想搞清楚表达式解析与边界处理到底该怎么做的开发者。我会沿着从布局到事件、从状态到算法、从测试到进阶的顺序把能直接复现的代码和参数逐段拆开讲。2. 计算器界面从布局文件到控件的完整搭建2.1 先画控件树TextView 与 Button 的职责划分打开 Android Studio 新建一个 Empty Views Activity 项目后很多人的第一反应是往布局里拖控件。拖拽虽然快但生成的坐标式布局在真机上常常对不齐所以我更建议直接写 XML 布局。计算器的视觉结构其实很固定顶部一块用于显示输入和结果下方是数字、运算符、清空和等号按键。这里的职责划分非常重要——显示区用一个 TextView 就够了不需要 EditText因为计算器不允许用户直接编辑表达式所有输入只能通过按键事件进入。按键则全部使用 Button方便统一处理点击事件。控件确定后接下来是容器的选择。常见做法是用 LinearLayout 嵌套每一行按键放一个水平方向的 LinearLayout整体再套一个垂直方向的 LinearLayout如果你熟悉 GridLayout也可以直接用四行五列的网格。用网格会更简洁但对新手来说嵌套 LinearLayout 的可读性更高出问题时也好定位。我一般会把数字按键的形状、间距、字体大小抽成公共属性写到独立的 XML 里统一管理避免重复定义。2.2 完整布局文件尺寸、权重与每行按键分配下面是一份可以直接复制到activity_main.xml的布局代码我用的控件树是外层垂直 LinearLayout 放显示区和键盘区键盘区内部用四个水平 LinearLayout 分行。按键布局参考了常见手机计算器的排列第一行是清空、括号、退格和除法第二到第四行是数字与加减乘第五行是数字 0、小数点、等号和取反。所占宽度权重已分配好任何屏幕宽度下都能均匀铺开。?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/tv_expression android:layout_widthmatch_parent android:layout_height64dp android:gravitycenter_vertical|end android:hint0 android:textSize28sp android:maxLines1 android:ellipsizestart android:background#F5F5F5 android:paddingHorizontal16dp / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop8dp Button android:idid/btn_clear android:layout_width0dp android:layout_height56dp android:layout_weight1 android:textC / Button android:idid/btn_left_bracket android:layout_width0dp android:layout_height56dp android:layout_weight1 android:text( / Button android:idid/btn_right_bracket android:layout_width0dp android:layout_height56dp android:layout_weight1 android:text) / Button android:idid/btn_divide android:layout_width0dp android:layout_height56dp android:layout_weight1 android:text/ / /LinearLayout !-- 其余四行按键按相同方式添加 -- /LinearLayout这段 XML 的核心是layout_width0dp配合layout_weight1这是线性布局里等分宽度的标准写法。0dp表示初始宽度为 0再由权重按比例分配剩余空间四个按钮各占四分之一。如果你把layout_width写成了wrap_content权重依然会生效但某些定制主题下可能被内容撑出偏差所以统一用 0dp 最稳。maxLines1和ellipsizestart是给显示区加的保护——表达式太长时从头缩略而不是把布局挤爆。这里没有给按钮设置文字颜色和背景是为了先用系统默认主题跑通流程再统一美化。2.3 显示区的状态位为什么单独留一个表达式字符串布局文件里只有一个 TextView但在处理逻辑中我还会维护两个字段expression和displayText。前者保存完整的用户输入例如123*(4-1)后者保存当前屏幕要显示的内容。为什么需要两份因为用户按的是按键最终要交给解析器的是一个合法中缀表达式但屏幕上显示的内容可能经过格式化例如数字位数限制、运算符号替换等。如果只维护一个字符串后面做表达式解析时还要先清洗文本容易漏掉负号和小数点。我当时做某个模拟项目 X 时犯过的错误是把显示文本直接当成表达式传给解析器结果用户输入3.时解析器直接崩溃。正确的做法很简单维护expression作为唯一的逻辑输入源按键事件都往它上面追加或修改displayText只负责把当前逻辑字符串展示到界面上。这样即使显示层做再多的格式化都不会污染解析层的输入。3. 按键事件与状态管理让按钮真正“按下有反应”3.1 统一分发用 View.OnClickListener 与 switch 分流所有按键布局搭好后下一步是把每个按钮的点击事件接起来。新手最常见的写法是给每个按钮单独写一个setOnClickListener二十几个按钮就要写二十几个监听器代码冗长且难以维护。我一般会让MainActivity直接实现View.OnClickListener然后在onCreate里统一注册最后在onClick里用一个switch按控件 ID 分流到不同的处理方法。public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView tvExpression; private String expression ; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvExpression findViewById(R.id.tv_expression); // 把所有按钮都绑定到同一个监听器 int[] btnIds { R.id.btn_0, R.id.btn_1, R.id.btn_2, R.id.btn_3, R.id.btn_4, R.id.btn_5, R.id.btn_6, R.id.btn_7, R.id.btn_8, R.id.btn_9, R.id.btn_clear, R.id.btn_divide, R.id.btn_multiply, R.id.btn_minus, R.id.btn_plus, R.id.btn_equal, R.id.btn_left_bracket, R.id.btn_right_bracket, R.id.btn_dot, R.id.btn_delete }; for (int id : btnIds) { findViewById(id).setOnClickListener(this); } } Override public void onClick(View v) { int id v.getId(); if (id R.id.btn_clear) { expression ; } else if (id R.id.btn_equal) { calculate(); } else if (id R.id.btn_delete) { deleteLastChar(); } else { appendChar(((Button) v).getText().toString()); } updateDisplay(); } }这段代码的逻辑简单直接数字、运算符、括号、小数点统一走appendChar清空和等号单独处理。Button.getText()拿到的是按钮上的文字这样写不用关心按钮 ID 与字符的映射关系新增加一个按键只需把 ID 加进数组即可。小缺点是性能上多了一次类型强转但在计算器场景里按键频率极低完全不必在意。注册按钮时用循环逐个绑定和逐个findViewById再逐个setOnClickListener效果相同但行数少一半以上。3.2 输入状态机禁止非法按键组合有了统一的监听入口关键点就变成了appendChar里的输入校验。计算器不是文本框不是所有字符都能随意拼接例如12、3**4、(12这类输入要么是非法表达式要么会干扰解析。我的做法是维护一个非常轻量的“输入状态机”而不是在解析时才去纠错。状态机只需回答一个问题当前字符能不能追加到表达式末尾。判断依据是末尾字符的类型规则如下表当前末尾字符允许追加的数字/小数点允许追加的运算符允许追加的左括号允许追加的右括号允许追加的负号数字是是否是否小数点是是否是否运算符是否是否是左括号是否是否是右括号是是否是否这条规则看起来繁琐但落地成代码很短。我在appendChar里先读取expression的最后一个字符然后用两层判断过滤非法组合。比如末尾是时用户又按了*直接忽略本次输入末尾是(时用户按了)也直接忽略。规则里专门给负号留了一条——只要前一个字符是运算符或左括号就把负号当作一元运算符接受后面解析阶段再针对一元负号做处理。状态机的核心价值是尽早拦截非法输入让表达式解析器只需要面对“结构合法但不一定可计算”的表达式大大降低解析器的复杂度。3.3 横竖屏切换为什么旋转屏幕后输入全没了Android 的 Activity 在屏幕旋转时默认会销毁并重建如果不做任何处理expression这个普通字段就会清空用户辛苦输入的算式在转个屏后全部消失。很多人在计算器项目里遇到这种情况第一反应是关掉屏幕旋转用android:screenOrientationportrait锁死竖屏。这个办法确实最简单不少商用计算器也是这么干的但从练习角度来说我更建议第一次做的时候先体验一下问题再用正确的方式解决。正确做法有两种第一种是让数据跟着配置变化走覆盖onSaveInstanceState保存expression再在onCreate里取回。第二种是把表达式字符串放到ViewModel里让数据在 Activity 重建后依然存活。计算器这种单页面场景onSaveInstanceState完全够用。需要注意onSaveInstanceState只在“非用户主动销毁”时调用例如旋转、后台被杀用户按返回键退出时不会调用所以不需要担心把临时表达式持久化到磁盘。恢复时也别直接往expression里塞建议先走一遍 3.2 的校验把可能因为中途退出留下的非法组合清掉。4. 表达式解析与结果计算把中缀表达式算对的三种思路与一种稳妥实现4.1 三个方案选型直接解析、递归下降、调度场算法用户输入的一串字符是“中缀表达式”也就是运算符在两个操作数中间的标准写法。要计算它常见思路有三个。第一个是直接在字符串上按优先级递归处理——找到最低优先级的运算符把左右两边拆成两个子表达式分别计算。思路最简单但括号和优先级组合多了以后递归深度和边界分支很难控制。第二个是递归下降解析把表达式分解为term、factor这种文法规则这是编译器常用的方法能力强但对初学者来说概念负担较重。第三个是中缀转后缀再求值也就是常说的调度场算法把12*3变成1 2 3 * 这样的后缀序列然后用一个栈从头到尾求值。这个方案思路直白代码也就百行左右还能顺带把括号处理掉。我推荐第三种原因有两点。第一它把“解析”和“计算”分成两个独立阶段方便分别测试转换阶段只负责改变顺序计算阶段只负责弹栈压栈。第二后缀表达式天然没有优先级和括号问题计算阶段只要按顺序依次处理操作数和运算符即可。对于计算器这个体量的项目调度场算法在代码复杂度和可维护性上是最均衡的。4.2 中缀转后缀的完整实现两个栈与优先级表下面这段是我在 Android Studio 里调试通过的表达式解析器去掉注释约一百二十行。它包含两个类ExpressionParser负责把中缀表达式转成后缀队列ExpressionEvaluator负责计算结果。这里的关键点有三个一是用Map保存运算符优先级二是用Character.isDigit识别数字边界三是单独处理一元负号。一元负号是指形如-3或(-5中的负号它与二元减号3-2在语义上完全不同若不区分-3^2或者(-35)都会算错。import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; public class ExpressionParser { // 运算符优先级表数值越大优先级越高 private static final MapCharacter, Integer PRIORITY new HashMap(); static { PRIORITY.put(, 1); PRIORITY.put(-, 1); PRIORITY.put(*, 2); PRIORITY.put(/, 2); } public ListString toSuffix(String expr) { ListString output new ArrayList(); StackCharacter ops new Stack(); int i 0; int n expr.length(); boolean mayBeUnary true; // 当前位置是否可能出现取反负号 while (i n) { char c expr.charAt(i); if (Character.isDigit(c) || c .) { StringBuilder num new StringBuilder(); while (i n (Character.isDigit(expr.charAt(i)) || expr.charAt(i) .)) { num.append(expr.charAt(i)); i; } output.add(num.toString()); mayBeUnary false; continue; } if (c () { ops.push(c); mayBeUnary true; i; continue; } if (c )) { while (!ops.isEmpty() ops.peek() ! () { output.add(String.valueOf(ops.pop())); } if (!ops.isEmpty()) { ops.pop(); // 弹出左括号 } mayBeUnary false; i; continue; } if (isOperator(c)) { // 场景A当前负号属于一元取反用特殊标记 ~ 压栈 if (c - mayBeUnary) { ops.push(~); i; mayBeUnary true; // 取反后面可能紧跟数字或左括号 continue; } // 场景B普通二元运算符先弹出优先级不低于当前符号的已有符号 while (!ops.isEmpty() ops.peek() ! ( PRIORITY.getOrDefault(ops.peek(), 0) PRIORITY.get(c)) { output.add(String.valueOf(ops.pop())); } ops.push(c); mayBeUnary true; i; continue; } // 无法识别的字符直接跳过; 更严格的做法是抛异常 i; } while (!ops.isEmpty()) { output.add(String.valueOf(ops.pop())); } return output; } private boolean isOperator(char c) { return c || c - || c * || c /; } }逻辑说明mayBeUnary是识别一元负号的开关它的初始值是true因为表达式开头处如果直接出现负号那一定是取反。之后每遇到一个操作数或右括号mayBeUnary变为false每遇到一个运算符或左括号mayBeUnary变为true遇到取反负号时继续保留true。这样1--2这样的输入会解析成1 - (~2)等价于12(--5)则会解析为负负得正。而3-2里中间的减号由于前一个字符是数字mayBeUnary为false会按二元运算符处理。toSuffix返回的列表里每个元素要么是数字字符串要么是单个运算符字符数字多位数和小数都已被拼成完整字符串。4.3 后缀表达式求值一个栈完成所有运算后缀表达式求值非常简单遍历输出列表数字压栈运算符弹两个数字算完再压栈。下面是对应的求值器代码包含除零保护。需要特别注意的是~一元取反符它只需要弹出一个操作数。import java.util.List; import java.util.Stack; public class ExpressionEvaluator { public double evaluate(ListString suffix) { StackDouble stack new Stack(); for (String token : suffix) { if (isNumber(token)) { stack.push(Double.parseDouble(token)); } else if (token.equals(~)) { // 一元取反弹出栈顶的一个数 double value stack.pop(); stack.push(-value); } else if (isOperator(token)) { double right stack.pop(); double left stack.pop(); char op token.charAt(0); double result; switch (op) { case : result left right; break; case -: result left - right; break; case *: result left * right; break; case /: if (right 0) { throw new ArithmeticException(除零错误); } result left / right; break; default: throw new IllegalStateException(未知运算符: token); } stack.push(result); } } // 最终栈顶就是计算结果 return stack.pop(); } private boolean isNumber(String token) { char first token.charAt(0); return Character.isDigit(first) || token.length() 1; } private boolean isOperator(String token) { return token.length() 1 -*/.contains(token); } }参数说明double作为操作数类型可以覆盖绝大多数的整数和小数计算但要注意浮点数精度问题例如0.10.2结果会是0.30000000000000004放到显示区时需要做格式化只保留合理的小数位数。关于isNumber方法我写的判断是首字符是数字或者字符串长度大于 1。这样-3这种字符串虽然首字符是负号但长度大于 1也会被识别为数字——因为-3在一元取反处理前已经由~占据了一个独立 token后续数字 token 只包含纯数字和小数点。调用时顺序是先ExpressionParser.toSuffix拿到后缀列表再ExpressionEvaluator.evaluate拿到结果最终在MainActivity里把结果显示到 TextView。4.4 连续按等号为什么结果会莫名其妙地跳动很多计算器都有“连等”功能例如输入53按等号得8再按等号得11再按得14。这看起来是个小功能但如果不实现用户会在你的应用上疯狂按等号然后说“结果不对”。问题在于每次按等号时解析器拿到的是当前expression字符串但第二次按等号时expression已经是8了再算一次还是8不会继续加 3。要实现连等需要在第一次按下等号时记录两个数据上一次的运算符和上一次的右操作数3。第二次及以后按等号时直接用上一轮结果作为新左操作数再与上一轮的运算符和右操作数拼接成新表达式。我一般会维护三个字段lastOperator、lastOperand、lastResult。按下等号时先判断expression是否包含运算符包含则正常解析并更新这三个字段不包含运算符说明用户输入的是纯数字直接把数字作为结果。第二次按等号时如果lastOperator不为空就用lastResult lastOperator lastOperand拼出一个新的中缀表达式再走一遍解析和求值。这里有个坑连等和一元负号混在一起时容易出错例如5-(-3)后再连等拼接时要小心括号我的做法是拼接右操作数时总是用括号包起来保证-(不被误解析为普通二元减号接普通数字。5. 计算器避坑指南5 个让新手翻车的边界情况与排查方法5.1 除以零App 直接闪退没有任何提示现象输入1/0按下等号应用瞬间退出Logcat 里出现ArithmeticException。原因4.3 的求值器里对除零抛出了异常但MainActivity的calculate()方法没有捕获任何异常也没有对异常做界面提示。解决在calculate()里用try-catch包住解析和求值逻辑捕获ArithmeticException后把显示区置为“不能除以 0”或类似文案同时保留用户的输入表达式方便用户修改。另一个思路是不抛异常而是在求值器里返回Double.NaN但NaN在界面上显示为NaN用户可能看不懂不如直接给中文提示。5.2 连续输入两个运算符解析结果完全不是预期现象输入3-5按等号得到-2看起来没毛病输入3/2解析器直接崩溃或输出空结果。原因3-5能通过是因为-被识别为一元取反但/后面跟时被当成了普通二元运算符于是表达式的 token 序列变成3 / 2缺少右操作数栈弹出时出现EmptyStackException。解决这类问题靠解析器兜底不如靠输入状态机前置拦截。在 3.2 的校验规则里运算符后面只允许出现数字、左括号或取反负号不允许出现另一个二元运算符。所以3/2在appendChar阶段就应该被过滤掉根本走不到解析器。如果你在某次修改中跳过了输入校验记得补上。5.3 小数点重复输入一个数字里出现两个点现象用户输入1.2.3解析器把1.2.3当成一个数字字符串Double.parseDouble直接抛NumberFormatException。原因Character.isDigit只负责判断数字字符.被并入数字片段时解析器不知道同一个数字里不能出现两个小数点。解决在appendChar里追加.之前先从当前位置往前找到最近的运算符或左括号检查这两个分隔符之间是否已经存在.。更简单的写法是如果当前要追加.且expression的最后一个字符是.直接忽略如果最后一个字符是数字则反向遍历到运算符或括号为止存在.就忽略本次输入。注意3.这种以点结尾的输入应该是合法的因为用户可能还要继续输入只是在按下等号前解析器需要把3.归一化为3—— 这一步可以在getExpression传给解析器之前用replaceAll(\\.(?[^0-9]|$), )处理。5.4 屏幕旋转后输入清空用户破口大骂的最常见原因现象旋转手机或模拟器输入框里的数字和运算符全部消失。原因expression是MainActivity的普通字段Activity 被系统销毁后重建字段恢复为默认空字符串。解决在onSaveInstanceState里保存expression在onCreate里通过savedInstanceState取出并恢复。如果你用了ViewModel则不需要在 Activity 层面做任何保存和恢复数据会自动存活。注意只保存expression和必要的状态字段即可不要保存TextView本身的对象引用因为重建后的TextView是一个新实例。5.5 括号不配对算出结果让人完全摸不着头脑现象输入((12)*3按等号得到9但很多人预期这是非法表达式应该报错。原因默认字符串解析规则里右括号数量可以被while循环中的ops.pop()静默容忍但左括号多于右括号时toSuffix会把左括号压栈后在最后统一弹出不会报错结果反而是正确计算的。严格模式下这不算 bug但对用户来说缺少右括号的表达式应该被提示非法。解决在toSuffix完成转换后检查ops栈里是否还有左括号有则说明括号不配对抛出异常并在calculate()里捕获提示“括号不完整”。注意如果用户输入了奇数个单引号或双引号这类问题计算器场景里不会出现不用考虑。6. 验证与进阶从“能算出结果”到“敢说自己写完了”6.1 手动冒烟测试清单每次改动后照着点一遍写完了不等于能交付我每次改完计算器逻辑都会按下面这张清单过一遍全部通过才算阶段性完成。这张表花五分钟就能点完但能过滤掉绝大多数低级问题。测试用例期望结果主要验证点12*37运算符优先级(12)*39括号优先级0.10.20.3经格式化浮点精度处理5/0提示不能除以0异常捕获与界面提示3-5-2一元负号与二元运算符共存-3*2-6一元负号在开头3.3末尾小数点的归一化((23)提示括号不完整括号配对的严格校验52再按首次7再次9连等功能旋转屏幕输入保留状态保存与恢复如果清单里某项不通过不要急着改代码先按 5.4 的方法看 Logcat 日志定位是解析层的问题还是界面层的问题。解析器是纯 Java 类你甚至可以在本地写一个main方法直接传入字符串测试完全不依赖 Android 环境排查速度快得多。6.2 可以继续做的三个方向从计算器到表达式引擎第一个方向是支持更多运算符例如取余%、乘方^、开平方sqrt。实现方式是在PRIORITY里增加对应优先级在ExpressionEvaluator里增加对应分支并把界面上某个不常用的按键替换成新功能。第二个方向是把解析器抽取成独立的纯 Java 类不依赖任何 Android SDK 类然后引入本地单元测试框架用Test注解写十几个断言用例这样每次改完优先级或括号逻辑都能一键回归。第三个方向是给显示区增加历史记录例如下拉查看最近十次计算表达式这个功能点会迫使你把expression、lastOperator、lastOperand这些状态字段重新设计成一组可序列化的数据类为后续持久化打基础。我现在的习惯是每次写计算器类项目第一版一定只跑通“数字、四则运算、括号”第二版才加一元负号和连等第三版才考虑美化与历史记录。这个分阶段节奏帮我避开了很多把问题混在一起的翻车现场。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站