
简介这是一个基于 Android Studio 的简易计算器完整工程界面参考小米MIUI计算器风格适合刚接触安卓开发、希望用完整小项目巩固UI与逻辑能力的初学者。压缩包共957个文件大小约21.35MB其中包含292个界面布局与样式文件、276张界面图片素材、22个逻辑源码文件另有可直接安装的调试版安装包、Gradle构建脚本及构建中间文件便于查看每一步产物。已有2191人学习下载。项目完整演示了从布局设计、事件绑定到运算处理的流程通过ConstraintLayout让按钮和显示区域随屏幕自适应在MainActivity中为每个按钮设置点击监听并实现加减乘除以及连续表达式解析同时利用样式文件自定义按键颜色、圆角、文字大小接近MIUI计算器的视觉效果。除了核心代码还有完整的资源目录和调试配置读者可在开发环境中打开后边运行边学习也可以直接安装调试包体验效果并在此基础上继续添加开方、括号等扩展功能是一个兼具学习与改造价值的安卓入门实战项目。1. 用 AndroidStudio 写一个 MIUI 同款计算器从 Empty Activity 到能装进手机的 APK计算器是 Android 入门最经典的练手项目但大多数教程写完的界面停留在「能用」的层面——灰底白字、按钮挤在一起毫无美感。这个资源的不同之处在于它的 UI 刻意对齐了 MIUI 计算器的视觉风格你拿到的不是一个 Demo而是一套能直接改造成自己作品的布局思路和样式方案。项目基于 Java 实现工程结构采用 Android Studio 默认的 Empty Activity 模板核心代码集中在 activity_main.xml 和 MainActivity.java 两个文件里非常适合想搞清楚「Android 项目到底怎么从零长成一个 App」的初学者。有一点先说破计算器的难点从来不是加减乘除而是连续运算时的状态管理这一块后面会重点拆。2. 布局是第一道坎LinearLayout 权重网格与按钮基座设计2.1 为什么按钮矩阵不用 ConstraintLayout很多教程推荐用 ConstraintLayout 做一切但计算器的按钮区是一个严格的等宽网格用 LinearLayout 嵌套反而更稳。每个功能键占一行行内每个按钮用 layout_width0dp 加 layout_weight1 平分宽度这样无论屏幕多宽按钮永远等宽、间距一致不需要写任何约束条件。外层用一个垂直 LinearLayout顶部是显示区底部是按钮区结构清晰到不用看预览图也能想象出来。摘要里提到的 ConstraintLayout 在这个项目里不是主角它是用来固定显示区高度和整体边距的真正承担按钮矩阵的是 LinearLayout 加权重这套组合。权重布局的另一个好处是零适配成本手机在 360dp 宽和 420dp 宽的屏幕上按钮都会自动拉伸不会出现左边挤右边空的情况。2.2 activity_main.xml 完整结构下面这份布局文件可以直接套用它定义了 5 行按钮第一行是 AC、正负号、百分号、除号第二行是 7 8 9 乘号第三行是 4 5 6 减号第四行是 1 2 3 加号第五行是 0、小数点、等号。MIUI 计算器里 0 键是占两列宽的第五行我用了一个横向 LinearLayout 加两个纵向排列的按钮组来实现类似效果。?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:background#F2F3F5 TextView android:idid/tv_result android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:gravitybottom|end android:padding24dp android:text0 android:textColor#1A1A1A android:textSize56sp android:textStylebold / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:paddingLeft12dp android:paddingRight12dp android:paddingBottom12dp Button android:idid/btn_clear stylestyle/CalcButton.Operator android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:textAC / Button android:idid/btn_sign stylestyle/CalcButton.Operator android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text/- / Button android:idid/btn_percent stylestyle/CalcButton.Operator android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text% / Button android:idid/btn_divide stylestyle/CalcButton.Operator android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text÷ / /LinearLayout !-- 7 8 9 × 行 -- LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:paddingLeft12dp android:paddingRight12dp Button android:idid/btn_7 stylestyle/CalcButton.Number android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text7 / Button android:idid/btn_8 stylestyle/CalcButton.Number android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text8 / Button android:idid/btn_9 stylestyle/CalcButton.Number android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text9 / Button android:idid/btn_multiply stylestyle/CalcButton.Operator android:layout_width0dp android:layout_height64dp android:layout_weight1 android:layout_margin6dp android:text× / /LinearLayout !-- 4 5 6 - 行id 命名规则与上一行一致 -- !-- 1 2 3 行id 命名规则与上一行一致 -- /LinearLayout布局参数的几个关键点按钮高度统一用 64dp这是单手操作最舒服的触控高度按钮之间用 6dp 间距比系统默认的按钮间距更紧凑接近 MIUI 的密度显示区设置 layout_weight1 是为了吃掉剩余空间让按钮区永远贴底。布局文件里省略了 4、5、6、1、2、3 这几行实际工程里按同样的结构复制三行把 id 和 text 改掉就行不建议自己另起一套命名后面写 Java 绑定时按规律找 id 会快很多。2.3 样式引用的作用布局里每个按钮都用 stylestyle/CalcButton.Operator 或 stylestyle/CalcButton.Number这些样式定义在 values/styles.xml 中统一控制文字颜色、背景、圆角和字号。这样做的直接好处是改一个按钮的视觉效果不用碰布局文件比如想把数字键背景从浅灰换成白色只改 styles.xml 里一处即可。按钮背景用的是 drawable 目录下的 selector 文件实现按下变深的反馈效果这个在第四章会单独展开。3. 计算逻辑别用 if 堆firstOperand pendingOp 状态机与防除零3.1 事件绑定的选择按钮点击事件在 Android 里有两种常见绑法在布局 XML 里写 android:onClick或者在 Java 代码里用 setOnClickListener。我建议这个项目全部用 Java 绑定原因有两个一是最后要加长按删除功能onLongClick 只能通过代码注册二是把 findViewById 和 listener 放在一起结构上更接近真实项目的写法。为了避免十几个按钮每个都写一遍匿名内部类我在代码里用 switch 按 view.getId() 分发代码量直接砍半。3.2 状态机核心逻辑计算器的难点在于处理「1 2 3」这种连续按运算符的场景。我第一次写的时候用「等号才计算」的思路结果按完 再按 显示区直接乱掉。正确做法是把计算过程拆成两个阶段按运算符时如果当前没有等待中的运算符就把显示区的数字存为 firstOperand再把刚按的运算符存为 pendingOp如果已经有 pendingOp说明用户是想连续计算先算完上一轮把结果显示出来再更新运算符。按等号时执行 pendingOp 对应的运算结果写入 firstOperand这样用户可以继续按运算符接着算行为跟 MIUI 计算器一致。public class MainActivity extends AppCompatActivity { private TextView tvResult; private String firstOperand null; private String pendingOperator null; private String currentInput ; private boolean resultShown false; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvResult findViewById(R.id.tv_result); bindButtons(); } private void bindButtons() { 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, R.id.btn_dot }; for (int id : numberIds) { findViewById(id).setOnClickListener(v - onNumberClick((Button) v)); } int[] operatorIds { R.id.btn_add, R.id.btn_subtract, R.id.btn_multiply, R.id.btn_divide, R.id.btn_percent, R.id.btn_sign }; for (int id : operatorIds) { findViewById(id).setOnClickListener(v - onOperatorClick((Button) v)); } findViewById(R.id.btn_equal).setOnClickListener(v - onEqualClick()); findViewById(R.id.btn_clear).setOnClickListener(v - onClearClick()); } private void onNumberClick(Button btn) { String text btn.getText().toString(); if (resultShown) { currentInput ; resultShown false; } if (..equals(text)) { if (currentInput.contains(.)) return; if (currentInput.isEmpty()) currentInput 0; } currentInput text; tvResult.setText(currentInput); } private void onOperatorClick(Button btn) { String op btn.getText().toString(); if (/-.equals(op)) { handleSign(); return; } if (currentInput.isEmpty() firstOperand null) return; if (firstOperand ! null pendingOperator ! null !currentInput.isEmpty()) { calculate(); } firstOperand tvResult.getText().toString(); pendingOperator op; currentInput ; resultShown false; } private void onEqualClick() { if (firstOperand null || pendingOperator null) return; calculate(); pendingOperator null; firstOperand null; resultShown true; } private void calculate() { double num1 Double.parseDouble(firstOperand); double num2 currentInput.isEmpty() ? Double.parseDouble(firstOperand) : Double.parseDouble(currentInput); double result 0; switch (pendingOperator) { case : result num1 num2; break; case -: result num1 - num2; break; case ×: result num1 * num2; break; case ÷: if (num2 0) { tvResult.setText(错误); resetState(); return; } result num1 / num2; break; } if (Double.isInfinite(result) || Double.isNaN(result)) { tvResult.setText(错误); resetState(); return; } String display formatResult(result); tvResult.setText(display); firstOperand display; currentInput ; } private String formatResult(double value) { if (value Math.floor(value) !Double.isInfinite(value)) { return String.valueOf((long) value); } return String.valueOf(value); } private void resetState() { firstOperand null; pendingOperator null; currentInput ; resultShown false; } }这段代码里有几个容易忽略的细节。第一个是currentInput.isEmpty()时按运算符比如刚打开 App 直接按加号这时候直接 return 不处理避免出现以运算符开头的非法表达式。第二个是连续按等号的场景按完等号后 firstOperand 存的是结果pendingOperator 会被清空但用户可以直接再按运算符继续计算这是firstOperand还在的原因。第三个是除零保护除以整数 0 时 Java 会抛 ArithmeticException但 Double 类型除以 0.0 不会抛异常而是得到 Infinity所以必须显式判断num2 0和Double.isInfinite(result)否则用户看到的是满屏的 Infinity 而不是友好的错误提示。3.3 百分号和正负号的处理这两个键属于单目运算不需要参与 pendingOp 状态机。正负号的实现是如果当前输入以 - 开头就去掉否则在前面加 -。百分号则是把当前显示值除以 100。注意百分号在 MIUI 计算器里的行为是「除以 100」而不是「取百分数」别搞混。这两个操作都只修改 currentInput 和 tvResult不影响 firstOperand 和 pendingOperator所以放在 onOperatorClick 里提前返回是合理的不会打乱状态机的节奏。4. values/styles 与 drawable selector不写一行 Java 的按钮换肤4.1 为什么用 selector 而不是 backgroundTintAndroid 5.0 之后可以用 android:backgroundTint 给按钮着色但 backgroundTint 只会改变背景色不会处理按下的视觉反馈。MIUI 计算器的按钮有非常明显的按压效果手指按下去时按钮颜色变深松开恢复。这个交互在原生 Button 上是没有的必须用 drawable selector 定义 state_pressed 状态下的不同颜色。color 的色值就不多说了。关键是把按钮拆成三类数字键用浅灰背景、深色文字运算符键用深灰背景、白色文字等号键用橙色背景、白色文字。这样在代码里只需要三个 selector每个按钮通过 style 引用对应的 selector 作为 background视觉逻辑就能完全收敛到资源文件里。4.2 styles.xml 与 colors.xml 落地代码!-- values/colors.xml -- resources color namecolor_bg#F2F3F5/color color namecolor_number_bg#FFFFFF/color color namecolor_number_bg_pressed#E0E0E0/color color namecolor_operator_bg#E6E6E6/color color namecolor_operator_bg_pressed#C7C7C7/color color namecolor_equal_bg#FF9500/color color namecolor_equal_bg_pressed#E08600/color color namecolor_number_text#1A1A1A/color color namecolor_operator_text#1A1A1A/color color namecolor_equal_text#FFFFFF/color color namecolor_accent#FF9500/color /resourcescolors.xml 的作用是把所有颜色值集中管理。我习惯把按压态颜色写在本色旁边这样调对比度时一眼能对上。color_equal_bg_pressed比color_equal_bg深 15% 左右这个亮度差在视觉上刚好能感知又不会显得突兀。!-- values/styles.xml -- resources style nameCalcButton.Number item nameandroid:layout_width0dp/item item nameandroid:layout_height64dp/item item nameandroid:layout_weight1/item item nameandroid:layout_margin6dp/item item nameandroid:backgrounddrawable/bg_btn_number/item item nameandroid:textColorcolor/color_number_text/item item nameandroid:textSize26sp/item item nameandroid:textStylebold/item item nameandroid:stateListAnimatornull/item /style style nameCalcButton.Operator item nameandroid:layout_width0dp/item item nameandroid:layout_height64dp/item item nameandroid:layout_weight1/item item nameandroid:layout_margin6dp/item item nameandroid:backgrounddrawable/bg_btn_operator/item item nameandroid:textColorcolor/color_operator_text/item item nameandroid:textSize26sp/item item nameandroid:textStylebold/item item nameandroid:stateListAnimatornull/item /style style nameCalcButton.Equal item nameandroid:layout_width0dp/item item nameandroid:layout_height64dp/item item nameandroid:layout_weight1/item item nameandroid:layout_margin6dp/item item nameandroid:backgrounddrawable/bg_btn_equal/item item nameandroid:textColorcolor/color_equal_text/item item nameandroid:textSize26sp/item item nameandroid:textStylebold/item item nameandroid:stateListAnimatornull/item /style /resourcesstyles.xml 里有一个必须设置的项android:stateListAnimator要置为 null。原生 Material 按钮默认带水波纹和高度抬升动画如果你设置了自定义 background这个默认动画会导致按钮按下时出现光影变化和背景色变化叠加的「两层反馈」看起来非常怪异。置 null 之后按钮的按下反馈完全交给 selector 控制。dp 与 sp 的边界也在这里讲清楚按钮高度、间距、圆角半径统一用 dp它们不随系统字体缩放只有文字大小用 sp这样用户调大系统字体时计算器数字还能正常放大。很多新手把按钮高度写成 sp结果用户一改字体按钮直接撑破布局这是血泪经验。4.3 drawable selector 与圆角形状!-- drawable/bg_btn_number.xml -- selector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_pressedtrue shape solid android:colorcolor/color_number_bg_pressed / corners android:radius16dp / /shape /item item shape solid android:colorcolor/color_number_bg / corners android:radius16dp / /shape /item /selectorbg_btn_operator.xml 和 bg_btn_equal.xml 结构相同只改 solid 的颜色引用。圆角统一 16dp这是 MIUI 计算器视觉语言的核心参数——比 iOS 的 20dp 略小比 Material 的 4dp 圆润得多。如果你在真机上觉得圆角不对多半是屏幕密度导致视觉偏差16dp 在 2.75x 屏幕上的实际像素是 44px看起来刚好。这里要注意 shape 里的corners必须放在solid之后虽然 XML 对顺序没有硬性要求但有些老旧真机的 Drawable 解析器对顺序敏感按 solid 再 corners 的顺序写最稳。selector 的 item 顺序也不能乱state_pressed 的 item 必须放在前面否则后面的 item 永远匹配不上按压反馈会失效——这是个非常经典的翻车点。5. 打包与调试验证APK 产物、Logcat 与五个常见翻车点5.1 构建产物与增量编译缓存在 Android Studio 里点击 Build Build APK(s)构建完成后在项目的 app/build/outputs/apk/debug/ 目录下能找到 app-debug.apk。这个 APK 可以直接 adb install 到手机上。你会在项目的根目录和 app/build 目录里看到很多增量编译缓存文件比如 fileHashes.bin、taskArtifacts.bin、localJarClasspathSnapshot.bin 之类它们对应 Gradle 的构建缓存机制删掉会导致下一次构建变慢正常开发不用理会。如果你用命令行构建Windows 下执行 gradlew.bat assembleDebugMac/Linux 执行 ./gradlew assembleDebug产物路径不变。5.2 真机调试与 Logcat 的正确姿势System.out.println 在 Android 里是打印不到的必须用 Log.d。真机调试时 Logcat 默认显示的是「无过滤条件」状态你会看到系统的大量日志先按包名过滤logcat --pid$(pidof -s 你的应用包名)或者直接在 Logcat 面板的包名输入框里填包名。新手最容易在这上面卡一个小时以为代码没执行其实是日志被系统信息淹没了。adb install -r app-debug.apk adb logcat --pid$(pidof -s com.example.calculator) -d | grep CalcLog加 -r 参数可以覆盖安装保留数据适合调试时反复安装。grep 关键字CalcLog对应代码里的 Log.d 标签这样只输出自己关心的日志。如果 adb install 报 INSTALL_FAILED_UPDATE_INCOMPATIBLE说明手机上已有签名不同的同名应用先卸载再装报 INSTALL_FAILED_TEST_ONLY 则在命令里加 -t 参数允许安装测试包。5.3 避坑清单按「现象 — 原因 — 解决」排1. 现象 连续按运算符比如「2 3 」显示直接变成 5 但在继续按数字时被重置原因onOperatorClick 里计算完没有把 resultShown 置为 false导致下一次数字点击走了「清空当前输入」的分支。解决在 onOperatorClick 的 calculate() 调用后手动resultShown false或者在 onNumberClick 里判断「按下数字时如果结果是运算符刚计算完的状态不清空而是接续输入」。MIUI 计算器的行为是「2 3 4」直接得出 9但如果你按了「2 3 」再按数字「4」它会清空显示开始全新输入。这个语义差异要在状态机里明确区分。2. 现象 按钮按下去颜色不变没有任何反馈原因drawable selector 的 item 顺序写反了state_pressed 放在最后永远匹配不到或者 background 引用的是颜色资源而不是 drawable 资源。解决把 selector 文件里带 state_pressed 的 item 放到最前面检查布局里 android:backgroundcolor/xxx 是否写成了颜色引用颜色不能触发 state 切换必须用 drawable 开头的 selector。3. 现象 真机上按钮显示成方块圆角全没了原因高版本 Android 的 Button 默认有 Material 样式background 被主题覆盖。解决在 styles.xml 里给五个按钮样式都加上item nameandroid:backgrounddrawable/bg_btn_number/item同时给 Activity 的主题设置为不带 Material 的 Theme.Material.Light.NoActionBar 或者 Theme.AppCompat.Light.NoActionBar。有时候模拟器显示正常但真机变方就是因为 emulator 用的主题和真机不同这个坑很玄学直接改主题能解决。4. 现象 除零后显示「错误」但按任何数字键都无法恢复原因calculate() 里重置状态后currentInput 为空但 resultShown 还是 false导致后续输入没有走「清空旧值」的分支。解决在 resetState() 里把 resultShown 也置为 false同时在 onNumberClick 里加判断if (错误.equals(tvResult.getText().toString())) { currentInput ; }。任何错误态都必须保证下一次输入能完全重置这是状态机设计的底线。5. 现象 布局预览显示正常但 APK 装到手机后底部按钮被导航栏挡住原因Android 的全面屏手势导航栏是半透明的布局没有适配系统导航栏。解决给根布局加上android:fitsSystemWindowstrue或者在 styles.xml 的主题里设置item nameandroid:windowLightNavigationBartrue/item配合android:windowNavigationBarColor#F2F3F5让导航栏和 App 背景融为一体。计算器这种底部按钮密集的应用这个适配不做设备一换代就直接翻车。6. 现象 每次改代码重新构建特别慢要等几十秒原因Gradle 在 debug 构建时会做增量编译但如果代码里用了反射或者动态类加载增量编译机制会自动失效。计算器项目本身没有这个问题慢大概率是第一次构建没走完或者你动过 build.gradle 导致配置阶段重跑。解决保持 Android Studio 打开别关进程连续改 Java 代码用 Build Apply Changes改资源文件用 Build Rebuild ProjectApply Changes 只支持代码热替换混用会导致反复全量编译。5.4 布局实时预览的正确打开方式Layout Editor 的 Design 预览模式在布局基础上能直接看到不同尺寸设备的渲染效果。我习惯在预览面板右上角的设备选择器里切换 Pixel 2411dp 宽和 Pixel Tablet1280dp 宽两个极端分别看按钮间距和权重是否正常。这里要记住预览只是渲染模型不代表真机表现特别是字体渲染和阴影效果模拟器上和真机可能有肉眼可见的差异。凡是涉及颜色和圆角的调整最终都要以真机截图为准。6. 把计算器做成能用的样子长按删除、竖屏锁定与 release 签名验证6.1 长按删除当前输入的最后一位MIUI 计算器支持在输入区长按删除这在简易计算器里通常是缺失功能。实现思路不用动状态机只需要在 TextView 的 onLongClick 里对 currentInput 做字符串裁剪tvResult.setOnLongClickListener(v - { if (currentInput.isEmpty()) { return true; } currentInput currentInput.substring(0, currentInput.length() - 1); if (currentInput.isEmpty()) { currentInput 0; } tvResult.setText(currentInput); return true; });注意这个逻辑不应影响 firstOperand 和 pendingOperator长按删除只作用于正在编辑的当前输入。如果此时已经显示结果currentInput 会被清空再接数字等同于新输入。长按的判定由系统提供不需要额外处理。6.2 竖屏锁定与清单配置计算器在横屏下需要重新排列按钮简易版没必要做。AndroidManifest.xml 里给 MainActivity 加android:screenOrientationportrait这样无论用户怎么转手机界面都保持竖屏。这个配置的副作用是锁死了 Activity 的旋转重建意味着你不会遇到 onSaveInstanceState 相关的问题但也意味着你失去了练习状态保存的机会。如果你想把状态保存练一遍可以把 orientation 设置去掉并实现 onSaveInstanceState 保存 firstOperand、pendingOperator、currentInput 三个变量。activity android:name.MainActivity android:exportedtrue android:screenOrientationportrait intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity6.3 release 签名验证与安装检查调试 APK 用的是 debug 签名安装到手机没有问题但如果要把 APK 发给别人必须要签名成 release 包。在 build.gradle 的 android 块里配置 signingConfigs 指定签名文件路径、别名、两个密码然后 buildTypes.release 里引用它。签名完成后用 apksigner 验证apksigner verify --verbose app-release.apk输出里有Verified using v1 scheme: true和Verified using v2 scheme: true就表示签名成功。验证结束后把这个 APK 装到手机实测一遍主界面显示正常、旋转屏幕不重启、连续运算状态不错乱、除零显示「错误」且能恢复、长按删除逐字符生效。我自己的习惯是每次改完布局或状态机都强制走一遍这五个场景全部通过才认为这次改动没有引入回归问题因为计算器这种项目越是看着简单越容易在改小功能时把状态机改坏。希望这套拆解能帮到你照着这份笔记把代码落进 Android Studio你会拿到一个真正有 MIUI 质感的计算器而不仅仅是一个能跑通的 Demo。本文还有配套的精品资源点击获取