
1. 项目盘点这个科学计算器到底做了什么先说我翻出来的这个项目。这是一个基于Android Studio开发的科学计算器软件源代码语言用的是Java工程结构完整核心计算逻辑没有引入任何第三方库完全是自己写的表达式解析引擎。它覆盖的功能不只有加减乘除三角函数、对数、开平方、幂运算、阶乘、取余、正负号切换这些科学计算里常用的操作都在里面用户直接输入“sin(30)sqrt(16)”这种自然表达式按一下等号就能得到结果。我为什么专门把它拎出来写一篇博文是因为这类项目在网上的资源其实非常多但质量参差不齐。大多数教程和源码停留在“能跑”的层面界面按钮歪歪扭扭、按压没有反馈、表达式解析逻辑一塌糊涂甚至有人直接调用系统自带的“eval”遇到括号和负号就崩。真正把从工程搭建、界面布局到算法实现全链路讲清楚的很少。所以这篇内容我会按一个可复现的完整项目来拆适合三类人看正在做Android课程设计的学生、刚入门想练手的小白、以及想把这类计算器项目拿过去移植改造再做二次开发的开发者。这个项目的亮点有三个。第一表达式解析完全手写基于调度场算法也就是把中缀表达式转成后缀表达式再求值这是数据结构课上的经典内容写在课程设计里非常加分。第二界面用GridLayout加权重方案来做不管手机屏幕是宽是窄按键都能均匀撑开不会出现小屏挤成一团、大屏空一大片的尴尬。第三所有按键共用一套样式和按压反馈资源想改主题皮肤只需要动一个文件。1.1 功能清单与UI整体观感整个计算器界面分为两个区域上方是显示区下方是按键区。按键加上显示部分一共做了26个控制元素我把功能列成一个表方便大家对照着看分类按键说明基础运算 - * / %双目运算符%是取余幂运算^支持2^10这种写法括号( )支持多层嵌套三角函数sin cos tan参数按角度制输入内部转弧度反三角函数asin acos atan反三角结果按角度制输出对数ln log自然对数与常用对数其他函数sqrt 1/x x!开平方、倒数、阶乘控制键C Del 清空、退格、计算结果辅助键. /-小数点、正负号切换显示区我特意设计成两行。上一行实时显示当前输入的完整表达式下一行展示上一次计算的结果。这样做的好处是用户能随时确认自己输入了什么而不是只有一个孤零零的数字出错时也容易定位。1.2 工程结构与技术栈工程的目录结构大概长这样ScientificCalculator/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/calculator/ │ │ │ ├── MainActivity.java // 界面交互入口 │ │ │ ├── CalculatorEngine.java // 表达式解析与计算核心 │ │ │ └── ButtonClickHandler.java // 按钮事件分发 │ │ └── res/ │ │ ├── layout/activity_main.xml │ │ ├── values/strings.xml │ │ ├── values/colors.xml │ │ ├── values/styles.xml │ │ └── drawable/ │ │ ├── bg_btn_normal.xml │ │ └── bg_btn_pressed.xml │ └── build.gradle └── build.gradle技术栈非常朴素Java 8语法级别UI用XML声明式布局计算引擎纯Java实现。minSdkVersion设为21也就是Android 5.0targetSdkVersion我用的33这个范围基本覆盖市面上主流机型又不太需要处理老版本兼容问题。整个项目除了Android自带的SDK没有任何第三方依赖。2. 环境准备从下载Android Studio到跑通第一个工程这个项目不挑Android Studio版本但很多人一上来就卡在环境上。光下载安装折腾半天、项目导入了却编译不过这些我都经历过所以把环境这一环单独讲透。2.1 Android Studio下载与国内镜像加速下载Android Studio第一选择永远是官网地址是Android开发者官方网站里的Downloads页面那里是最新最干净的安装包。不过国内网络环境下直接从官网拖这个安装包确实慢几百MB的东西经常要挂很久。这种问题不用硬扛国内几个大厂都有官方镜像腾讯、华为、阿里都提供Android Studio安装包和SDK组件的镜像源速度比直连官网快很多。我的个人习惯是小版本更新就直接从官网下大版本或者要补SDK组件的时候用镜像源拉实测下来省时间也稳定。另外注意一个老生常谈的点新版Android Studio基本都自带了JetBrains Runtime不需要额外安装JDK。但如果是老工程、老版本Gradle可能会提示找不到JDK这种情况去File Project Structure里把SDK Location和JDK路径指正确就行。网上还能搜到不少“android studio 3.5版本下载”的诉求一般是老电脑跑不动新版。3.5时代的构建体系和插件都比较旧拿来看老项目可以新写工程就别折腾了直接上最新稳定版少踩兼容性的坑。2.2 首次启动与中文界面设置新版Android Studio默认是全英文界面想改成中文有两个办法。最省事的是在Settings Plugins里直接搜索“Chinese (Simplified) Language Pack”这是JetBrains官方的中文语言包插件装完之后重启就变成全中文界面。如果搜索不到多半是插件仓库源的问题可以手动去插件官网下载对应的zip包再通过设置页齿轮图标里的“从磁盘安装插件”导入。这里我想多说一句设置中文确实降低上手门槛但我建议英文菜单的大概位置也记一下因为以后在网上搜教程、查报错信息绝大多数资料和截图都是英文界面的关键词也是英文的比如Clean Project、Sync Project with Gradle Files。中英文切换壁垒其实是习惯问题两边都认识遇到问题才能更从容。2.3 新建项目的关键参数环境装好后开始实操。打开Android Studio选择New Project模板一定要选Empty Views Activity注意不是Empty Activity因为部分新版本里Empty Activity默认生成的是Compose模板和这个Java加XML的项目对不上。关键参数这么填NameScientificCalculatorPackage namecom.example.scientificcalculatorLanguage选JavaMinimum SDKAPI 21Build configurationGroovy DSL把这些记好不要在中文界面里手滑选了Kotlin或者Compose模板否则后面拿到的源码还得重新适配工程配置平白多出一堆事。3. 界面实现布局、样式与可复用的自定义按键计算器的UI看起来简单但真做起来有几个细节特别重要按键平均分布、按压要有反馈、适配不同屏幕尺寸、代码不能冗余。这一节我把方案和踩坑点都过一遍。3.1 整体布局为什么选GridLayout而不是TableLayout计算器的按键天然是网格结构最直接的想法是用TableLayout或者GridLayout。我最终选GridLayout核心原因是它能用layout_columnWeight和layout_rowWeight让按键按比例均分而TableLayout对行内拉伸支持不好每个按钮都要单独处理layout_weight26个按钮写下来全是重复劳动。具体布局是这样的最外层是垂直LinearLayout上半部分放显示区的TextView下半部分放GridLayoutcolumnCount设为5、rowCount设为6。每个按键通过权重属性在水平方向各占五分之一、垂直方向各占六分之一这样不管屏幕宽度是320dp还是420dp按键都能均匀撑满整块区域天然适配不同机型。3.2 按键的按压反馈与圆角样式很多课设计算器的按钮用的是系统默认样式灰不溜秋按下去没有任何视觉变化显得很粗糙。我给这个项目做了两个drawable资源一个是普通状态的浅蓝底圆角按钮一个是按压状态的颜色加深然后在styles.xml里定义一个CalcButton样式把背景指向一个selector选择器style nameCalcButton item nameandroid:layout_width0dp/item item nameandroid:layout_height0dp/item item nameandroid:layout_columnWeight1/item item nameandroid:layout_rowWeight1/item item nameandroid:layout_margin4dp/item item nameandroid:backgrounddrawable/bg_btn_selector/item item nameandroid:textSize18sp/item item nameandroid:textColor#FFFFFF/item /stylebg_btn_selector.xml里用selector包裹两个shapeselector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_pressedtrue shape solid android:color#1E88E5/ corners android:radius8dp/ /shape /item item shape solid android:color#0D47A1/ corners android:radius8dp/ /shape /item /selector这个方案的好处是当用户手指按下按钮时系统根据state_pressed自动切换背景松开时自动恢复完全不用写任何触摸监听代码。成本极低但手感提升非常明显比默认按钮强太多了。3.3 用style批量生成按键减少XML冗余26个控件如果每个都写一遍layout_width、textSize、layout_margin这些属性activity_main.xml会膨胀成几百行的重复代码。正确的做法是把公共属性提取成style就像上面的CalcButton已经包含了大小、权重、背景、字号、颜色每个按钮只需要声明自己的id、text和styleButton android:idid/btnSin stylestyle/CalcButton android:textsin /以后要调整按键大小、圆角、配色只需要改CalcButton一处所有按键全局生效。这就是“自定义组件”思维在XML层面的落地。如果还想更进一步可以写一个自定义View类继承Button在构造函数里统一做初始化但做计算器这种规模的项目style方案已经足够没必要把类层次搞得太复杂。4. 计算引擎表达式解析的完整实现界面只是皮计算核心才是这个项目的灵魂也是最大的坎。很多人在这一步直接抄库但其实手写一个表达式解析器比想象中简单而且能学到很多东西。4.1 自己手写解析器还是直接引第三方库网上很多算法器项目直接引入exp4j、mXparser这类表达式解析库一行代码完成字符串求值。如果你只想快速出功能、不关心原理用库确实是最快路径。但科学计算器几乎是计算机专业学生必做的题目用库等于把最该练的算法部分外包了。我选择手写理由有三个。一是调度场算法是数据结构栈的经典应用写一遍对栈和递归的理解会深很多。二是没有任何第三方依赖APK体积小项目干净。三是不受库的API限制想加什么样的函数规则都可以自己说了算。实际上完整实现也就200行左右的代码性价比极高。4.2 词法分析把字符串切分成Token计算的第一步是把用户输入的字符串“2sin(30)*3”逐字扫描拆成一个一个的Token。Token的类型分四种数字、运算符、函数名、括号。切分逻辑核心是数字字符和小数点累积成完整数字串英文字母累积成函数名其它单个符号直接作为一个Token。需要注意函数名可能是多位的比如sin、cos、sqrt所以要用while循环把连续字母都读进来不能只取一个字符private static ListString tokenize(String expr) { ListString tokens new ArrayList(); StringBuilder num new StringBuilder(); for (int i 0; i expr.length(); i) { char c expr.charAt(i); if (Character.isDigit(c) || c .) { num.append(c); } else { if (num.length() 0) { tokens.add(num.toString()); num.setLength(0); } if (Character.isLetter(c)) { StringBuilder fn new StringBuilder(); while (i expr.length() Character.isLetter(expr.charAt(i))) { fn.append(expr.charAt(i)); i; } i--; tokens.add(fn.toString()); } else { tokens.add(String.valueOf(c)); } } } if (num.length() 0) { tokens.add(num.toString()); } return tokens; }这段代码有个容易踩坑的地方函数名读取的时候内部while循环会把i推到字母末尾所以外层for循环里要执行一次i--否则会跳过字符。写完建议单步调试走一遍特别是有连续函数嵌套的情况。另外一个绕不开的问题就是负号。用户输入“-53”或者“2*(-3)”时减号既可以是双目运算符也可以是负号。我的处理方式是在转后缀阶段判断如果减号出现的位置应该是一个操作数就把它当成负号处理实质是往输出里加一个“-1”再往运算符栈里压一个“*”把负号统一转成“乘以-1”。4.3 调度场算法中缀表达式转后缀表达式中缀表达式是给人看的后缀表达式是给计算机算的。转换过程用到两个数据结构一个输出列表保存后缀结果一个操作符栈临时存放运算符。规则也不复杂数字直接进输出列表函数名和左括号压栈右括号出现时把栈里直到左括号的元素全部弹出到输出列表左括号丢弃如果左括号上面还有函数名也一并弹出新运算符的优先级小于等于栈顶优先级时先把栈顶弹出到输出再把自己压栈表达式扫描完之后把栈里剩余元素全部弹出。我用“234”画了一张表帮助理解数字2直接输出运算符“”优先级高于“”先压栈遇到数字4后结束最后把栈里的“”和“”依次弹出最终得到后缀表达式“2 3 4 * ”。计算时先算34得到12再算212得到14完全符合人脑的优先级逻辑。实际实现时还要考虑未匹配括号和负号。完整代码如下private static ListString toPostfix(ListString tokens) { ListString output new ArrayList(); DequeString stack new ArrayDeque(); boolean expectOperand true; for (String token : tokens) { if (isNumber(token)) { output.add(token); expectOperand false; } else if (isFunction(token)) { stack.push(token); expectOperand true; } else if (token.equals(()) { stack.push(token); expectOperand true; } else if (token.equals())) { while (!stack.isEmpty() !stack.peek().equals(()) { output.add(stack.pop()); } if (stack.isEmpty()) { throw new IllegalArgumentException(括号不匹配); } stack.pop(); if (!stack.isEmpty() isFunction(stack.peek())) { output.add(stack.pop()); } expectOperand false; } else { if (token.equals(-) expectOperand) { output.add(-1); stack.push(*); continue; } while (!stack.isEmpty() isOperator(stack.peek()) getPriority(stack.peek()) getPriority(token)) { output.add(stack.pop()); } stack.push(token); expectOperand true; } } while (!stack.isEmpty()) { output.add(stack.pop()); } return output; }重点解释expectOperand这个标记。它表示当前正在期待一个操作数还是期待一个运算符。初始是true遇到数字、右括号之后变成false遇到左括号、函数名、运算符之后变成true。这样在判断负号时只需要看expectOperand是否为true就能判定当前位置是不是需要负号比看上一个字符要准确得多。4.4 后缀表达式求值与科学函数映射后缀表达式求值就非常简单了准备好一个数字栈从左到右扫描遇到数字压栈遇到运算符或函数就从栈里弹出对应数量的操作数算完把结果再压回去最后栈里剩下的唯一元素就是结果。private static double evalPostfix(ListString postfix) { DequeDouble stack new ArrayDeque(); for (String token : postfix) { if (isNumber(token)) { stack.push(Double.parseDouble(token)); } else if (isFunction(token)) { double arg stack.pop(); switch (token) { case sin: stack.push(Math.sin(Math.toRadians(arg))); break; case cos: stack.push(Math.cos(Math.toRadians(arg))); break; case tan: stack.push(Math.tan(Math.toRadians(arg))); break; case ln: stack.push(Math.log(arg)); break; case log: stack.push(Math.log10(arg)); break; case sqrt: stack.push(Math.sqrt(arg)); break; default: throw new IllegalArgumentException(未知函数: token); } } else { 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; case ^: stack.push(Math.pow(a, b)); break; case %: stack.push(a % b); break; default: throw new IllegalArgumentException(未知运算符: token); } } } return stack.pop(); }三角函数这里有个特别容易踩的坑Math.sin接收的是弧度制参数而用户习惯输入角度制。我这版统一做了角度转弧度所以输入sin(30)得到0.5符合直觉。如果你更想做成纯弧度制把Math.toRadians那一层去掉即可但一定要在界面上标清楚否则用户会以为程序算错了。阶乘我单独说明一下。“x!”按钮点击后直接在表达式末尾追加!词法分析时把!当成一个独立Token转后缀阶段遇到!直接输出到后缀序列因为它是单目后缀运算符天然跟在操作数后面。求值的时候如果碰到!就弹出栈顶数字做阶乘再压回去。这里建议限制输入范围阶乘超过20以后double的精度已经跟不上了。5. Activity交互与代码组织引擎写完下一步是把它和界面接起来。这个环节代码量不大但特别考验组织能力处理不好MainActivity会膨胀成一个大杂烩。5.1 按钮点击的统一分发26个按钮如果每个都单独写一个OnClickListener代码会变成一片重复模板。我的做法是把数字键和功能键分开处理。功能键每个逻辑不同比如C、Del、在onClick里用switch分支处理。数字键和运算符键逻辑相似直接用循环批量设置监听器。这里有个省事的技巧XML里给数字键统一命名成btn0到btn9代码中用getIdentifier批量查找for (int i 0; i 9; i) { int resId getResources().getIdentifier(btn i, id, getPackageName()); Button btn findViewById(resId); btn.setOnClickListener(this); }这样10个数字键只用了三行代码就全部绑定完毕省去了写10个findViewById的麻烦。getIdentifier会有极小的汇总查找开销但点击事件这种低频操作完全无所谓。不过要注意这个方法依赖资源命名规律如果项目里资源命名不规整还是老老实实手写findViewById更稳。5.2 表达式合法性校验与异常兜底用户输入不可能是完全合法的比如空表达式直接按等号、表达式里出现连续两个运算符、括号不匹配。这些情况如果直接交给引擎处理很可能抛异常闪退。我做了三层防御第一层输入阶段拦截。等号按键只有在当前表达式非空、最后一个字符不是运算符时才允许触发计算。第二层引擎抛异常。CalculatorEngine里对不合法Token抛IllegalArgumentException并带出错描述。第三层Activity层catch。计算入口用try-catch包住捕获异常后在结果显示行显示“表达式错误”程序不崩溃。三层加起来用户几乎不可能把程序弄崩溃。这在课程设计答辩或者作品展示时是非常加分的稳定性表现。5.3 用好抽取方法快捷键整理代码项目写大了MainActivity很容易膨胀建议养成“抽取方法”的习惯。Android Studio的抽取方法快捷键是CtrlAltMWindows系统是CtrlAltMMac是CmdOptionM。用法很简单选中一段代码按下快捷键输入新的方法名IDE会自动把这段代码搬到独立方法里并把原来的位置替换成方法调用。我在整理这个项目时把“更新表达式显示”“计算结果”“处理退格”“处理清空”四个逻辑块分别抽成了独立方法onClick主体从80行瘦身到30行可读性好非常多。这个方法平时写代码也用得上尤其是从网上抄来一大段代码想整理归类时比手动剪切粘贴快太多。6. 问题排查与移植经验写项目的过程就是不停踩坑的过程。下面这些问题都是实际遇到过的高频坑整理成一个速查清单给大家。6.1 资源重复错误“资源重复错误”是Android Studio里非常经典的一个坑报错信息通常长这样Error: Attribute XXX has already been defined或者Duplicate resources。出现原因基本都是同一个资源被定义了两遍。最常见的场景有两个。一是从网上下载项目后别人工程里已经有同名资源你把整个res文件夹覆盖粘贴进来没有先删旧的。二是多模块项目里两个module的同名资源合并冲突。解决分三步看Build窗口的报错信息里面会带资源路径顺着路径找到重复文件或同名定义删掉重复项只保留一份。如果是values目录下的同名item比如两个styles.xml都定义了CalcButton就把后出现的或者内容不全的那个删掉执行Build Clean Project再重新Build。如果清理之后还是报同样的错多半是增量编译缓存出问题了直接File Invalidate Caches / Restart让IDE重建缓存和索引。这个操作不会丢任何代码只是重建索引可能要花几分钟遇到诡异问题先试这个。6.2 编译不过的常见原因项目导入新环境后编译失败最普遍的原因不是代码错误而是Gradle版本不匹配。报错里出现“Gradle sync failed”或者“Minimum supported Gradle version is X.X”基本就是版本对不上。解决办法是打开gradle-wrapper.properties文件调整distributionUrl里的Gradle版本号再在build.gradle里把Android Gradle Plugin版本改成匹配的版本。经验规律是新版Android Studio打开老项目大概率会提示升级Gradle直接点升级按钮就好老版本AS打开新项目就需要手动降级相对麻烦。我的建议是先把本机Android Studio升到最新稳定版然后让AS自动去适配旧项目。这是最省力的路径比自己手工去查版本对照表靠谱。6.3 项目移植到其它电脑的正确姿势把项目从一台电脑搬到另一台电脑最忌讳把整个目录原封不动压缩打包传过去。因为本地配置文件、build目录、.gradle缓存这些带着本机路径和机器信息的东西过去了只会产生一堆报错。正确做法是只拷贝这些必要文件ScientificCalculator/ ├── app/ │ ├── src/ // 所有源码和资源 │ └── build.gradle // 模块构建配置 ├── build.gradle // 项目级构建配置 ├── settings.gradle // 工程设置 ├── gradle.properties // Gradle属性 └── gradle/ └── wrapper/ └── gradle-wrapper.properties新电脑上打开项目后不要着急等Gradle自动同步如果提示SDK缺失就去Project Structure里把SDK Location指到本机SDK路径。移植过来的第一件事永远是执行一次Build Clean Project把带过来的build残留清干净再编译。很多人新环境编译报各种奇怪的错十有八九是旧的build目录惹的祸。6.4 界面适配与数学符号显示适配问题主要体现在两处。一是小屏手机上按键文字挤在一起解决办法是把GridLayout的按钮间隔从4dp调到2dp适当减小textSize这些参数都集中在styles.xml里改一处全局生效。二是部分机型中文字体显示偏粗可以在显示区的TextView上显式设置fontFamily为sans-serif让系统用默认无衬线字体渲染整体会清爽很多。还有一个隐藏比较深的坑界面里要显示“×”“÷”“√”“π”这些数学符号如果项目的源码字符编码不是UTF-8编译时会出现乱码甚至编译失败。Android Studio新版本默认UTF-8基本不会踩到但老项目如果是从GBK编码环境迁移过来的记得在build.gradle里配置好源码编码确保中文字符和数学符号都能正常读取显示。7. 实际效果与我的最终建议项目完整跑起来之后我测试了一批典型输入sin(30)输出0.5sqrt(169)输出132^10输出10245!输出1201/(23)输出0.2多层括号嵌套也能正确计算结果都符合预期。从环境搭建到完整跑通熟练的话两三个小时可以搞定新手花一个晚上也足够。最后分享几点实际体会。第一计算器项目最重要的不是界面多花哨而是算法逻辑严谨。提交之前多测边界情况连按等号、连续输入小数点、除以0、负数开平方根这些场景都要有合理反应而不是崩溃。第二代码注释要适度。类和方法级别的功能注释必须有但没必要每行都注释一遍那样反而显得很不专业。第三如果想在此基础上继续扩展优先级最高的方向是界面主题切换和计算历史记录。这两个功能实现成本低、演示效果好放在作品集里非常加分。还有一个实用建议项目做完之后用Git做版本管理第一次提交时记得排除.local.properties这类带本机路径的配置文件以后换电脑、换版本、给别人参考都会非常从容。计算器虽小但它把一个完整Android项目的环境配置、界面搭建、算法实现、异常处理、工程迁移全部走了一遍。认真做完收获远比想象的多。