Android Studio计算器开发全攻略:从GridLayout布局到中缀转后缀实现
简介面向Android开发初学者这是一套基于Android Studio与Java实现的简易计算器完整工程界面参考MIUI计算器风格适合作为理解Android应用开发全流程的入门项目。压缩包共957个文件约21.35MB涵盖xml布局与主题样式、png图标与背景图片、java业务逻辑源码、class编译产物、json配置以及可直接安装的apk文件类型丰富。已有2191人参与学习下载。工程中通过activity_main.xml设计计算器UI结合ConstraintLayout与其他布局实现自适应排列并用Button、TextView和OnClickListener完成按键交互MainActivity.java中封装了加减乘除等运算方法可处理连续表达式同时涉及styles.xml自定义按钮风格、Logcat调试、构建APK等环节。此外资源中还包含Gradle构建脚本与调试相关文件导入Android Studio后即可运行体验也可在此基础上扩展科学计算、历史记录等功能作为课程设计或毕业设计的实用参考。读者可借此系统练习Android UI设计、事件处理、计算逻辑与资源管理为后续开发积累完整经验。1. 用Android Studio写一个计算器为什么值得你认真做一遍新手的第一个Android项目十有八九是计算器。Android Studio新建工程完成后对着白底黑字的默认界面发呆是常态按钮怎么排列、点击怎么响应、表达式怎么解析、UI怎么做才不像课堂作业。市面上的教程大多只给一个LinearLayout加十几个Button能算但质感离系统自带计算器差得远。这一类以“使用AndroidStudio编写简易计算器精美UI”命名的zip工程核心价值不在抄而在于演示一个小型Android应用该怎么组织布局、样式、状态管理和运算逻辑各归其位。这里按我复现这类工程的经验从空工程到可打包的完整应用拆开讲适合刚学完Activity和布局、想交付第一个作品的人也适合面试前想用一个完整项目证明动手能力的开发者。2. 项目配置与资源文件动手写代码前先管好这三件事2.1 新建工程时的默认值怎么改Empty Views Activity、SDK与语言选择新建项目不少人直接点Next接受默认模板于是拿到一个Compose Material3的骨架。计算器是控件密集、状态简单的应用传统View体系里用GridLayout加Button逻辑比Compose直观得多。非要用Compose当然也能写但为了照着教程和网上现成代码排错我建议新建工程时选Empty Views Activity语言选Kotlin。官方模板里的Compose工程带了大量依赖对新手来说启动时间更长出问题更难定位。Minimum SDK选23还是24取决于你要覆盖的设备。选23可以覆盖Android 6.0以上的全部设备但如果你要用的某个AndroidX库要求API 24起步编译期会报错。计算器这个项目用到的都是基础控件API 23完全够。Target SDK保持Android Studio默认给的当前版本不用刻意降。Package name建议取com.example.calculator。不用纠结正式上架时的域名后面改起来不麻烦如果一开始就取个奇怪的名字后面在代码里搜包名替换反而容易漏。工程创建完记得等Gradle第一次同步结束再改文件否则一边下载依赖一边改配置经常触发莫名其妙的sync失败。第一次sync的耗时和网络关系很大Gradle要拉取Android Gradle Plugin和Kotlin插件耐心等它是值得的。还有一个容易被忽略的点新建工程时Android Studio会让你选择Gradle JDK。默认选项通常是“Embedded JDK”也就是IDE内置的JBR版本。如果本机另外装了JDK 17而工程里的Gradle版本是7.x运行时会报Unsupported class file major version。遇到这种问题在Project Structure里把Gradle JDK改成JDK 17并确认Android Gradle Plugin版本在7.2以上。2.2 colors.xml 与主题精美UI的第一层地基计算器的UI质感不在按钮有多花哨而在颜色和间距是否统一。Android工程默认的colors.xml里只有purple_500、purple_700等Material默认色计算器用不上。新建res/values/colors.xml按深色计算器的标准配色定义六种颜色resources color namecalc_bg#1C1C1E/color color namecalc_display#FFFFFF/color color namebtn_number_bg#333333/color color namebtn_operator_bg#FF9500/color color namebtn_func_bg#A5A5A5/color color namebtn_text_light#FFFFFF/color color namebtn_text_dark#000000/color /resources配色取自主流系统计算器的深色模式背景是近黑的深灰数字键用比背景亮一档的中灰运算符用橙色做视觉强调AC、正负号这类功能键用浅灰。颜色名里的calc_前缀是个人习惯为的是和库里的默认色区分开后面写selector时一眼能认出是自己定义的。注意按钮文字颜色也要在colors.xml里定义数字键上文字是白色功能键浅灰底上用黑色这个对比关系直接决定UI的干净程度。接着改themes.xml。默认主题是Theme.Material3.DayNight带ActionBar。计算器不需要标题栏把主题改成Theme.MaterialComponents.NoActionBar并指定状态栏颜色style nameTheme.Calculator parentTheme.MaterialComponents.NoActionBar item namecolorPrimarycolor/calc_bg/item item nameandroid:statusBarColorcolor/calc_bg/item /styleandroid:statusBarColor只在API 21以上生效而Minimum SDK是23所以不用担心旧版本。Android 15开始强制要求edge-to-edge状态栏默认透明很多以前能用的statusBarColor设置开始失效。如果你用的compileSdk是35运行在Android 15设备上会看到状态栏区域透出后面的布局颜色这时要在onCreate里调用enableEdgeToEdge()并把根布局的fitsSystemWindows设为true。这个细节后面避坑章节还会展开。提示主题的parent不要用Theme.Material3.NoActionBar搭配从旧工程抄来的控件属性。MaterialComponents和Material3的控件样式不完全兼容Button的默认背景和minWidth在两种主题下表现不同混用容易遇到布局对不上。2.3 解压zip工程后怎么打开Gradle、JDK与SDK的三方对齐标题里的zip是一个完整的Android Studio工程。拿到压缩包后正确做法是先解压再用Android Studio的Open按钮选择解压后的根目录让IDE自己识别settings.gradle。不要在欢迎页用Import Project(Gradle)的旧选项也不要新建工程再往进拷文件。工程里的.gradle和.idea目录记录了创建者的本机SDK路径和Gradle版本你用本地新版Android Studio打开时IDE会提示Gradle版本不符按提醒升级或降级到对应版本即可。最常见的失败是sync时报错Could not find com.android.tools.build:gradle:7.4.2。这时检查三处File菜单的Project Structure里SDK Location是否指向你机器上的SDKgradle-wrapper.properties里的distributionUrl是否指向能访问的Gradle版本Gradle JDK设置是否和本地安装的JDK大版本匹配。Android Gradle Plugin 7.x要求JDK 118.x要求JDK 17版本对不上时sync会卡在很模糊的提示上。这本质上不是工程代码的问题而是编译链路的版本匹配问题。还有一点zip工程如果是从网上下载的Windows系统解压时可能出现文件路径过长或带有非法字符的情况。Android Studio打开后界面空白或某些资源文件读不到先看文件管理器里解压是否完整。macOS和Linux上解压相对省心Windows用户建议用7-Zip解压到纯英文路径比如D:\Projects\Calculator路径里带中文或空格有时会让NDK或CMake的构建挂掉。解压完成后第一件事不是点Run而是先看Build窗口里sync是否通过sync通过后再运行能少踩一半的坑。3. 精美UI布局把按钮排成系统计算器的手感3.1 三种网格方案对比为什么GridLayout最适合计算器计算器界面本质上是一个5行4列的网格外加顶部一条显示区。实现网格的方式有三种嵌套LinearLayout、GridLayout、ConstraintLayout。直接用LinearLayout嵌套五层每层一个水平LinearLayout代码量看起来少但每个按钮之间要对齐的话每一行的weight和margin都要单独调加一个按钮要数半天括号。GridLayout是Android原生提供的网格容器设置columnCount之后按xml顺序自动填充最贴近计算器的结构。ConstraintLayout适合复杂页面但在这里有点浪费——为20个按钮每个写一组layout_constraint维护成本高于收益。三者的取舍可以归纳成一张表方案优点缺点适用场景LinearLayout嵌套直观、易理解层数深、间距难统一新手练习GridLayout代码少、网格规则行高控制不够灵活计算器、键盘类界面ConstraintLayout定位灵活按钮多时代码冗长不规则仪表盘、自定义界面实际工程里我一般采用GridLayout包一层columnCount4每个按钮设layout_margin6dp让按键之间有均匀缝隙。行间距如果也想要不用额外包View直接在GridLayout的useDefaultMargins属性上做区分这个属性会把默认的系统margin套到每个子View上但不同设备表现不一致建议还是手动写margin。GridLayout还有一个容易翻车的点如果设置了rowCount5但子View数量不是20个比如漏掉一个按钮GridLayout会把最后一行空出来造成底部大块空白。所以这里只设columnCount不设rowCount让系统按子View数量自动分行。3.2 按钮按压反馈一份selector解决80%的UI质感问题原生Button自带Ripple效果但计算器这种高频点按的界面水波纹和深色背景混在一起看不清按下的是哪颗键。精美UI的观感差别很大程度上来自按压时颜色的明确变化。在res/drawable下新建btn_bg_selector.xmlselector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_enabledfalse shape android:shaperectangle solid android:color#1C1C1E/ /shape /item item android:state_pressedtrue shape android:shaperectangle solid android:color#FF9500/ /shape /item item shape android:shaperectangle solid android:color#333333/ corners android:radius24dp/ /shape /item /selectorselector里item的顺序是血泪经验state_enabledfalse必须放最前面state_pressedtrue其次不带state的默认item放最后。系统按顺序逐个匹配第一个条件成立的item生效。如果默认item写在前面它会拦截所有状态按压反馈就永远不出现。按钮按下变橙色松手变回原来的灰色视觉反馈和系统计算器一致。要做到这个效果还需要在Button上设置android:stateListAnimatornull。Android 5.0以上Button默认带一个高度抬起的动画selector的颜色变化会在按下瞬间被动画覆盖两者叠加看着像按钮在抖。圆角corners的radius是24dp不是随意取的和按钮高度的比例有关——按钮高度约64dp时24dp的圆角让边缘接近半圆但保留一点方感太圆会显得偏女性化太小则生硬。更精细的做法是给数字键、运算符键、功能键分别写selector因为它们的底色和按压色不同。数字键按住变浅灰运算符键按住变深橙功能键按住变深灰。实现上只是把上面selector里的颜色换掉然后每个Button设置不同的background即可。功能键上的文字是黑色还需要单独指定android:textColor。我见过一些工程的按钮背景直接用android:backgroundTint指定颜色按下时的反馈交给Ripple但Ripple在浅色按钮上几乎不可见selector仍然是计算器这类界面的可靠方案。3.3 显示区TextView字体、对齐与自动缩小的三件套计算器顶部显示区看似只是一个TextView但难点在于数字多了怎么办。如果不做处理一行放不下时会换行整个布局塌掉。正确配置是限高、右对齐、自动缩小字号TextView android:idid/display android:layout_widthmatch_parent android:layout_height120dp android:gravityend|center_vertical android:maxLines1 android:textColorcolor/calc_display android:textSize64sp app:autoSizeTextTypeuniform app:autoSizeMinTextSize24sp app:autoSizeMaxTextSize64sp /自动缩小的属性在API 26以上原生支持为了兼容更低版本要使用app:前缀的AppCompatTextView。如果你在xml里写了android:autoSizeTextType它在旧设备上会被忽略这就是为什么明明照着文档写了却在小屏手机上不会缩号。显示区的字体建议用sans-serif-medium即FontFamilysans-serif-medium数字的笔画比默认的sans-serif略粗在深色背景上更清晰。这里有一个交互细节显示区的文字更新时计算过程表达式和当前输入数字要区分。一般做法是显示区只显示当前输入或结果过程中间表达式放在上方一行较浅的小字。新手工程常常只用一个TextView导致输入“123”时屏幕上只显示一个3用户完全不知道前面按过什么。想做成精致UI至少要加一个processTextView显示表达式再用一个resultTextView显示当前值。两行的高度差和字体色差都源于这一步。processTextView放上方的布局顺序是固定的把它放在GridLayout之前高度设40dp左右文字字号20sp、颜色半透明白。4. 计算逻辑落地状态、解析与绑定三层分开写4.1 中缀转后缀不引入第三方库的四则运算解析计算器的核心是表达式解析。网上能搜到不少简易计算器zip其中用eval、ScriptEngine甚至反射调用JavaScript引擎的都有。这类方案代码量最少但要在Android上运行时引入额外库或依赖而且精度和行为不可控。自己写解析器面试时也能聊得清楚毕竟四则运算的解析是每个程序员都应该能写出来的东西。经典做法是中缀转后缀RPN再对后缀表达式求值。判一个数字是否结束靠的是扫描时遇到运算符或字符串结尾。下面是中缀转后缀的Kotlin实现fun infixToRpn(expression: String): ListString { val output mutableListOfString() val stack ArrayDequeChar() val num StringBuilder() for (ch in expression) { when { ch.isDigit() || ch . - num.append(ch) else - { if (num.isNotEmpty()) { output.add(num.toString()) num.clear() } while (stack.isNotEmpty() priority(stack.first()) priority(ch) ) { output.add(stack.removeFirst().toString()) } stack.addFirst(ch) } } } if (num.isNotEmpty()) output.add(num.toString()) while (stack.isNotEmpty()) { output.add(stack.removeFirst().toString()) } return output } fun priority(op: Char): Int when (op) { , - - 1 *, / - 2 else - 0 }这段代码里的ArrayDeque当栈用addFirst入栈removeFirst出栈stack.first()是栈顶。没有处理括号因为简易计算器没有括号键但带括号的表达式如果混进来会被当成普通运算符处理所以调用前要做好表达式清洗。priority函数里等号和其他非法字符返回0是为了让它们在转后缀时被当成低优先级运算符弹出之后求值阶段再报错。转成后缀之后求值就机械了从左到右扫描遇到数字入栈遇到运算符弹出两个操作数算完再入栈fun evalRpn(tokens: ListString): Double { val stack ArrayDequeDouble() for (token in tokens) { when (token) { - { val b stack.removeFirst() val a stack.removeFirst() stack.addFirst(a b) } - - { val b stack.removeFirst() val a stack.removeFirst() stack.addFirst(a - b) } * - { val b stack.removeFirst() val a stack.removeFirst() stack.addFirst(a * b) } / - { val b stack.removeFirst() val a stack.removeFirst() stack.addFirst(a / b) } else - stack.addFirst(token.toDouble()) } } return stack.removeFirst() }注意减法时a和b的顺序后缀表达式3 5 -对应的是3-5而不是5-3所以弹出来时先弹b再弹a。这一步写反是所有RPN实现里最高频的翻车点。除法同理。0.10.2不等于0.3是Java/Kotlin层面逃不掉的精度问题简易计算器可以在显示层做四舍五入保留10位小数写成DecimalFormat(#.##########)可以解决大部分视觉误差。4.2 按钮事件绑定一个接口绑定全部按钮20个按钮逐个写setOnClickListener会占掉onCreate几十行。常见的整洁写法是Activity实现View.OnClickListener用一个id列表绑定在onClick里用when分发class MainActivity : AppCompatActivity(), View.OnClickListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) listOf( 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_dot, R.id.btn_add, R.id.btn_sub, R.id.btn_mul, R.id.btn_div, R.id.btn_eq, R.id.btn_ac ).forEach { id - findViewByIdButton(id).setOnClickListener(this) } } override fun onClick(v: View?) { when (v?.id) { R.id.btn_0 - inputDigit(0) R.id.btn_dot - inputDot() R.id.btn_add - inputOperator() R.id.btn_sub - inputOperator(-) R.id.btn_mul - inputOperator(*) R.id.btn_div - inputOperator(/) R.id.btn_eq - evaluate() R.id.btn_ac - clearAll() else - Unit } } }注意绑定列表里没有放btn_sign正负号和btn_percent百分号因为这两个键的功能依赖当前输入状态如果当前输入为空按了也应该无响应。与其在onClick里判空不如直接不绑定让它们保持不可用状态视觉上更符合系统计算器。这个取舍可以延伸到所有按钮不是所有按钮在所有状态下都可点UI上应该同步反映出来。绑定事件时如果你的compileSdk版本较新findViewById可以被ViewBinding替代布局里所有带id的控件自动生成为同名字段。但对计算器这种控件数量固定的小界面findViewById的样板代码并不多ViewBinding的好处主要体现在列表项或复杂布局上这里不强上。4.3 状态管理三个变量管住输入、表达式和重复等号计算器的状态要管住三样东西正在输入的数字、已经确认的表达式、上一个运算符。很多zip工程翻车就翻在这里它们用一个TextView.text当全局状态每次点击都从TextView里抠字符串再拼回去除数和连续等号会出现错乱。推荐的三个成员变量变量类型作用currentInputString当前正在输入的数字可能含小数点expressionString已确认的左侧表达式如“12”lastOperatorChar?记录等号前一个运算符支持连按等号对应的事件处理在4.2的inputDigit、inputOperator、evaluate中展开。核心逻辑是用户按运算符时把currentInput拼进expression并清空currentInput按等号时把当前输入拼进表达式转后缀、求值、显示结果同时把本次运算符和操作数保留用于连按等号时的二次计算。连续等号“12315再按得18”的实现方式是在evaluate末尾判断lastOperator和lastOperand是否为空如果用户没有输入新数字就重复上一次运算。小数点要单独处理currentInput里已经包含.时再按小数点应该无效。这是输入合法性检查的一部分不能等到解析时报错。这类输入层校验如果做得完整解析层就只需要关心纯数学表达式出错的概率大幅下降。我见过不少新手把“连续按运算符”“按完等号又按数字”这些交互状态全堆在if else里最后代码像意大利面。用三个变量加一个isNewInput布尔值把“当前输入是否是新数字”这个状态单独表示处理起来会清晰很多。5. 计算器必踩的5个坑现象、原因与解决5.1 模拟器卡在starting upApp迟迟不显示现象Android Studio里的模拟器启动后长时间停在“Starting up”的加载界面计算器App装了但打开黑屏或者界面刷新很慢点击按钮有明显延迟。原因模拟器冷启动本身就要拉系统镜像计算器工程体积小不代表系统镜像小。如果AVD配置里Graphics选的是Software渲染走CPUUI界面卡顿是必然的。另一个常见原因是电脑开了Hyper-V或WHPX和Android Emulator自带的HAXM/GVM冲突模拟器进程反复重启。解决在AVD Manager里编辑模拟器配置Graphics改成Hardware - GLES 2.0Boot option改成Cold boot关闭无关的虚拟化软件冲突。实际开发中计算器这类小工程用真机调试更省心用USB连手机开USB调试秒级安装还能真实感受按钮的按压反馈模拟器上的触摸反馈和真机是两回事。如果坚持用模拟器第一次启动时多等几分钟之后的热启动会快很多不要一看到starting up就杀掉进程重来。5.2 软键盘弹出抢占半边屏幕现象在模拟器或真机上每按一次数字键系统软键盘就弹出来把计算器按键挡住大半点其他按键也躲不开。原因TextView有焦点且允许输入法连接。虽然计算器所有输入都靠Button但显示区TextView默认有焦点软键盘被唤起。解决在根布局上加上android:focusabletrue和android:focusableInTouchModetrue让根布局优先持有焦点。同时给activity配置windowSoftInputModestateHidden|adjustNothing明确声明不需要软键盘窗口。这两步加完之后模拟器再打开App就不会有输入法弹出了。注意focusable和focusableInTouchMode要同时设置只设前者在触屏设备上不生效。5.3 除零不崩溃但显示奇怪字符现象按“1÷0”显示Infinity按“0÷0”显示NaN有的机器上还会白屏。原因现代计算器把除法当Double运算0.0做除数不会抛异常IEEE 754标准里规定了结果是Infinity或NaN。显示层直接把Double.toString的结果丢给TextView屏幕上就出现了Infinity。某些库或系统版本对Double.toString(“NaN”)处理异常进而崩溃。解决在evaluate()求值完成后统一判断结果val result evalRpn(rpn) if (result.isNaN() || result.isInfinite()) { display.text 错误 clearAll() return }isNaN和isInfinite是Double自带的方法不需要引入额外库。注意这个判断要放在任何格式化之前比如BigDecimal或DecimalFormat对Infinity的行为不尽相同绕过了等于没拦。清空状态是为了避免用户接着按数字时把“错误”两个字拼进表达式里。5.4 旋转屏幕后表达式清空现象手机上把计算器横过来输了一半的表达式和当前数字全部消失屏幕显示0。原因Activity默认在旋转时被销毁重建成员变量全部归零。计算器没有做状态保存也没有锁定屏幕方向。解决开发学习阶段最省事的办法是在AndroidManifest.xml里给MainActivity加android:screenOrientationportrait。如果希望支持横屏就在activity里重写onSaveInstanceState把currentInput、expression、lastOperator三个变量写进BundleonCreate里再取回。只有锁竖屏时才不需要处理这一层。锁定方向后布局里就不用再考虑横屏的适配对这个小项目来说是最优解。5.5 高DPI和定制ROM下按钮文字截断现象小米、华为等ROM上“AC”“%”文字显示不全按钮背景是圆角但文字被裁掉一半有些ROM上按钮的按压颜色看起来不正常。原因各厂商对系统默认字体做了修改英文字母和中文字符的宽度度量不完全一致。Button的padding在不同ROM上也有差异MaterialButton还自带了inset机制如果不小心给Button设置了过大的minWidth布局权重分配会把可用宽度压缩文字就放不下。解决文字截断优先排查按钮的padding和minWidth。在xml里给Button设置android:padding0dp并用android:minWidth0dp覆盖系统默认值让宽度完全由GridLayout的weight决定。如果用的是androidx.appcompat.widget.AppCompatButton还要注意背景Drawable的inset需要设置android:insetLeft4dp这类inset参数。最后的兜底手段是把字号适度调小比如把运算符键的textSize从28sp降到24sp视觉差别不大但留出了文字内边距。这类问题在模拟器上几乎复现不了只能在真机上验证调试时多拿一台国产ROM的手机有好处。6. 进阶玩法把简易计算器做成能上架的作品拿到一个能跑的zip工程只是开始。想让这个计算器在作品集或面试里立住接下来最值得做的是四件事历史记录、振动反馈、主题跟随、签名打包。历史记录用SharedPreferences存JSON数组就够Room在这里是大炮打蚊子限制最多50条超出就删最早的。存取时可以复用同一个工具类getSharedPreferences(calc_history, MODE_PRIVATE)序列化用org.json里的JSONArray不需要额外依赖。振动反馈复用系统服务即可但注意Android 13以上要检查VIBRATE权限是否被用户关闭否则直接崩溃val vibrator if (Build.VERSION.SDK_INT 31) { getSystemService(VibratorManager::class.java).defaultVibrator } else { Suppress(DEPRECATION) getSystemService(VIBRATOR_SERVICE) as Vibrator } vibrator?.vibrate(VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE))20毫秒的振动时长是短促反馈的常用值超过50毫秒就会感觉手机在震而不是在点按。振动要放在按钮的onClick里而不是onTouch里否则滑动经过按钮也会触发体验会变得很廉价。主题跟随系统是把颜色资源放进values-night目录计算器这种深色为主的界面白天模式反而要重新设计一套浅色配色工作量比想象中大但做完之后“跟随系统”这一条在作品集里是实打实的亮点。签名打包前记得先把keystore备份到项目外的目录我见过太多人换电脑后keystore丢失导致无法更新应用。最后的加分项是写几个JUnit用例把中缀转后缀的边界case铺开除零、连续小数点、连续等号、超大数字的精度。我自己最大的教训就是把测试留到功能全做完才补真到补的时候已经不想动了。从一开始就给计算器写测试后面每改一个按钮的状态逻辑都能立刻看到回归结果。希望帮到你。本文还有配套的精品资源点击获取