1. 多进制计算器项目概述
这个基于Android Studio开发的多进制计算器应用,是我在探索Android开发过程中完成的一个实用工具类项目。它不仅能处理常规的十进制运算,还支持二进制、八进制和十六进制的计算与转换,特别适合程序员、电子工程师等需要频繁进行多进制运算的用户群体。
作为一名长期从事移动开发的工程师,我深知一个优秀的计算器应用应该具备哪些特性。市面上大多数计算器要么功能过于简单,要么界面复杂难用。这个项目正是为了解决这些痛点而设计的,它提供了:
- 完整的四则运算功能(加、减、乘、除)
- 括号优先级运算支持
- 四种进制(二进制、八进制、十进制、十六进制)的实时切换
- 内存存储功能(MC、MR、MS、M+、M-)
- 智能的输入处理和错误检测机制
2. 项目架构与技术选型
2.1 开发环境配置
这个项目使用Android Studio作为开发环境,基于Java语言实现。选择Java而非Kotlin主要出于以下考虑:
- Java在Android开发中更为成熟稳定
- 计算器这类工具类应用对语言特性要求不高
- 便于更多开发者理解和维护代码
项目的最低API级别设置为21(Android 5.0),确保覆盖绝大多数设备。Gradle配置中只引入了基础的appcompat库,没有使用其他第三方依赖,保证了应用的轻量性。
2.2 核心类设计
整个应用采用单Activity架构,所有逻辑集中在MainActivity.java中。这种设计对于计算器这类简单应用非常合适:
java复制public class MainActivity extends AppCompatActivity {
// UI组件引用
private EditText etResult;
private TextView tvExpression;
private TextView tvCurrentBase;
// 核心数据
private StringBuilder expression = new StringBuilder();
private int currentBase = 10; // 默认十进制
private double memoryValue = 0;
// 数字按钮数组
private Button[] numberButtons;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
initNumberButtons();
setupAllButtons();
updateButtonsForCurrentBase();
}
// ...其他方法
}
2.3 界面布局设计
界面采用LinearLayout和TableLayout组合实现,确保在不同尺寸设备上都能良好显示。activity_main.xml中定义了:
- 顶部显示当前进制的TextView
- 进制选择按钮区域
- 表达式和结果显示区域
- 计算器键盘区域(采用TableLayout实现网格布局)
特别值得一提的是,我们为不同进制的按钮设置了不同的背景色,帮助用户快速识别当前模式:
xml复制<Button
android:id="@+id/btnHex"
android:backgroundTint="#4CAF50" <!-- 十六进制绿色 -->
.../>
<Button
android:id="@+id/btnDec"
android:backgroundTint="#2196F3" <!-- 十进制蓝色 -->
.../>
<Button
android:id="@+id/btnOct"
android:backgroundTint="#FF9800" <!-- 八进制橙色 -->
.../>
<Button
android:id="@+id/btnBin"
android:backgroundTint="#F44336" <!-- 二进制红色 -->
.../>
3. 核心功能实现细节
3.1 多进制输入处理
计算器需要根据当前进制动态调整可用的数字按钮。例如在二进制模式下,只允许输入0和1;八进制模式下允许0-7等。这是通过updateButtonsForCurrentBase()方法实现的:
java复制private void updateButtonsForCurrentBase() {
// 重置所有数字按钮
for (Button btn : numberButtons) {
if (btn != null) {
btn.setEnabled(true);
btn.setAlpha(1.0f);
}
}
// 处理十六进制按钮
int[] hexButtonIds = {R.id.btnA, R.id.btnB, R.id.btnC, R.id.btnD, R.id.btnE, R.id.btnF};
boolean isHex = currentBase == 16;
for (int id : hexButtonIds) {
Button btn = findViewById(id);
if (btn != null) {
btn.setVisibility(isHex ? View.VISIBLE : View.GONE);
}
}
// 处理小数点按钮(仅在十进制可用)
Button decimalBtn = findViewById(R.id.btnDecimal);
boolean decimalEnabled = currentBase == 10;
decimalBtn.setEnabled(decimalEnabled);
decimalBtn.setAlpha(decimalEnabled ? 1.0f : 0.5f);
// 根据进制禁用数字
switch (currentBase) {
case 8: // 八进制:禁用8,9
findViewById(R.id.btn8).setEnabled(false);
findViewById(R.id.btn8).setAlpha(0.5f);
findViewById(R.id.btn9).setEnabled(false);
findViewById(R.id.btn9).setAlpha(0.5f);
break;
case 2: // 二进制:禁用2-9
for (int i = 2; i <= 9; i++) {
String btnId = "btn" + i;
int resId = getResources().getIdentifier(btnId, "id", getPackageName());
Button btn = findViewById(resId);
if (btn != null) {
btn.setEnabled(false);
btn.setAlpha(0.5f);
}
}
break;
}
}
3.2 表达式求值引擎
计算器的核心是表达式求值功能。我采用了经典的"Shunting Yard"算法将中缀表达式转换为后缀表达式,再通过栈结构计算结果。这种方法的优势在于:
- 正确处理运算符优先级
- 支持括号改变运算顺序
- 算法效率高,时间复杂度为O(n)
java复制private double evaluateExpression(String expr) throws Exception {
if (expr == null || expr.trim().isEmpty()) return 0;
// 检查括号匹配
if (!isParenthesesBalanced(expr)) {
throw new Exception("括号不匹配");
}
// 转换为后缀表达式
String postfix = infixToPostfix(expr);
// 计算后缀表达式
return evaluatePostfix(postfix);
}
// 中缀转后缀(Shunting Yard算法)
private String infixToPostfix(String infix) throws Exception {
StringBuilder output = new StringBuilder();
Stack<Character> operators = new Stack<>();
// 预处理表达式
String[] tokens = processInfixExpression(infix);
for (String token : tokens) {
if (token.isEmpty()) continue;
if (isNumber(token)) {
output.append(token).append(" ");
} else if (token.equals("(")) {
operators.push('(');
} else if (token.equals(")")) {
while (!operators.isEmpty() && operators.peek() != '(') {
output.append(operators.pop()).append(" ");
}
if (!operators.isEmpty()) operators.pop();
} else if (token.length() == 1 && isOperator(token.charAt(0))) {
char op = token.charAt(0);
while (!operators.isEmpty() &&
operators.peek() != '(' &&
getPrecedence(operators.peek()) >= getPrecedence(op)) {
output.append(operators.pop()).append(" ");
}
operators.push(op);
}
}
while (!operators.isEmpty()) {
output.append(operators.pop()).append(" ");
}
return output.toString().trim();
}
// 计算后缀表达式
private double evaluatePostfix(String postfix) throws Exception {
Stack<Double> stack = new Stack<>();
String[] tokens = postfix.split("\\s+");
for (String token : tokens) {
if (token.isEmpty()) continue;
if (isNumber(token)) {
stack.push(parseNumber(token));
} else if (token.length() == 1 && isOperator(token.charAt(0))) {
if (stack.size() < 2) {
throw new Exception("操作数不足");
}
double b = stack.pop();
double a = stack.pop();
stack.push(applyOperator(token.charAt(0), a, b));
}
}
if (stack.size() != 1) {
throw new Exception("表达式错误");
}
return stack.pop();
}
3.3 进制转换实现
进制转换功能是此计算器的特色之一。当用户切换进制时,当前显示的数字会自动转换为新进制的表示形式:
java复制private void setBase(int base) {
try {
String exprStr = expression.toString().trim();
if (!exprStr.isEmpty()) {
double result = evaluateExpression(exprStr);
currentBase = base;
expression.setLength(0);
expression.append(convertBase(result));
tvExpression.setText("");
etResult.setText(expression.toString());
} else {
String currentText = etResult.getText().toString();
if (!currentText.equals("0") && !currentText.equals("错误")) {
double value = parseNumber(currentText);
currentBase = base;
etResult.setText(convertBase(value));
} else {
currentBase = base;
etResult.setText("0");
}
}
// 更新UI显示当前进制
switch (base) {
case 16: tvCurrentBase.setText("十六进制"); break;
case 10: tvCurrentBase.setText("十进制"); break;
case 8: tvCurrentBase.setText("八进制"); break;
case 2: tvCurrentBase.setText("二进制"); break;
}
updateButtonsForCurrentBase();
} catch (Exception e) {
etResult.setText("错误");
}
}
// 实际进制转换方法
private String convertBase(double value) {
long longValue = (long) value;
boolean isNegative = value < 0;
longValue = Math.abs(longValue);
String result;
switch (currentBase) {
case 16:
result = Long.toHexString(longValue).toUpperCase();
break;
case 8:
result = Long.toOctalString(longValue);
break;
case 2:
result = Long.toBinaryString(longValue);
break;
default:
result = String.valueOf(longValue);
}
return isNegative ? "-" + result : result;
}
4. 用户体验优化技巧
4.1 智能输入处理
为了让计算器使用起来更加自然,我实现了一系列智能输入处理逻辑:
- 自动区分负号和减号:根据上下文判断"-"是作为负号还是减号
- 小数点处理:防止重复输入小数点,只在十进制模式下启用
- 括号自动补全:确保括号匹配
- 隐式乘法:数字后直接跟左括号时自动添加乘号
java复制private void handleInput(String input) {
// 清除错误状态
if (etResult.getText().toString().equals("错误")) {
expression.setLength(0);
tvExpression.setText("");
etResult.setText("0");
}
String exprStr = expression.toString();
// 智能处理负号/减号
if (input.equals("-")) {
if (exprStr.isEmpty() || exprStr.endsWith("(") ||
exprStr.endsWith("+") || exprStr.endsWith("-") ||
exprStr.endsWith("×") || exprStr.endsWith("/") ||
exprStr.endsWith(" ")) {
expression.append("-"); // 负号
} else {
expression.append(" - "); // 减号
}
}
// 处理小数点
else if (input.equals(".")) {
if (currentBase == 10) {
String[] parts = exprStr.split("[+\\-×/\\s()]");
if (parts.length > 0 && !parts[parts.length-1].contains(".")) {
expression.append(".");
}
}
}
// 处理左括号(自动添加乘号)
else if (input.equals("(")) {
if (!exprStr.isEmpty() && !exprStr.endsWith("(") &&
!exprStr.endsWith("+") && !exprStr.endsWith("-") &&
!exprStr.endsWith("×") && !exprStr.endsWith("/") &&
!exprStr.endsWith(" ")) {
expression.append(" × (");
} else {
expression.append("(");
}
}
// 处理右括号(确保匹配)
else if (input.equals(")")) {
int openCount = countChar(exprStr, '(');
int closeCount = countChar(exprStr, ')');
if (openCount > closeCount) {
expression.append(")");
}
}
// 普通输入
else {
expression.append(input);
}
updateDisplay();
}
4.2 实时计算与显示
为了提升用户体验,计算器实现了实时计算功能——在用户输入过程中就显示当前表达式的结果:
java复制private void updateDisplay() {
String exprStr = expression.toString();
tvExpression.setText(exprStr);
try {
if (!exprStr.trim().isEmpty()) {
double result = evaluateExpression(exprStr);
etResult.setText(formatNumber(result));
} else {
etResult.setText("0");
}
} catch (Exception e) {
// 表达式不完整时不显示错误
}
}
4.3 内存功能实现
计算器提供了完整的内存功能,包括存储(M)、读取(R)、清除(C)、加(M+)、减(M-)等操作:
java复制private void setupMemoryButtons() {
findViewById(R.id.btnMC).setOnClickListener(v -> memoryValue = 0);
findViewById(R.id.btnMR).setOnClickListener(v -> {
try {
String value = convertBase(memoryValue);
handleInput(value);
} catch (Exception e) {
etResult.setText("错误");
}
});
findViewById(R.id.btnMS).setOnClickListener(v -> {
try {
double result = evaluateExpression(expression.toString());
memoryValue = result;
etResult.setText(formatNumber(result));
} catch (Exception e) {
etResult.setText("错误");
}
});
findViewById(R.id.btnMPlus).setOnClickListener(v -> {
try {
double result = evaluateExpression(expression.toString());
memoryValue += result;
etResult.setText(formatNumber(result));
} catch (Exception e) {
etResult.setText("错误");
}
});
findViewById(R.id.btnMMinus).setOnClickListener(v -> {
try {
double result = evaluateExpression(expression.toString());
memoryValue -= result;
etResult.setText(formatNumber(result));
} catch (Exception e) {
etResult.setText("错误");
}
});
}
5. 开发中的关键问题与解决方案
5.1 表达式解析的边界情况
在开发表达式求值功能时,遇到了多种边界情况需要处理:
- 空表达式处理
- 括号不匹配情况
- 操作数不足情况
- 除以零错误
- 无效数字格式
解决方案是为每种错误情况定义明确的异常处理逻辑:
java复制private double evaluateExpression(String expr) throws Exception {
if (expr == null || expr.trim().isEmpty()) {
return 0;
}
// 检查括号匹配
if (!isParenthesesBalanced(expr)) {
throw new Exception("括号不匹配");
}
// 转换为后缀表达式
String postfix = infixToPostfix(expr);
// 计算后缀表达式
return evaluatePostfix(postfix);
}
// 在调用处捕获所有异常并显示错误
private void calculateResult() {
try {
String exprStr = expression.toString().trim();
if (!exprStr.isEmpty()) {
double result = evaluateExpression(exprStr);
expression.setLength(0);
expression.append(formatNumber(result));
tvExpression.setText("");
etResult.setText(expression.toString());
}
} catch (Exception e) {
etResult.setText("错误");
expression.setLength(0);
tvExpression.setText("");
}
}
5.2 多进制数字解析
不同进制下的数字解析是一个复杂问题,特别是处理以下情况:
- 十六进制包含A-F字符
- 二进制只允许0和1
- 八进制只允许0-7
- 十进制支持小数
- 负数的处理
解决方案是编写专门的数字验证和解析方法:
java复制private boolean isNumber(String str) {
if (str == null || str.isEmpty()) return false;
boolean hasSign = str.startsWith("-");
String numStr = hasSign ? str.substring(1) : str;
if (numStr.isEmpty()) return false;
switch (currentBase) {
case 16:
return numStr.matches("[0-9A-Fa-f]+(\\.\\d+)?");
case 10:
return numStr.matches("\\d+(\\.\\d+)?");
case 8:
return numStr.matches("[0-7]+");
case 2:
return numStr.matches("[01]+");
default:
return numStr.matches("\\d+(\\.\\d+)?");
}
}
private double parseNumber(String str) throws Exception {
if (str == null || str.isEmpty()) return 0;
boolean isNegative = str.startsWith("-");
String numStr = isNegative ? str.substring(1) : str;
if (numStr.isEmpty()) return 0;
try {
double value;
switch (currentBase) {
case 16:
if (numStr.contains(".")) {
String[] parts = numStr.split("\\.");
long intPart = Long.parseLong(parts[0], 16);
if (parts.length > 1 && !parts[1].isEmpty()) {
double fracPart = Double.parseDouble("0." + parts[1]);
value = intPart + fracPart;
} else {
value = intPart;
}
} else {
value = Long.parseLong(numStr, 16);
}
break;
case 10:
value = Double.parseDouble(str);
break;
case 8:
value = Long.parseLong(numStr, 8);
break;
case 2:
value = Long.parseLong(numStr, 2);
break;
default:
value = Double.parseDouble(str);
}
return isNegative ? -value : value;
} catch (NumberFormatException e) {
throw new Exception("无效的数字: " + str);
}
}
5.3 按钮状态管理
在不同进制下,数字按钮的可用状态需要动态调整。这涉及到:
- 禁用不合适的数字按钮
- 显示/隐藏十六进制按钮(A-F)
- 启用/禁用小数点按钮
- 视觉反馈(透明度变化)
解决方案是维护一个按钮数组,在进制切换时统一更新状态:
java复制private void initNumberButtons() {
numberButtons = new Button[]{
findViewById(R.id.btn0), findViewById(R.id.btn1), findViewById(R.id.btn2),
findViewById(R.id.btn3), findViewById(R.id.btn4), findViewById(R.id.btn5),
findViewById(R.id.btn6), findViewById(R.id.btn7), findViewById(R.id.btn8),
findViewById(R.id.btn9), findViewById(R.id.btnA), findViewById(R.id.btnB),
findViewById(R.id.btnC), findViewById(R.id.btnD), findViewById(R.id.btnE),
findViewById(R.id.btnF)
};
}
private void updateButtonsForCurrentBase() {
// 重置所有数字按钮
for (Button btn : numberButtons) {
if (btn != null) {
btn.setEnabled(true);
btn.setAlpha(1.0f);
}
}
// 处理十六进制按钮
int[] hexButtonIds = {R.id.btnA, R.id.btnB, R.id.btnC,
R.id.btnD, R.id.btnE, R.id.btnF};
boolean isHex = currentBase == 16;
for (int id : hexButtonIds) {
Button btn = findViewById(id);
if (btn != null) {
btn.setVisibility(isHex ? View.VISIBLE : View.GONE);
}
}
// 处理小数点按钮
Button decimalBtn = findViewById(R.id.btnDecimal);
boolean decimalEnabled = currentBase == 10;
decimalBtn.setEnabled(decimalEnabled);
decimalBtn.setAlpha(decimalEnabled ? 1.0f : 0.5f);
// 根据进制禁用数字
switch (currentBase) {
case 8: // 八进制:禁用8,9
findViewById(R.id.btn8).setEnabled(false);
findViewById(R.id.btn8).setAlpha(0.5f);
findViewById(R.id.btn9).setEnabled(false);
findViewById(R.id.btn9).setAlpha(0.5f);
break;
case 2: // 二进制:禁用2-9
for (int i = 2; i <= 9; i++) {
String btnId = "btn" + i;
int resId = getResources().getIdentifier(btnId, "id", getPackageName());
Button btn = findViewById(resId);
if (btn != null) {
btn.setEnabled(false);
btn.setAlpha(0.5f);
}
}
break;
}
}
6. 项目扩展与优化方向
6.1 功能扩展建议
当前计算器已经实现了核心功能,但还可以进一步扩展:
- 添加位运算功能(AND、OR、XOR、NOT等)
- 支持科学计算功能(三角函数、对数、指数等)
- 增加历史记录功能
- 支持更多进制(如三十二进制、六十四进制等)
- 添加主题切换功能
6.2 性能优化建议
虽然计算器应用对性能要求不高,但仍有一些优化空间:
- 使用StringBuilder代替字符串拼接
- 优化表达式解析算法
- 减少不必要的对象创建
- 使用更高效的数据结构
6.3 代码重构建议
为了使代码更易于维护和扩展,可以考虑:
- 将表达式求值引擎抽离为独立类
- 使用MVP或MVVM架构分离UI和业务逻辑
- 增加单元测试
- 完善注释和文档
7. 项目部署与使用说明
7.1 构建与运行
- 使用Android Studio打开项目
- 确保已安装合适的Android SDK版本
- 连接设备或启动模拟器
- 点击运行按钮构建并安装应用
7.2 使用指南
- 默认处于十进制模式
- 点击左上角按钮切换进制
- 输入表达式后点击"="计算结果
- 使用M系列按钮操作内存
- 点击C按钮清除当前输入
7.3 已知限制
- 非十进制模式下不支持小数运算
- 十六进制模式下字母必须大写
- 表达式长度有限制
- 大数运算可能溢出
在实际开发过程中,我发现Android的计算器应用虽然看似简单,但要实现一个功能完善、用户体验良好的产品,需要考虑的细节非常多。这个项目让我深入理解了表达式求值算法、多进制转换、Android UI状态管理等关键技术点。特别是Shunting Yard算法的实现,让我对编译原理中的词法分析和语法分析有了更直观的认识。