尧图精选

Android入门实战:从零用Android Studio开发计算器App完整指南

🕒 发布时间:2026/10/2 9:25:35 📁 来源:尧图网络
简介面向零基础初学者的Android Studio计算器项目源码适合Android课程设计、大作业参考也适合刚跑通Hello World的开发者作为第二个练手实例。项目基于Java实现加减乘除、小数点与清零等基本运算重点演示界面布局、按钮事件响应、控件绑定与基础逻辑组织配套博客讲解可帮助逐步消化。压缩包共409个文件整体约13.37MB包含flat编译资源、xml布局与配置、java源代码、gradle构建脚本、jar依赖、apk安装包以及png图标等类型覆盖Android工程从源码、资源到构建产物的主要环节解压后可按Android Studio工程结构直接导入运行并通过源码与资源文件对照理解项目构建流程。目前已有3755人学习浏览。通过查看源码、布局和构建配置读者可快速掌握Android项目组织方式与按钮点击事件写法为后续独立开发打好基础。1. 为什么计算器是小白上手安卓app最划算的一个项目很多人学 Android Studio 开发的第一个念头是写 Hello World但 Hello World 点一下按钮弹一行字证明不了你会写界面也证明不了你会处理逻辑。计算器这个入门级安卓 app 恰恰把两大块都占了上面是按钮和显示区的 UI 布局底下是表达式解析和运算逻辑。从零开始用 Android Studio 把加减乘除、小数点和括号做到能真机算题全部代码两百行上下比看十篇教程都来劲。它能一次性串起 Activity、布局文件、控件绑定、事件监听、异常处理这些后面天天用的概念做完你就有底气往应用市场投第一个包了。这篇笔记直接用一套能复现的写法带你走完建工程、画界面、写逻辑、打 APK以及五个我实际踩过的坑。2. 搭建可运行的安卓工程新建项目、GridLayout 布局和虚拟机三件事2.1 新建项目时三处不能跳过的手工配置新建工程用 Android Studio 的 New Project 选 Empty Views Activity包名建议用com.example.calculator语言选 Java 或 Kotlin 都行这篇用 Java。等你点了 Finish先别急着写代码有三处配置不提前改后面会翻车。第一处模板工程默认用 ConstraintLayout这个布局做计算器键盘并不顺手因为你要的是整齐的按钮网格。后面我直接把布局文件换成 GridLayout你需要在app/build.gradle的 dependencies 里删掉或保留implementation androidx.constraintlayout:constraintlayout:2.x.x。保留也无所谓但为了避免编译时多拉一个依赖我一般会删掉dependencies { implementation androidx.appcompat:appcompat:1.6.1 // implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation com.google.android.material:material:1.9.0 }删掉 ConstraintLayout 后Activity 里的setContentView不受影响因为你引用的是自己的R.layout.activity_main。真正要改的是布局根节点这个在下一节。第二处Gradle 的 SDK 版本。新建项目默认compileSdk 34、targetSdk 34如果你手里的 Android Studio 是较老版本可能没有下载 API 34 的 SDK。这时候去 SDK Manager 里装 34或者直接把这行改成compileSdk 33同时把targetSdk也改成 33。改完记得 Sync Now。第三处把 Theme 改成不带 ActionBar 的否则计算器顶部会顶一条系统标题栏显示应用名。在values/themes.xml里把parentTheme.Material3.DayNight改成parentTheme.Material3.DayNight.NoActionBar。这是一个细节很多人做完才发现标题栏占掉了显示区的高度。这三处改完工程才算真正“干净”。模板自带的MainActivity里有一段FloatingActionButton相关的代码没有的话跳过有的话顺手删掉因为计算器用不到悬浮按钮。2.2 用 GridLayout 排计算器键盘行数列数、按钮拉伸与字号先明确结构上方是一块很大的显示区下方是 4 行 4 列的按钮键盘。显示区用 TextView键盘区用 GridLayout。整体竖排所以根节点用 LinearLayout方向为 vertical。GridLayout 是 Android 官方自带的网格布局控件不需要额外引库。关键属性是rowCount和columnCount一旦指定行数列数子视图按顺序填充。想让按钮均分宽度和高度核心技巧是给每个按钮设layout_columnWeight1和layout_rowWeight1这两个属性是 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 TextView android:idid/tv_display android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:background#F5F5F5 android:gravitybottom|end android:padding16dp android:text0 android:textSize42sp / GridLayout android:layout_widthmatch_parent android:layout_height0dp android:layout_weight3 android:rowCount4 android:columnCount4 Button android:idid/btn_7 android:layout_columnWeight1 android:layout_rowWeight1 android:text7 / !-- 其余按钮按顺序排列 -- /GridLayout /LinearLayout关键点layout_height0dp配合layout_weight让显示区和键盘区按 1:3 分配高度。如果你改成wrap_content键盘会顶出去或者显示区挤成一条线。每个按钮都要写layout_columnWeight和layout_rowWeight少写一个那一格就不会拉伸按钮会变成自己内容的大小非常难看。Button 默认有边框和背景在 Material 主题下按钮是圆角的这没问题。但如果你的按钮文字太小在themes.xml里全局改会影响其他地方我一般直接在布局里给每个 Button 写android:textSize24sp。偷懒做法是抽一个 style 出来但入门阶段直接写属性出问题好排查。2.3 布局预览与虚拟机为什么模拟器跑出来和预览不一样写完 XMLAndroid Studio 的 Preview 窗口会显示渲染效果但很多人会遇到一个问题预览里按钮排列正常跑到模拟器上却出现按钮被挤到屏幕外或者行距忽大忽小。最常见的原因就是根节点 LinearLayout 的layout_weight没写或者 GridLayout 的权重属性漏了。预览窗口在渲染时会宽容处理一些属性而真机不会它会严格按布局参数排。另一个问题是模拟器本身的屏幕密度。如果你创建的虚拟机是Pixel 2 API 33分辨率 1080×1920按钮显示正常但如果你用Nexus One这类小屏老设备42sp 的显示区字会溢出。建议创建虚拟机时直接选 Pixel 系列屏幕比例和现代手机一致不要贪旧机型。跑虚拟机的步骤工具栏点 Device ManagerCreate Device选 Pixel 4下载系统镜像后 Finish。启动一次虚拟机通常需要两分钟如果是 AMD 处理器记得在 BIOS 里开 SVM 虚拟化不开的话模拟器会直接卡死在启动画面这是新手第一个玄学问题。3. 计算逻辑用逆波兰表达式Java 实现与两个边界坑3.1 为什么中缀计算器最容易写崩拿到“12×3”你当然知道先乘后加但计算机从左往右读按顺序算会得到 9。解决办法有两种一是维护两个栈实时比较运算符优先级边读边算二是先把中缀表达式转成后缀表达式再一次性求值。我推荐第二种也就是逆波兰表达式。原因很简单转换和求值是两段独立代码每一段都能单独测试。中缀转后缀时遇到数字就输出遇到运算符则根据栈顶优先级决定是否弹栈后缀求值时遇到数字入栈遇到运算符则弹出两个数计算再把结果压回栈。整个过程没有括号的递归没有对“当前该不该算”的状态判断。表达式可以包含十进制小数所以解析要用Double.parseDouble但这里藏着两个坑一是String转double前必须 trim去掉用户手滑输入的空格二是连续输入“12.”这种以点结尾的数parseDouble(12.)在 Java 里居然能过但会丢掉小数点显示时就不对。后面小节会专门处理显示层。3.2 逆波兰实现转后缀、求值、一次性处理小数的 Java 代码第一步写一个独立工具类ExpressionEvaluator.java放在和 MainActivity 同级的包下。代码如下import java.util.ArrayList; import java.util.List; import java.util.Stack; public class ExpressionEvaluator { public static double eval(String expr) { ListString rpn toRPN(expr); return calculate(rpn); } private static ListString toRPN(String expr) { ListString output new ArrayList(); StackString ops new Stack(); String num ; for (int i 0; i expr.length(); i) { char c expr.charAt(i); if (Character.isDigit(c) || c .) { num c; } else { if (!num.isEmpty()) { output.add(num); num ; } if (c () { ops.push(String.valueOf(c)); } else if (c )) { while (!ops.isEmpty() !ops.peek().equals(()) { output.add(ops.pop()); } ops.pop(); } else if (-*/.indexOf(c) 0) { String cur String.valueOf(c); while (!ops.isEmpty() priority(ops.peek()) priority(cur)) { output.add(ops.pop()); } ops.push(cur); } } } if (!num.isEmpty()) { output.add(num); } while (!ops.isEmpty()) { output.add(ops.pop()); } return output; } private static int priority(String op) { if (*/.contains(op)) return 2; if (-.contains(op)) return 1; return 0; } private static double calculate(ListString rpn) { StackDouble stack new Stack(); for (String token : rpn) { if (-*/.contains(token)) { double b stack.pop(); double a stack.pop(); switch (token) { case : stack.push(a b); break; case -: stack.push(a - b); break; case *: stack.push(a * b); break; case /: stack.push(a / b); break; } } else { stack.push(Double.parseDouble(token)); } } return stack.pop(); } }逻辑说明toRPN里num字符串负责拼接多位小数遇到运算符时先把手里的数字丢进输出列表再处理运算符栈。括号处理是重点遇到右括号时要把左括号之前的所有运算符全部弹到输出里左括号本身弹掉但不进输出。calculate里的弹栈顺序也很关键先弹b再弹a对应二元运算的左操作数和右操作数。如果你写成a - b的顺序反了计算结果会差一个符号这也是新手最容易翻车的位置。除法同理a是被除数b是除数顺序错了 8÷4 会算出 0.5。这段代码没有处理表达式开头是负号的情况比如输入“-53”程序会把减号当成二元运算符最终栈里只剩下一个数字没法配对就抛异常。解决办法在 MainActivity 里接到输入后判断字符串第一个字符是不是“-”如果是在前面补一个 0 再传给求值器比如“-53”变成“0-53”结果一致语义也没变。3.3 精度问题0.10.2 不是 0.3逆波兰表达式计算出来是double但double的二进制浮点表示天生无法精确表示比如 0.1所以 0.10.2 的结果不是 0.3而是 0.30000000000000004。这个在计算器上非常扎眼用户会觉得你的 app 有 bug。常见的做法是在显示层格式化而不是在计算层处理。计算层保持double显示层用BigDecimal或者DecimalFormat控制输出位数。计算器场景一般保留 8 到 10 位小数就够用import java.text.DecimalFormat; public static String formatResult(double value) { DecimalFormat df new DecimalFormat(0.#########); return df.format(value); }逻辑说明0.#########表示最多保留 9 位小数末尾的 0 不显示。除不尽的循环小数比如 1÷30.3333333333会被截到 9 位正常使用不会发现精度损失。如果你用df.format(0.30000000000000004)得到的是0.3用户就看不到那一长串尾巴了。这里的另一个坑是DecimalFormat默认会做四舍五入不是截断。对计算器来说四舍五入是符合直觉的因为 1÷3 显示成 0.333333333 和显示成 0.333333334 用户都能接受但如果你做的是进制转换之类需要精确截断的场景就不能用这个格式。4. 按钮事件绑定与输入状态机一个监听器处理全部按键4.1 布局文件里每个按钮都写 android:onClick 会怎样有一种偷懒写法是在 XML 里给每个 Button 写android:onClickonClick然后在 Activity 里写一个同名方法接收View参数。这样代码量确实少但缺点也很明显一旦方法名写错运行到点击时才崩溃编译期不报错而且同一份布局文件如果被多个 Activity 复用还得保证每个 Activity 都有同名方法。这个方案对新手不友好。我一般直接在 Java 代码里统一绑定。在MainActivity的onCreate里把数字按钮和运算符按钮归成两组ListButton循环设置监听器public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView tvDisplay; private String currentInput ; private boolean isLastResult false; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvDisplay findViewById(R.id.tv_display); int[] numberIds {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}; for (int id : numberIds) { findViewById(id).setOnClickListener(this); } int[] opIds {R.id.btn_plus, R.id.btn_minus, R.id.btn_mul, R.id.btn_div, R.id.btn_clear, R.id.btn_dot, R.id.btn_eq, R.id.btn_back}; for (int id : opIds) { findViewById(id).setOnClickListener(this); } } Override public void onClick(View v) { int id v.getId(); if (id R.id.btn_clear) { currentInput ; tvDisplay.setText(0); return; } if (id R.id.btn_eq) { // 计算结果 } // ... } }这里关键是implements View.OnClickListener配合setOnClickListener(this)所有按钮的点击事件都汇到同一个onClick。好处是只有一个入口后续加退格、括号之类的新按钮只需要在onClick里加一个if分支。逻辑说明放在代码之后findViewById循环里用了两个数组分别装数字和运算符以后要加一个取反按钮就往数组里加一个 id。4.2 连续按小数点、重复运算符、除数为 0 的处理计算器最容易被用户按出问题的是表达式不合法。比如用户连按两次小数点得到“1..2”Double.parseDouble直接崩溃再比如用户按完“5 ”马上又按“3”表达式会变成“5 3”这个其实没问题但用户按完“5 ”再按“×”表达式会变成“5 ×”求值器遇到连续运算符会直接崩。处理方案不是把逻辑写在求值器里而是写在按钮输入的入口处。每按一个数字先判断当前输入串最后一个字符是不是运算符或左括号如果是且新按的也是运算符就替换掉最后一个字符而不是追加。每按一个小数点先判断当前正在输入的数字里是否已经有点判断方式是从表达式最后一个运算符位置往后查如果已含点就忽略本次点击if (id R.id.btn_dot) { int lastOpIndex Math.max(currentInput.lastIndexOf(), Math.max(currentInput.lastIndexOf(-), Math.max(currentInput.lastIndexOf(*), currentInput.lastIndexOf(/)))); String tail currentInput.substring(lastOpIndex 1); if (tail.contains(.)) { return; } if (tail.isEmpty()) { currentInput 0.; } else { currentInput .; } tvDisplay.setText(currentInput); }这里用lastIndexOf找到最后一个运算符的位置substring取出最后一个操作数如果里面已经有点就忽略点击。如果尾数为空说明用户刚按完一个运算符就按了点此时要补一个“0.”否则表达式变成“5 .”也是非法的。除数为 0 的处理放在求值器外面。在onClick里按下等号后先调ExpressionEvaluator.eval但用try-catch包住if (id R.id.btn_eq) { if (currentInput.isEmpty()) return; try { double result ExpressionEvaluator.eval(currentInput); tvDisplay.setText(ExpressionEvaluator.formatResult(result)); } catch (ArithmeticException | NumberFormatException e) { tvDisplay.setText(错误); } }ArithmeticException在 Java 里不会因为除零而触发double 除零得到 Infinity真正会出现的是EmptyStackException和NumberFormatException所以 catch 里写RuntimeException更稳妥。你可以把 catch 的粒度放大直接 catch 所有运行时异常这样任何非法表达式都只显示“错误”而不是让 app 闪退。4.3 按“”之后再按数字是继续算还是新开一局这是计算器交互里最影响体验的细节。用户算完“123”接着按“4”期望看到的是新表达式从 4 开始而不是“34”。所以在按下等号并拿到结果后要设置一个标志位isLastResult然后在数字按钮分支里判断if (id R.id.btn_0 id R.id.btn_9) { if (isLastResult) { currentInput ; isLastResult false; } currentInput 3; // 实际是从按钮文字里取 tvDisplay.setText(currentInput); }逻辑说明isLastResult在等号分支里置为 true在数字点击时如果为 true说明上一次已经算完新输入要覆盖而不是追加。运算符点击时不用清空因为“3 ×”是在结果 3 的基础上继续运算这是符合用户预期的。但注意如果用户按完等号后直接按运算符再按数字表达式会变成“[新结果] × 数字”此时isLastResult要在运算符按下时重置为 false否则第二次按数字时会被误判为“新开一局”。这三个状态位currentInput、isLastResult、tail.contains(.)就构成了计算器的输入状态机。所有入口都在同一个onClick方法里状态机维护起来不难。如果以后要加括号按钮只需要在运算符分支里把()也当作合法输入并检查括号匹配求值器里的栈已经支持了。5. 避坑记录入门计算器也会踩的五个真坑5.1 模拟器上按键间距忽大忽小按钮像是被拉伸后又挤回来现象Preview 里 4×4 键盘整整齐齐跑到虚拟机里第一行按钮很宽第二行又很窄按钮之间缝隙不对称。原因GridLayout 里只写了layout_columnWeight1没有写layout_rowWeight1或者只写了行权重没写列权重。GridLayout 与 LinearLayout 的 weight 机制不同行和列要分别指定拉伸方向少一个维度那一维度就按内容高度排列按钮文字大小不一致时就会出现锯齿状排列。解决给 GridLayout 下每个 Button 同时写layout_columnWeight1和layout_rowWeight1并且把 GridLayout 的layout_height设成0dp配合外部 LinearLayout 的layout_weight。如果已经全写了还是乱检查是不是把layout_columnSpan误写成了layout_gravityfill_horizontal之类——这两个属性和按键无冲突但会改变跨列布局。5.2 编译通过但点按钮没反应连日志都不打现象在 Activity 里写了setOnClickListener运行后模拟器点击按钮界面没有任何变化也没有报错。原因最常见是布局文件里按钮 id 与findViewById里写的 id 不一致。比如 XML 里写id/btn_mulJava 里写findViewById(R.id.btn_mul)但 XML 里某个按钮的 id 在复制粘贴时改成了btn_multiply编译时 R 类不会报错因为两个 id 都存在只是你绑定错了对象。另一种可能是重复调用了setContentView后面的布局覆盖了前面的导致你绑定的是旧布局里的按钮。解决在onClick方法开头加一行Log.d(Calculator, clicked: v.getId())然后到 Logcat 里过滤 Calculator 标签。如果点击没打日志说明监听器没绑上如果打出来的 id 和预期不一致去 XML 里核对每个按钮的id命名。强迫自己在 XML 写完按钮后立刻检查 id 列表是最省事的消灭此类问题的方式。5.3 0.20.7 显示成 0.8999999999999999现象表达式“0.20.7”计算结果在 TextView 上显示一长串小数。原因double的底层浮点表示无法精确存储 0.2 和 0.7误差在运算中累积。这不是 Android 的问题是 Java 的问题C、Python 同样存在。解决显示层统一走formatResult方法保留 9 位小数四舍五入。注意不要在计算层用BigDecimal因为BigDecimal的除法需要传精度处理周期小数很麻烦对入门项目没必要。如果你确实想精确计算可以用BigDecimal.valueOf(double).setScale(4, RoundingMode.HALF_UP)替换DecimalFormat但表达式求值的中间结果也要用 BigDecimal整个过程代码会膨胀不少。计算器显示 9 位小数已经足够。5.4 打出来的 APK 装到手机上提示“解析失败”或“未安装应用”现象用 Android Studio 的 Build Build APK 生成 app-debug.apk传到手机后点击安装系统弹“解析包出现问题”。原因低版本系统上安装 targetSdk 31 的 APK 会出现这种情况尤其是 Android 7 以下的旧手机还有一种情况是 APK 是用 debug 签名签的部分国产 ROM 对 debug 签名的应用有安装限制。另外如果你开启了 Instant Run 或者从Build Build Bundle(s)里生成了 .aab那本身就不是 APK传到手机当然装不了。解决入门阶段直接用Build Build APK(s)产出在app/build/outputs/apk/debug/app-debug.apk。用一个支持 Android 10 以上的手机测试或者干脆用命令行adb install app-debug.apkadb 安装会输出更具体的错误信息比如 INSTALL_FAILED_UPDATE_INCOMPATIBLE 表示手机上已有签名不同的旧版本。后者对应的解决方案是卸载旧应用再装。5.5 Activity 频繁闪退Logcat 里全是 ClassNotFoundException现象启动 app 瞬间闪退Logcat 提示类找不到指向你写的工具类。原因新写的ExpressionEvaluator.java类没有保存到正确的包目录下。入口类找工具类靠包名定位如果你把类文件放在了com.example.calculator之外的目录比如java/com/example/calculator/utils而 Activity 里没有加import就会在运行时找不到类。编译期 AS 有时候不强制报错运行时才暴露。解决检查ExpressionEvaluator.java的包名声明和所在目录结构是否一致。按 Android Studio 提供的项目视图Java 文件要放在app/src/main/java/com/example/calculator/下与 MainActivity 同级这样无需 import。如果放在了子目录在 MainActivity 顶部加import com.example.calculator.ExpressionEvaluator;。每次新建类文件后手动看一眼文件头的package行这是很多人忽略的血泪经验。6. 把 APK 装到手机上的验证方法与一个值得改的小细节6.1 用命令行构建并安装三步验证你的 app 真能跑Android Studio 界面点一遍 Build 很多人都会但命令行构建能让你对产物路径有明确认知。打开项目根目录执行./gradlew assembleDebug构建产物在app/build/outputs/apk/debug/app-debug.apk。第二步连上开启 USB 调试的手机执行adb install -r app/build/outputs/apk/debug/app-debug.apk-r表示覆盖安装。第三步启动应用adb shell am start -n com.example.calculator/.MainActivity。如果手机屏幕上弹出计算器界面说明从源码到安装的整条链路都通了。这条链路里最容易失败的环节是 adb 没识别到设备先执行adb devices看一眼是否有 unauthorized 提示有就去手机上点允许调试弹窗。6.2 把等号后的结果接着参与运算一行代码做初值交换在onClick等号分支里计算完结果后不要直接清空输入串而是把结果字符串暂存起来当用户再按运算符时判断当前输入串是否等于暂存结果如果是清空输入串后只保留运算符后面新输入的数字。这样 336再按“×”再按 2 再按“”得到的是 12 而不是 6×2 的错乱结果。新手往往忽略这一点但做计算器到第二版时一定会被用户挑战。6.3 从计算器到下一个项目把你的键盘布局改成汇率换算做完计算器下一步最顺手的练手项目是汇率换算器同一套 GridLayout 键盘把运算逻辑换成查表换算再引入Spinner选择货币就把 Activity、布局、事件、数据解析全部复习了一遍。我第一次完整跑通这两个项目之后再去看 RecyclerView 列表和网络请求心里就完全不慌了因为知道一个 app 的代码再怎么复杂入口还是那套按钮到回调的链路。这个教训后来帮我省了大量在网上搜“为什么我的 app 一打开就闪退”的时间希望你也能从这篇笔记里获得同样的底气希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →