
最近整理自己写过的工具类项目翻到一份在 Android Studio 里完成的多进制计算器源码纯 Java 语言实现结构完整导入 IDE 就能直接编译运行。最早做它是因为调试网络协议时经常要在二进制、十六进制之间反复换算手机自带的计算器没有顺手的多进制入口于是自己动手写了一个。后来持续完善成一套可以在日常使用的计算器四个输入框分别对应二进制、八进制、十进制、十六进制改动任意一个框其他三个实时刷新。这份源码覆盖的知识点相当集中包含 Android 界面搭建、事件监听、输入过滤、进制转换核心算法正好适合刚学完 Android 基础、想拿一个小项目练手的开发者也适合需要频繁进制换算的前后端工程师拿去做日常工具。1. 项目整体设计与功能定位1.1 拆解需求清单与典型使用场景先把这个项目到底要做什么拆清楚。多进制计算器本质上不是一个真正常规意义上的“计算”工具而是一个进制转换工具。它承担的最核心工作是把用户在一个进制下输入的数实时映射成其他进制下的表示。我最初的需求清单列出来大概是这样的支持二进制、八进制、十进制、十六进制四种常用进制。用户只需要点击任意进制的输入框输入对应字符其他三种进制框自动显示换算结果。非法输入要被拦截比如在二进制输入框里敲“2”或“3”直接不生效。界面要简洁一个屏幕放得下单手操作顺手。不需要任何额外权限离线可用。权限这一点在后来帮了不少忙。因为很多工具类应用动不动就要存储权限装的时候让人心里犯嘀咕。而这份计算器从安装到使用不触碰任何敏感权限用户接受度天然高。典型使用场景我举三个实在例子。第一个是学习场景。学计算机组成原理的时候经常要做十进制与二进制的手工换算做完题想验证答案。用这个工具往十进制框里输入一个数二进制结果立刻出现效率比对着课本手算高得多。第二个是工作场景。分析 TCP 报文头、IP 地址、内存地址时十六进制和二进制换算非常频繁。比如看到一个十六进制地址 0x3FF想快速知道它二进制下是多少位直接输入就能得到答案。第三个是效率场景。平时用系统计算器切到“程序员”模式找按钮、切进制流程很繁琐。这种局部效率的提升长期积累下来省下的时间其实相当可观。1.2 技术选型为什么这份源码坚持用 Java有人会问现在 Android 新项目基本都是 Kotlin 了这份源码为什么还用 Java回答这个问题之前可以先看看这个项目的定位它不是一个大型商业 App而是一个源码结构清晰、便于学习和快速运行的实用工具。原因很实际。第一这个工具的核心逻辑也就是进制转换在 Java 标准库里已经有非常成熟的封装。Integer 类提供了 parseInt(String, radix)、toBinaryString、toOctalString、toHexString 等方法几乎不需要自己写复杂算法代码量小出 Bug 概率低。第二Java 语法对刚从 C 语言过来的学生更友好变量声明、方法定义、循环分支一目了然。对于一个以“学习 实用”为目标的项目用 Java 能让人把注意力集中在界面搭建和事件处理上而不是先补 Kotlin 语法的课。第三这份源码是从 Android Studio 早期版本用 Java 开发的后续要翻译成 Kotlin 只是语法层面的迁移逻辑完全一致。先把 Java 版本吃透理解 Activity 生命周期、视图绑定与回调机制之后无论是转 Kotlin 还是写其他跨平台方案概念迁移都很平滑。2. 多进制转换核心算法与 Java API 解析2.1 进制换算的数学原理回顾在写代码之前先把换算原理彻底想清楚。我们日常生活默认使用十进制隐藏的规则是“逢十进一”。二进制就是“逢二进一”八进制是“逢八进一”十六进制是“逢十六进一”。任何一个数在任意进制下都可以拆成按“位权”展开的形式。比如二进制的 1101从右往左每一位的位权分别是 2 的 0 次方、2 的 1 次方、2 的 2 次方、2 的 3 次方所以它表示的十进制数是1×2³ 1×2² 0×2¹ 1×2⁰ 8 4 0 1 13反过来十进制转二进制用“除二取余法”把十进制数不断除以 2记录每一步的余数最后把余数逆序排列。例如 13 这个数13 ÷ 2 6 余 1 6 ÷ 2 3 余 0 3 ÷ 2 1 余 1 1 ÷ 2 0 余 1把余数逆序排列得到 1101。这批数学原理虽然写代码时不需要手动实现一轮轮除法但理解它非常必要。因为程序里所有进制转换本质上都先回归到一个“中间进制”也就是先转成十进制整数再从十进制转成目标进制中间步骤用的就是位权计算。没有理解这一点后面面对负数、超大数时很容易被内置 API 的输出搞懵。2.2 Java 内置进制转换 API 的正确打开方式Java 在 java.lang.Integer 里把常用的进制转换封装得很好。先说最常见的三个方法Integer.toBinaryString(int)返回 int 参数的二进制字符串。Integer.toOctalString(int)返回八进制字符串。Integer.toHexString(int)返回十六进制字符串。Integer.parseInt(String s, int radix)按指定进制解析字符串返回 int。实际项目中我封装了一个转换工具类来统一管理这些 APIpublic class ConvertUtil { public static String toBinary(int num) { return Integer.toBinaryString(num); } public static String toOctal(int num) { return Integer.toOctalString(num); } public static String toDecimal(int num) { return String.valueOf(num); } public static String toHex(int num) { return Integer.toHexString(num).toUpperCase(); } public static int parse(String text, int radix) { return Integer.parseInt(text, radix); } }这四个方法组合起来就能覆盖“任意进制转任意进制”的常见需求。转换链路统一走“某进制字符串 - 十进制 int - 目标进制字符串”逻辑非常清晰。注意到 toHex 方法里调用了 toUpperCase()十六进制里的 a 到 f 统一显示成大写看起来更规整。这个细节在阅读内存地址时能减少误读比如“3ff”不如“3FF”直观。2.3 负数与边界情况处理进制转换的代码写起来容易但负数往往会让人栽跟头。原因在于 Java 里的 int 是带符号的补码表示。直接调 Integer.toHexString(-1)得到的结果不是 -1而是 ffffffff这是补码在内存中的真实形态。这一点和“数学上十六进制的 -1 应该怎么写”并不完全一致。所以我在项目里做了一个取舍界面上不直接支持输入负数作为被转换数。如果用户输入负号输入过滤器直接拦掉。这样虽然损失了一部分能力但换来的是界面逻辑的清晰用户看到的所有转换结果都是正整数及其对应进制的表现形式。如果未来确实要做负数转换建议在工具类里增加一个特殊处理分支把负数转换成对应的无符号表示。例如 int 类型的 -1 对应的无符号十六进制是 ffffffff无符号二进制是 32 个 1。功能可以后加但初期没必要给自己增复杂度。另外还有一个容易踩的边界情况是输入超长字符串。比如在二进制框里敲入一个 33 位的字符串Integer.parseInt 按 radix2 解析时会因为超出 int 范围而抛出 NumberFormatException。所以代码里对每个输入框的解析都做了 try-catch 包裹凡是解析失败都静默忽略不让用户看到崩溃窗口。3. 界面布局与交互设计实战3.1 布局结构设计与控件排布这份计算器的界面目标很明确一屏展示、单手可点、信息密度不低但不过度拥挤。我用的布局方案是垂直方向的 LinearLayout 作根容器内部按照“一行一个进制”的思路排布。具体结构是这样顶部是一个标题 TextView显示当前状态或功能提示。中间是四行每行由一个 TextView 标签和一个 EditText 输入框组成。底部是一排功能按钮包括清空和全部归零。每行的 TextView 固定宽度标识当前输入框的进制类型比如“二进制”“八进制”“十进制”“十六进制”。EditText 撑满剩余空间用 singleLine 属性限制单行输入。这里有一个经验二进制的 EditText 建议把输入类型设置为 android:inputTypetext而不是 number。默认的 number 键盘不含字母十六进制输入框又需要 a-f。为了让软键盘兼容所有情况四个输入框统一使用 text 输入类型再配合代码里的 InputFilter 做字符白名单校验。这样做还有个额外好处就是不同 ROM 上的数字键盘适配问题能降到最低。3.2 InputFilter 字符过滤与异常输入拦截InputFilter 是 Android 自带的文本过滤机制它能在字符真正写入前拦截不想要的字符。原理是让过滤器检查待插入的新串如果包含非法字符就返回空串相当于直接丢弃这轮输入。以二进制输入框为例需要实现一个只允许 0 和 1 的过滤器。我封装了一个通用的进制输入过滤器public class RadixInputFilter implements InputFilter { private String allowedChars; public RadixInputFilter(int radix) { StringBuilder builder new StringBuilder(); if (radix 2) { builder.append(01); } else if (radix 8) { for (char c 0; c 7; c) { builder.append(c); } } else if (radix 10) { for (char c 0; c 9; c) { builder.append(c); } } else if (radix 16) { for (char c 0; c 9; c) { builder.append(c); } for (char c A; c F; c) { builder.append(c); } } allowedChars builder.toString(); } Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { for (int i start; i end; i) { char c source.charAt(i); if (allowedChars.indexOf(c) 0) { return ; } } return null; } }这套过滤有个额外的便利用户在十六进制框里输入小写字母 a-f 也能被放行因为字符集里同时包含了对应的大写字母而 Android 软键盘默认输出的通常是低位十六进制的小写字母。为了避免后续展示不统一TextWatcher 里再统一转成大写就可以了。3.3 事件监听与实时刷新逻辑界面交互的本质是“事件驱动”。用户每敲一个字符系统就会触发 TextWatcher 的 afterTextChanged 回调。在这个回调里我读取当前输入框的文本、解析成整数、再写回另外三个输入框形成实时刷新。四个输入框都挂同一个逻辑模板的 TextWatcher只是解析的 radix 参数不同。项目中最需要注意的是递归更新问题当我在 afterTextChanged 里主动调用 otherEt.setText() 时会触发 otherEt 的 TextWatcher而它又反过来把解析结果写回当前框。如果不做防护两个输入框就会互相 setText形成死循环。解决办法是更新前设置一个全局 boolean 标志 isUpdatingprivate boolean isUpdating false; Override public void afterTextChanged(Editable s) { if (isUpdating) return; String input s.toString().trim(); if (input.isEmpty()) { clearAll(); return; } try { int value ConvertUtil.parse(input, radix); isUpdating true; updateOtherFields(value); } catch (NumberFormatException ignored) { // 输入不完整或非法不做处理 } finally { isUpdating false; } }这套代码的核心就是 isUpdating 标志位。它就像一个闸门程序自己写入的值不会再触发新一轮更新只有真正来自用户的输入才会进入转换流程。我刚开始开发时因为忘了加这个标志连续调节了快两个小时也没绕过互相刷新的问题后来才总结出这个经验凡是 TextWatcher 里 setText必防递归。4. 关键源码实现与模块剖析4.1 工程目录结构与模块职责一份 Android Studio 工程的标准结构我挑重点来讲MultiRadixCalculator/ ├── app/ │ ├── build.gradle │ └── src/main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/example/multiradix/ │ │ ├── MainActivity.java │ │ └── ConvertUtil.java │ └── res/ │ ├── layout/activity_main.xml │ ├── values/strings.xml │ └── values/colors.xml包名的 com.example 是 Android Studio 新建项目的默认前缀如果要发布到应用市场最好改成自己惯用的域名反写。如果只是本地编译运行不改完全没问题。ConvertUtil 放在纯 Java 包路径下MainActivity 里通过 static 方式直接调用逻辑层和界面层清晰分离。4.2 转换工具类不掺 UI 的纯逻辑层ConvertUtil 类坚持不依赖任何 Android 框架类只使用 Java 标准库。这么设计的好处有两个。第一如果以后要写单元测试可以直接在本机 JVM 环境运行不需要连接模拟器或真机。第二如果后面想把它提取成独立的通用库给其他项目复用也几乎没有迁移成本。这个类的代码在 2.2 节已经给出实际使用时的入口是 parse 和 toXxx 系列方法。值得强调的是parse 方法要放在 try-catch 外层调用NumberFormatException 一旦出现不捕获就直接导致 App 闪退。这里的异常也可能来自用户输入了无法解析的空字符串所以 catch 块里一定要留一个静默路径。4.3 MainActivity 中完整的事件链路MainActivity 做的事情很集中按顺序拆成三步。第一步初始化视图。在 setContentView(R.layout.activity_main) 之后用 findViewById 拿到四个输入框和清空按钮的引用。字符串资源里把标签文字统一写在 strings.xml方便后续做多语言适配。第二步为每个输入框设置 RadixInputFilter 和 TextWatcher。以二进制输入框为例new RadixInputFilter(2)十六进制输入框new RadixInputFilter(16)。这里不要遗漏把过滤器对 EditText 的文本变化生效editText.setFilters(new InputFilter[]{ filter })。第三步实现 updateOtherFields 方法。把当前解析到的十进制 int 值分别转成其他三种进制的字符串写入对应输入框。写入前后都要正确管理 isUpdating 标志复位操作必须在 finally 块里执行否则后续异常会导致标志位卡死。清空按钮的逻辑也很简单直接调用四个输入框的 setText()同时把 isUpdating 控制好要不然清空动作会依次触发四个 TextWatcher白白执行好几轮无效转换在低端手机上能明显感觉到卡顿。4.4 EditText 光标跳动与焦点管理的隐藏技巧在 TextWatcher 里主动 setText会带来一个很隐蔽的副作用EditText 的焦点和光标位置会被重置光标自动跳到文本末尾。对二进制、十六进制这种字符数量有限的输入来说影响不大但当输入内容比较长时用户光标被强制拉到末尾体验就变差了。我采用的策略是在 setText 之后立即调用 editText.setSelection(newLength)把光标放到文本末尾。虽然还没有做到“保留原光标位置”但这个工具的使用习惯决定了文本末尾恰好是用户下一次要输入的位置逻辑上合理。如果你以后要把它扩展成更复杂的编辑器建议在 setText 前把光标位置记录下来setText 后恢复原位。5. 实战踩坑实录与常见问题排查5.1 常见问题速查表把开发这份源码时遇到的高频问题整理成一张速查表以后如果自己忘了查这张表就能快速定位。问题现象根本原因解决方案两个输入框互相刷新卡死或死循环TextWatcher 递归触发加 isUpdating 标志位二进制框输入 2 竟然能上屏没有做输入过滤使用 RadixInputFilter十六进制显示成小写不专业没有统一大小写toUpperCase() 统一转大写输入超长二进制串App 直接退出超出 int 范围抛异常解析处 try-catch键盘弹出后界面被顶到变形没有配置软键盘模式manifest 里调整 adjustPan下载后提示解析失败注册了多余权限或签名问题清理权限并检查构建配置5.2 大整数溢出一个容易被忽略的隐性 Bugint 类型占 4 个字节最大值是 2147483647。按二进制表示它能容纳 31 位有效数值最高位是符号位。如果用户想转换一个巨大的十六进制数比如 0xFFFFFFFF这个数在 int 里会被解析成 -1而不是数学上的 4294967295。这意味着 parse 的结果和用户预期不一致后续所有转换结果全是错的。当时我一度考虑用 long 替代 int。long 能覆盖到 19 位十进制数容量确实更大但二进制的符号位问题依然存在。真正的根治方案是换成 BigInteger它可以表示任意大的整数。但 BigInteger 在频繁输入场景下会创建大量临时对象而且它在 Android 上的 toBinaryString、toHexString 方法也都有对应实现代码改动不难。我最后在产品里选择了一个折中方案int 范围内直接转换超出 int 范围就弹出提示告诉用户“数值过大仅支持 32 位 int 范围”。这个选择配合一次 Toast可以把错误结果挡在用户面前避免显示一个错误答案却让用户误以为正确。5.3 输入法弹窗遮挡与界面适配在真机测试时遇到过一个问题点击靠近屏幕底部的输入框后软键盘弹出来直接挡住输入位置。原因是 AndroidManifest.xml 里没有配置软键盘模式。后来我加了 windowSoftInputModeactivity android:name.MainActivity android:windowSoftInputModeadjustPan /activityadjustPan 的意思是软键盘弹出时系统把窗口整体向上平移让当前获得焦点的输入框尽量可见。后来又测试过 adjustResize 配合 ScrollView 的方式但因为这个工具就一屏内容adjustPan 已经足够。另一个跟键盘有关的坑来自物理键盘。部分模拟器默认开启了 PC 键盘某些蓝牙键盘在真机上也会触发 Tab 键切换焦点的行为导致输入框自动切换转换逻辑出现偶发空白。处理方式是在 AndroidManifest 里声明 stateHidden限制软键盘的自动弹出同时在每个 TextWatcher 里对空字符串做返回处理把空白状态统一视为“未输入”。5.4 模拟器与真机上的输入法差异模拟器上一切正常装到真机上却出现数字键盘不弹出的情况这类问题我也见过。原因在于部分定制 ROM 的默认输入法对 inputTypetext 的处理方式不同有的只会显示纯字符键盘。既然我们在布局阶段已经统一使用 text 输入类型这个问题的概率其实已经很低。如果仍然收到用户反馈说不方便可以改用 InputType.TYPE_CLASS_TEXT 加上 TYPE_TEXT_VARIATION_VISIBLE_PASSWORD 的组合。这个组合强制显示可见字符键盘同时也能兼顾数字输入属于比较通用的保险做法。6. 以这份源码为起点还能学什么、玩什么6.1 从源码中能学到的三个核心技能第一组件化思想。ConvertUtil 类和界面层完全解耦UI 只负责展示和收集输入逻辑层只负责数据转换。这种分层方式比把几百行代码堆在同一个 Activity 里要健康得多维护起来也轻松。以后写任何带表单的工具这套思路都能复用。第二事件驱动模型的实战理解。这套代码里有 TextWatcher、OnClickListener整个运行逻辑形成完整的“监听—响应—更新”回路。写任何需要实时更新的界面这组模式都逃不掉。把 isUpdating 标志位理解透以后处理回调嵌套、异步刷新都会更从容。第三输入校验与异常防御。虽然 RadixInputFilter 和 try-catch 看起来简单但它们撑起了整个工具的安全性。不管用户怎么乱输入App 都不会闪退这本身就是一种工程质量。很多商业 App 动不动就崩溃根本原因就是把这些问题看轻了。6.2 三个值得动手扩展的方向方向一给计算器加上四则运算。现在它只是纯转换功能。加入加减乘除意味着需要一套表达式解析器项目复杂度会明显上升。我的建议是先从“只在十进制输入框内输入表达式然后让其他进制显示结果”开始避免同时处理四个输入框的表达式的复杂度。方向二支持小数与任意进制转换。当前只覆盖整数。真正通用的转换器还需要处理小数点后的位权计算并且可以做成 2 到 36 进制任意切换而不是只支持四种固定进制。36 进制的由来是 0-9 加 A-Z刚好 36 个符号。方向三加入历史记录与深色主题。记录最近一定数量的转换结果本地保存。再给 UI 加一套暗色主题资源。这个扩展不需要改任何算法只需要在数据层加一个存储结构再在资源目录里补一套主题颜色非常适合练手。我个人后来选择了方向三给工具加进了深色模式。深夜调试设备时打开深色界面整个过程舒服了不少也顺便把 Android 的主题资源切换机制练熟了。这个设计决策的时间成本很低性价比很高。说一个这套源码给我最大的启发多进制计算器看起来是个不起眼的小东西但它把一个完整 App 开发中最常见的环节都覆盖了。从需求拆解、界面设计、算法选型到异常处理每一步都不是瞎拍脑袋而是有清晰的决策依据。如果你正在学 Android建议拿到这份源码后先不看 MainActivity 的完整实现而是自己先写一遍转换逻辑再对照源码看差异收获会比单纯读代码多不少。我自己在写完这个小工具之后再去做带表单、带输入校验的正式业务模块明显觉得心里更有底了。