news 2026/9/23 10:41:16

Android简易计算器实战:从双栈算法到精美UI开发全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android简易计算器实战:从双栈算法到精美UI开发全解析

简介:面向安卓初学者的完整工程,演示如何用安卓开发环境与Java语言编写一款仿MIUI风格的简易计算器,重点覆盖界面布局、按钮样式、点击事件以及四则运算等核心环节。压缩包共957个文件,大小21.35MB,主要包含292个XML布局与资源文件、276张PNG图片、22个Java源码文件,以及Gradle配置、JAR依赖库、可直接安装的APK等,类型齐全、目录清晰,便于按需查阅。目前已有2191人学习下载。工程内除MainActivity计算逻辑与activity_main布局外,还提供自定义主题样式、构建中间文件及调试版安装包,可对照理解安卓工程的编译与打包流程。通过学习可掌握约束布局、样式定制、事件监听和连续表达式解析等实用技能,项目代码注释完整,适合课程设计、实训作业或开发入门参考,也可作为同类计算器界面设计的模板,对提升安卓开发实战能力很有帮助。尤其适合需要提交安卓课程项目或快速搭建计算器应用的学习者,下载后可直接导入运行,边看边改,快速上手。

1. 别急着解压:先把“计算器.zip”当作一条完整研发链路来拆

直接说结论:AndroidStudio 下载的“简易计算器(精美UI).zip”,真正值钱的不是那几十行加减乘除,而是“界面、事件、状态”三件事在同一个 Activity 里怎么组织。这类 zip 资源包从网盘、论坛转手多次,最大的问题不是代码跑不通,而是工程结构与新版 Gradle 插件不兼容:你辛辛苦苦解压、打开、同步,红灯一排,很多人在这里就放弃了。这篇实战笔记按照“先定产品规格,再写布局,再写逻辑,最后美化与排错”的顺序,把这个计算器项目拆到能复现的程度。适合刚学完控件的在校生、想做课程设计或入门作品的自学者,也适合已经在上班但第一次从 zip 包接手别人项目的初级工程师。目标只有一个:让这个压缩包里的计算器,变成你亲手改过、能讲明白每一步的自己的作品。

2. 先定计算器的“产品规格”:功能清单与布局结构

2.1 简易不等于凑合:把用户故事写成四个规则

打开项目开始敲代码之前,最好先把“计算器应该长什么样、能做什么”写死。常见的错误是边写边加按钮,最后布局换来换去,代码一团乱麻。我一般会把需求钉成四条规则,直接写进代码注释里:

第一,只做整数的四则运算,不做括号、不做幂运算、不做正负号切换。这不是偷懒,而是让核心算法保持足够简单,读者能一眼看懂栈是怎么工作的;第二,用户按“数字键”时,输入区持续追加数字,最多输入 9 位,防止溢出;第三,用户按“运算符”时,如果当前正在输入数字,先把数字“落栈”,再保存运算符;第四,按“=”时,把栈里的数字和运算符按优先级算完,结果显示在输出区,同时保留完整算式在表达式区。

这四条规则回答了“按下去之后发生了什么”这个最根本的问题。很多从网上下载的计算器 zip 包,代码逻辑是用连续 if 处理点击事件,算 1+2+3 没问题,但一遇到 2+3×4 这种混合运算就给出 20,因为它是“按从左到右的顺序”硬算的——第一个加法结果 5,然后乘以 4。所以拿到包之后,先别急着改 UI,先看看算法是“单步累加”还是“带优先级处理”。如果是单步累加,你至少要能说出它的局限,再去考虑是不是要升级成双栈方案。

2.2 用 LinearLayout 拆键盘:权重才是指纹级参数

计算器的经典布局是“上屏 + 下键”,我用一个纵向 LinearLayout 装两个区:上半部分放表达式、结果显示,下半部分放键盘。键盘区继续纵向拆分,每一行是一个横向 LinearLayout,行内的按钮用 layout_weight 均分;行与行之间靠父容器的 weight 比例固定高度,这样屏幕尺寸变化时,键盘能自动拉伸而不是挤成一堆。

以下是布局骨架,四个按键一行,总共五行,代码结构完全对称:

<!-- activity_main.xml 骨架 --> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@color/calculator_bg"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:orientation="vertical" android:gravity="bottom|end" android:padding="20dp"> <TextView android:id="@+id/tv_expression" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:textColor="@color/text_secondary" android:maxLines="1"/> <TextView android:id="@+id/tv_result" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="48sp" android:textColor="@color/text_primary" android:maxLines="1" android:text="0"/> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:orientation="vertical"> <!-- 第一行:C del % ÷ --> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <Button android:id="@+id/btn_clear" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="C" android:tag="clear" android:onClick="onButtonClick"/> <Button android:id="@+id/btn_delete" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="del" android:tag="delete" android:onClick="onButtonClick"/> <!-- 其余按钮按同样规则继续 --> </LinearLayout> <!-- 第 2 ~ 5 行结构完全相同,只是按钮 tag 不同 --> </LinearLayout> </LinearLayout>

这段代码里最关键的是两个layout_weight:外层显示区与键盘区按 2:3 分配高度,保证键盘不挤压结果区;内层按键高度设为 0dp 再配合layout_weight="1",让每一行均分键盘高度。如果你想要 iOS 计算器那种“0 键占两列”的排布,可以把最后一行里的 0 键宽度设为 0dp、layout_weight="2",小数点按钮权重 1,逻辑上没有任何区别,权重相加等于总权重就行。

按钮上的tag属性和onClick是配套使用的。每个按钮的text是给人看的,tag是给代码识别的身份标识。我习惯用语义化字符串而不是数字 id:clear、delete、add、subtract、multiply、divide、equals,以及 num0 到 num9。这样点击监听里不需要 20 个 if 分支去判断view.getId(),一个switch (view.getTag().toString())就能全部收编。

2.3 shape 与 ripple:按下去要有“物理感”

默认 Button 是直角、灰色、带一点阴影的 Material 风格,直接拿来当计算器键帽,视觉效果像“网页里没写样式的按钮”,谈不上精美。我的做法是给每类按键单独写一个 shape 文件,用圆角半径和颜色区分数字键、功能键、运算符键。

<!-- res/drawable/bg_button_operator.xml --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#FF9F0A" /> <corners android:radius="28dp" /> </shape>

shape 文件只定义静态外观,真正的按压反馈还得靠 selector,这个稍后第四章重点说。这里要记住一个容易被忽略的参数:android:stateListAnimator。Android 5.0 之后 Button 默认带高度阴影,按下去时阴影会闪一下,计算器密集连按时眼睛很累。在 selector 方案下,我会在 Button 上写android:stateListAnimator="@null",把系统自带动画关掉,只保留背景变化,手感反而干净。

3. 让按钮“动”起来:统一点击事件与表达式逻辑

3.1 二十个按钮,只写一个监听方法

xml 里已经给每个按钮的android:onClick指定了onButtonClick,所以 Activity 里只需要实现这一个方法。很多老项目的 zip 包会用匿名内部类给每个按钮单独setOnClickListener,代码动辄三百行,维护起来非常痛苦。统一入口的好处是:所有点击行为集中管理,后续加“退格”“括号”时,只需在 switch 里补一个 case。

public class MainActivity extends AppCompatActivity { private final StringBuilder currentNumber = new StringBuilder(); private final StringBuilder expression = new StringBuilder(); private final Stack<Double> numbers = new Stack<>(); private final Stack<Character> operators = new Stack<>(); private TextView tvExpression; private TextView tvResult; private String lastOperator = ""; private double lastOperand = 0.0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvExpression = findViewById(R.id.tv_expression); tvResult = findViewById(R.id.tv_result); } public void onButtonClick(View view) { String tag = view.getTag().toString(); if (tag.startsWith("num")) { inputDigit(tag.substring(3)); } else { switch (tag) { case "add": inputOperator('+'); break; case "subtract": inputOperator('-'); break; case "multiply": inputOperator('*'); break; case "divide": inputOperator('/'); break; case "clear": clearAll(); break; case "delete": backspace(); break; case "equals": calculateResult(); break; default: break; } } } private void inputDigit(String digit) { if (currentNumber.length() < 9) { currentNumber.append(digit); expression.append(digit); updateResult(currentNumber.toString()); } } // 其余方法见后续小节 }

设计时我把“数字输入”与“运算符输入”分成两个方法,因为二者对状态的处理完全不同。数字是追加到currentNumber字符串和表达式串里;运算符则负责清空当前数字缓冲区、触发栈顶计算、把新运算符压栈,属于“切换操作”,混在一个方法里写很容易出现边界遗漏。

这里有个值得注意的点:我把expressioncurrentNumber分开维护,而不是让它们共享同一个 StringBuilder。原因是表达式区要展示完整算式,比如“12+5×3”,而结果区只显示正在输入的数字或最终结果。如果只用一个变量,回退、替换运算符时会非常被动。

3.2 双栈计算:不写逆波兰也能处理乘法优先级

简易计算器最常见的翻车点,是处理“3+4×2”。如果不做优先级,从左到右算答案是 14,但标准答案是 11。要正确处理四则运算又不引入逆波兰表达式,我推荐用“数字栈 + 运算符栈”的双栈方案,这是考研题和编译器课上调度场算法的简化版,代码量不大,逻辑还特别适合新手读。

核心操作是inputOperatorcollapse:输入新运算符时,先把正在输入的数字压栈;然后看栈顶运算符,如果栈顶优先级不低于新运算符,说明栈顶那个运算可以先算完,于是弹栈、弹两个数字、算完压回去;最后把新运算符压栈。等号则负责把栈里剩下的运算符全部算完。

private void inputOperator(char op) { // 当前输入的数字落栈 if (currentNumber.length() > 0) { numbers.push(Double.parseDouble(currentNumber.toString())); currentNumber.setLength(0); } // 处理连续运算符覆盖,比如输入 5 + - 3 if (currentNumber.length() == 0 && !operators.isEmpty()) { if (operators.peek() == '+' || operators.peek() == '-' || operators.peek() == '*' || operators.peek() == '/') { operators.pop(); } } // 栈顶优先级不低于当前运算符时,先把栈顶算了 while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(op)) { collapse(); } operators.push(op); expression.append(op); } private void collapse() { if (numbers.size() < 2) return; double b = numbers.pop(); double a = numbers.pop(); char op = operators.pop(); double result = apply(a, b, op); numbers.push(result); } private double apply(double a, double b, char op) { switch (op) { case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': if (b == 0.0) { runOnUiThread(this::clearAll); return 0.0; } return a / b; default: return 0.0; } } private int precedence(char op) { if (op == '+' || op == '-') return 1; if (op == '*' || op == '/') return 2; return 0; }

逐段说明逻辑。inputOperator的第一步是“清空当前数字”,这一步漏掉会导致数字重复入栈,比如按“12+34”,如果不先把 34 落栈,后面collapse会拿 34 和 12 做运算,变成 12+34+34。第二段 if 处理的是“5 + - 3”这种输入,此时currentNumber为空,若栈顶已经是运算符,直接弹出旧的、压入新的,避免出现连续两个运算符在栈里,导致collapse时 numbers 不足。

collapse每次弹出运算符栈顶和最近的两个数字。这里需要想清楚顺序:先弹出来的是 b,后弹出来的是 a。四则运算不满足交换律,减法尤其敏感,8-3 如果写成 3-8 整个结果就反了。apply内部对除法做了除零保护,这是计算器最基础的安全底线,我直接把clearAll当成异常出口,触发后全部复位,界面回到初始状态。

这套双栈逻辑比“把中缀转后缀再求值”少写一个栈,也不用维护表达式树,对一个目标为“简易”的计算器项目刚好到位。它的短板是暂不支持括号和一元负号,但第四章之后你会发现,这两个功能在实际使用中几乎不会被课程设计的评分老师测试。

3.3 三次“事故现场”:连按运算符、除零与浮点失真

第一类事故是连按运算符,比如“5 + × 3”。如果代码不做任何防护,inputOperator会把 × 和 + 一起压在栈里,按等号时collapse先算 5 与谁运算? nobody。我的处理方式是:在新运算符入栈前,检查currentNumber长度,如果为 0 且栈非空,说明用户刚按过运算符,此时直接弹出栈顶的旧运算符,用新的替换。这个逻辑在上一段代码里已经有,效果是“5 + ×”会被修正为“5 ×”,符合大部分真机计算器的输入习惯。

第二类事故是除零。Android 原生双精度浮点里1.0 / 0.0不会抛异常,结果是Infinity,显示在屏幕上是英文“Infinity”,对中文用户来说既突兀又不专业。更隐蔽的是0.0 / 0.0,结果是NaN,任何后续运算都会被污染:NaN 加上任何数还是 NaN,而且你很难从界面上一眼看出哪里出了问题。所以apply里单独判断 b 是否为 0.0,直接清空界面是性价比最高的处理。

第三类事故是浮点显示。计算0.1 + 0.2,结果不是 0.3,而是 0.30000000000000004。很多新手第一次看到这串长小数,以为是算法写错了,其实是 IEEE 754 浮点表示的天生缺陷。在calculateResult里,我一般用BigDecimal处理最终结果并去掉末尾多余的 0,或者使用格式化输出:

private String formatNumber(double value) { BigDecimal bd = new BigDecimal(Double.toString(value)); return bd.stripTrailingZeros().toPlainString(); }

BigDecimal(Double.toString(value))这一步很关键,不要直接写new BigDecimal(value)。直接传 double,会把二进制浮点数的“脏尾巴”也带进来;先转成字符串,再从字符串构建 BigDecimal,等价于把 0.30000000000000004 重新解析成用户看得懂的 0.3。

4. 精美UI是怎么“长”出来的:主题、selector与配色

4.1 主题选错,再好看的布局也白搭

很多人把这个 zip 包解压后跑起来,发现界面一片惨白,按钮完全没有设计感,第一反应是去改 Button 的背景色。但真正的问题往往出在主题:新版本 Android Studio 创建项目默认使用Theme.MaterialComponents.DayNight.DarkActionBarTheme.Material3,如果你拿到的是老 zip 包的 manifest,里面可能写的是Theme.AppCompat.Light,甚至直接是@android:style/Theme,于是 Material 组件特有的控件外观全部失效。

我的做法是统一使用Theme.MaterialComponents.DayNight.NoActionBar,然后再覆盖几个关键颜色:

<!-- res/values/themes.xml --> <style name="Theme.SimpleCalculator" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <item name="android:windowBackground">@color/calculator_bg</item> <item name="colorPrimary">@color/operator_color</item> <item name="colorSecondary">@color/function_color</item> <item name="android:statusBarColor">@color/calculator_bg</item> </style>

NoActionBar的作用是去掉默认标题栏。计算器这种全屏工具类应用,标题栏只会占高度、破坏沉浸感。我建议在 values、values-night 两份目录里各放一份 themes.xml,DayNight 主题会根据系统深浅模式自动切换,如果你只适配深色计算器,直接在values/themes.xml里把windowBackgroundstatusBarColor写成深色即可。

4.2 按压态、禁用态:用 selector 描述按钮的一生

圆角 shape 只定义了静态外观,按压反馈需要 selector 文件。selector 是一个状态列表,Android 根据按钮当前状态自动选择对应的 drawable:

<!-- res/drawable/bg_button_digit.xml --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <shape android:shape="rectangle"> <solid android:color="@color/digit_pressed" /> <corners android:radius="28dp" /> </shape> </item> <item> <shape android:shape="rectangle"> <solid android:color="@color/digit_normal" /> <corners android:radius="28dp" /> </shape> </item> </selector>

selector 里<item>的排列顺序有讲究:必须把state_pressed放在前面。只有它没有被命中时,才会落到下面那个无状态条件的 item。如果你反着写,按下状态永远被普通状态遮蔽,按压反馈出不来,界面看起来就像“死”的一样。

运算符等号键的 selector 同理,只是颜色换成橙色系。这里要额外补一个细节:Button 默认会强制把英文字母转大写,android:text="del"显示出来是“DEL”。如果你想要小写,必须在 Button 上写android:textAllCaps="false"。这是很多人忽略的“UI 设计死角”。

4.3 四个颜色值见真章:主色、功能色、背景、描边

精美 UI 不是靠堆元素实现的,而是靠克制。计算器这种每个屏只有二十来个按钮的界面,颜色体系越少越高级。我采用 iOS 计算器同款四色方案:背景深色、数字键深灰、功能键浅灰、运算符橙色。

calculator_bg #1C1C1E 整个界面的底色 digit_normal #333333 数字键普通态 digit_pressed #5A5A5F 数字键按压态 function_normal #A5A5A5 功能键(C、del、%) operator_normal #FF9F0A 运算符和等号键 text_primary #FFFFFF 主数字文本 text_secondary #A5A5A5 表达式文本

这套配色的核心逻辑是“明度差”:数字键在背景上要有轻微凸出感,所以用了高对比的深灰;运算符是整屏交互的高频键,用最亮的橙色做焦点;功能键的明度介于二者之间,既不像数字键那么沉闷,也不像运算符那么抢眼。如果你拿到一个 zip 包,发现它的计算器界面花花绿绿,把 Button 背景直接设成了红黄蓝紫,完全可以按上面的表重做一遍。

对了,有一个小技巧是运营商分区。第一行的 C、del 用浅灰,最后一行的等号用橙色,中间运算符用中灰或橙色描边。如果全键盘按钮都是一个颜色,用户需要在心里给按钮“分类”,操作成本就高;颜色一变,手指很快就形成“右上角是清空、右下角是等号”的肌肉记忆。

4.4 为什么按钮看起来“值钱”:间距、字重、禁止AllCaps

颜色只是第一步,真正让按钮产生“高级感”的是间距和字号。计算器按键之间如果贴得太紧密,容易误触,视觉上也显得廉价。建议每个按钮四周留 4dp 左右的 margin,行与行之间不必额外加空,因为 margin 已经提供了呼吸感。

字号方面,结果区 TextView 建议 48sp 起步,表达式区 24sp;数字按钮 32sp,运算符按钮 36sp。这个参差感能让视线自然聚焦到结果区。字重上,数字按钮和结果区用android:textStyle="bold",而表达式区用普通字重,形成“不重要的在过去,重要的在现在”的层次。

另外,很多网上的代码会把 Button 的android:textSizeandroid:layout_height写死,小屏手机上按钮塞不下、大屏手机上字小得可怜。正确做法是:按钮高度交给layout_weight均分,文字字号引用@dimen资源而不是硬编码,这样后续出平板适配,只需要改一个 dimens.xml 文件。

5. 经典避坑合集:ZIP解压、Gradle同步与运行崩溃

5.1 ZIP这一关:GBK乱码、伪加密与命令行解压

先说现象一:下载来的 zip 包在 Windows 上解压,项目里的注释、Java 文件的中文全部乱码,Android Studio 打开后代码一团糟。原因很典型:zip 规范里文件名可以用 UTF-8 编码,也可以用本地编码,国内的打包者很可能用了 GBK。解决方法是解压时强制指定编码,Windows 下的 Bandizip 或 7-Zip 在解压选项里选“GBK”,Linux 上可以这样:

unzip -O gbk 计算器.zip -d calculator

-O gbk让 unzip 按 GBK 解释文件名,解压到calculator目录,之后再用 Android Studio 以 UTF-8 打开项目文件。如果你用了 macOS 自带 unzip,可能不支持-O参数,此时改用ditto -x -k或者直接换用 The Unarchiver 工具更省事。

再说现象二:zip 包解压时提示需要密码,可你明明下载的是免费资源。这可能不是真的加密,而是 zip 伪加密——打包者把压缩包的加密标志位置成 1,数据本体并没有加密。用十六进制编辑器打开 zip,搜索 “PK\x01\x02” 中央目录记录,把通用标志位第 0 个 bit 从 1 改成 0 即可解除。这里有一个通用修复脚本:

import sys with open(sys.argv[1], "rb+") as f: data = bytearray(f.read()) start = 0 pos = data.find(b"PK\x01\x02", start) while pos != -1: flag = data[pos + 8] if flag & 0x01: data[pos + 8] = flag & 0xFE # 清掉 bit0 start = pos + 1 pos = data.find(b"PK\x01\x02", start) f.seek(0) f.write(data)

用法是python fixzip.py your.zip,脚本会把所有中央目录文件头的加密位清掉。注意:如果 zip 是真正的加密包,这样改会破坏文件结构,解压时依然会报错;伪加密则能直接通过。这个脚本我一般只在“资料包打不开”时应急用,正规项目包很少出现伪加密,但搜索引擎搜“zip伪加密”进来的人,八成都在这个坎上。

5.2 版本对应关系:AGP 7.0.4到底配哪个Gradle

重灾区来了。老 zip 包往往是两年前的工程,build.gradle里写着classpath 'com.android.tools.build:gradle:4.2.0',你的 Android Studio 已经把 AGP 升级到 8.x,同步时不是报“旧版本 AGP 不支持”,就是报“需要更高版本的 Gradle”。很多新人的第一个反应是去搜索“AGP 7.0.4下载哪个版本”,然后下载一个完全不对的 Gradle 版本装上,继续报错。

这里说清楚最核心的匹配关系:AGP 和 Gradle 是两个独立但强绑定的系统。AGP 是 Android 构建插件,运行在 Gradle 里;Gradle 版本过低,AGP 的新 API 调不到,版本过高又可能触发兼容性检查报错。常见搭配如下:

AGP 版本 Gradle 最低版本 推荐 JDK 4.2.2 6.7.1 JDK 8/11 7.0.4 7.0.2 JDK 11 7.4.2 7.5 JDK 11 8.1.1 8.0 JDK 17 8.5.2 8.7 JDK 17

当你拿到 zip 包时,先去gradle/wrapper/gradle-wrapper.properties里看distributionUrl写的 Gradle 版本,再看项目根build.gradle里的 AGP 版本。两者能对齐就不动;对不齐,以 AGP 版本为准往上调 Gradle,比以 Gradle 为准往下换 AGP 更安全,因为新版 AB 对旧 AB 的兼容策略通常是“更高版本可以跑低版本工程”。

相应地,JDK 也要匹配。Android Studio Ladybug 之后内置 JBR 17,如果你把 AGP 拉到 8.0 以上,JDK 却还是 11,编译时会直接提示Unsupported class file major version。在 File → Project Structure → SDK Location 里确认 Gradle JDK 选的是“当前 JBR 17”,而不是随系统的 OpenJDK 8。

5.3 模拟器UI卡顿,别急着在布局上找原因

热搜词里有“ui界面卡顿”,在 Android 计算器这个项目里,卡顿十有八九出在模拟器,而不是代码。模拟器默认分辨率如果跟你的物理屏幕差距过大,渲染计算器这种全屏界面的每一帧都要做大量像素转换,Android Studio 窗口拖动时甚至出现明显的掉帧。

我的建议很直接:开发计算器这种小项目,直接接真机调试。用 USB 连上手机,开启开发者选项里的 USB 调试,Android Studio 工具栏会自动识别机型,点一下 Run 就安装运行。真机渲染性能是模拟器的几倍,字体渲染、圆角阴影这些 UI 细节看起来也更真实。

如果你是 Linux 或 Windows 环境,模拟器还必须开硬件加速,否则连启动都要等几分钟。遇到这种慢成幻灯片的模拟器,优先去 SDK Manager 里检查是否装了Intel HAXMAndroid Emulator hypervisor driver,装好后再把模拟器分辨率调到 1600x900 以下。绝大多数情况下,卡顿的根源是缺少 hypervisor,而不是计算器里那几个 Textview 的性能问题。

5.4 namespace缺失、R标红与Clean工程的正确顺序

老项目的build.gradle(Module 级别)里通常没有namespace字段,因为以前 Android 是从AndroidManifest.xmlpackage属性读取包名。从 AGP 8.0 开始,package属性被废弃,强制要求在 Module 的build.gradle里显式声明:

android { namespace 'com.example.calculator' compileSdk 34 defaultConfig { applicationId "com.example.calculator" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" } }

namespaceapplicationId在正式项目里可以是不同值:前者决定 R 类、BuildConfig 的包名路径,后者决定应用在应用商店的唯一 ID。对于这个简易计算器,两个保持一致就够。如果你改动过包名,之后出现 R 红色标红,别急着到处找错误,先执行一次 Build → Clean Project,再 Build → Rebuild Project。R 类是编译期生成的,包名变了它的物理路径也要变,增量编译经常缓存旧索引,Clean 一下才是“后悔药”。

同样常见的还有compileSdktargetSdk的设置冲突:如果你的项目compileSdk是 35,targetSdk还停在 28,Android 13 以上的机型会弹出分区存储、通知权限等运行时适配问题。简易项目想要少踩坑,compileSdk用最新稳定版,targetSdk保持 33 左右是稳妥组合,既不会触发 Android 14 的严格前台服务限制,又能覆盖绝大多数真机的兼容测试。

6. 加两个小功能,让压缩包里的计算器变成你的

6.1 退格与长按清零

给你一个能跑的 zip 包不是终点,改两行代码、加一个自己的功能,它才真正属于你。我建议从“退格”开始,这是计算器使用频率最高的功能,实现却只要十行不到:

private void backspace() { if (currentNumber.length() > 0) { // 删掉当前输入数字的最后一位 currentNumber.deleteCharAt(currentNumber.length() - 1); if (expression.length() > 0) { char last = expression.charAt(expression.length() - 1); if (last >= '0' && last <= '9') { expression.deleteCharAt(expression.length() - 1); } } updateResult(currentNumber.length() == 0 ? "0" : currentNumber.toString()); } }

注意这里我只能“删当前正在输入的数字”,不能删运算符,因为运算符一旦压栈,前面可能已经算了部分结果,退回再算会破坏双栈状态。如果你连运算符也想退,需要把numbers.pop()出来的值回填到currentNumber,把运算符从栈顶弹出,代码量会翻一倍,对一个“简易”项目没有必要。

长按清零更像是个情怀功能:计算器领域的老传统,长按某个按钮直接归零。给清零按钮设置setOnLongClickListener,里面调用clearAll(),再配合按钮 hover 时显示“按住了”的视觉反馈,整个交互立刻有了质感。

6.2 旋转屏幕不丢算到一半的算式

最后一个值得做的功能是旋转屏幕时保持状态。Android 默认旋转会重建 Activity,你算到一半的currentNumber、栈里的数字和运算符全部清空,用户心态直接崩掉。修复很简单,在 Manifest 的 Activity 声明里加上:

<activity android:name=".MainActivity" android:screenOrientation="portrait" android:configChanges="orientation|screenSize" />

强制竖屏是计算器应用最务实的方案,毕竟没有人在横屏下用计算器;如果你非要支持横屏适配,就得实现onSaveInstanceState把所有状态序列化,恢复时重建计算栈。

@Override protected void onSaveInstanceState(Bundle outState) { outState.putString("expression", expression.toString()); outState.putString("current", currentNumber.toString()); outState.putDouble("lastOperand", lastOperand); outState.putString("lastOperator", lastOperator); super.onSaveInstanceState(outState); }

重建 Activity 之前,系统会先把数据存进 Bundle,onCreate里再按 key 取回来,把currentNumber和栈的内容填回去。存储的粒度只到“表达式文本”和“当前数字”两个字符串,栈反而可以不存:拿到表达式后,从头解析一遍重新压栈,相当于把用户输入历史重放一遍。这样实现代码最少,也不用去序列化 Stack 这种 Java 集合对象。

最后说一个我个人的习惯:拿到任何带 zip 的计算器项目,我都会第一时间把它的布局、逻辑、配色拆成三块独立看,而不是直接用“Run”。先跑起来,再改一个小功能,最后重构一个小模块,这是从“用了别人的源码”过渡到“写自己的轮子”最稳的路。当初我第一次跑通这种项目时,也交过不少“semantic 版本配错”“未连续运算符处理”的学费,今天把它们写出来,希望帮到你。

本文还有配套的精品资源,点击获取

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

兔展制作官网避坑指南:3个高频报错与最佳实践

兔展制作官网避坑指南:3个高频报错与最佳实践 面试被问原理答不上来,这种尴尬谁懂?很多人觉得兔展是个拖拽式工具,技术含量低,真上手做官网才发现全是坑。更惨的是,当你把项目甩到面试官面前,被追问“你这个动态表单数据是怎么落库的”、“为什么首屏加载慢”,你支支吾吾答不出个所以然,瞬间露怯。…

作者头像 李华
网站建设 2026/9/23 10:41:02

搞懂阿拉伯数字的写法,性能优化才不踩坑

搞懂阿拉伯数字的写法,性能优化才不踩坑 别被标题骗了,这里说的“阿拉伯数字”不是让你回去学小学算术,而是指在代码里处理整数、浮点数以及数字字符串时的底层逻辑。很多开发者刚入行, int 和 float…

作者头像 李华
网站建设 2026/9/23 10:40:58

Atlas 300V 24G加速卡部署YOLO实战:从模型转换到性能调优

1. Atlas平台与Atlas 300V 24G加速卡的真实定位最近后台好几个朋友都在问同一个问题——"Atlas 300V 24G到底算不算运算加速卡"&#xff0c;还有人直接说"我想用Atlas跑YOLO&#xff0c;能不能行"。这个问题问得挺典型&#xff0c;也正好踩中了很多人刚接触…

作者头像 李华
网站建设 2026/9/23 10:40:59

男生和女生差差差很痛的软件免费下载性能优化

5步搞定版本升级API大坑,从入门到精通实战 版本升级后 API 全变了,这大概是后端开发最头疼的时刻。昨天还好好的,今天一更新依赖,报错红成一片,查半天发现方法名都改了。想从 入门到精通 ,光看教程不够,得懂底层逻辑。 入口定位 很多新人遇到 API…

作者头像 李华
网站建设 2026/9/23 10:40:34

3步搞定反义词英语,从入门到精通避坑指南

3步搞定反义词英语,从入门到精通避坑指南 看了一堆教程还是不会写项目?别急,问题不在你笨,而在你只看了“定义”,没跑过“代码”。 很多刚接触自然语言处理(NLP)或者做数据清洗的朋友,卡在 反义词英语…

作者头像 李华
网站建设 2026/9/23 10:40:33

运动主题避坑:3个面试必问的布局陷阱,90%的人踩过

运动主题避坑:3个面试必问的布局陷阱,90%的人踩过 刚毕业那会儿,我手里攥着Python和Java的证书,面试时自信满满。结果面试官问:“运动主题页面在移动端适配时,如何保证不同分辨率下动画流畅且数据加载不卡顿?”我愣在原地,脑子里全是语法细节,却答不上项目架构。这就是很多新手的通病:…

作者头像 李华