Android 计算器开发实战:从界面设计到核心逻辑

简介: 本文手把手教你从零构建Android计算器,涵盖环境搭建、MVC分层架构、界面布局、状态管理、事件监听、输入校验、错误提示及独立计算类封装。强调“界面与逻辑分离”,助你写出高内聚、低耦合、易维护的代码。

摘要:本文手把手带你从零构建一个基于 Android 的计算器应用。内容涵盖项目环境搭建与分层架构规划、界面布局与组件设计、核心状态变量管理、按钮事件监听、屏幕显示更新、输入合法性校验、用户提示机制,以及独立计算类的封装与联调测试。通过完整流程,你将掌握 Android 开发中“界面与逻辑分离”的核心思想,并学会如何写出高内聚、低耦合、易维护的 Android 应用代码。

关键词:Android;移动开发;计算器;MVC 架构;事件驱动;输入校验;代码封装

很多初学者在接触 Android 界面编程时,往往卡在“逻辑”与“界面”分离的难点上。写一个命令行计算器很简单,但一旦要求有窗口、有按钮、能实时显示结果,代码结构就容易变得混乱。特别是当需要处理用户误操作、异常输入或者复杂的运算逻辑时,如果没有良好的架构规划,后期维护简直是一场灾难。

其实,构建一个健壮的 Android 计算器应用,核心不在于算法有多高深,而在于如何优雅地管理状态、响应事件以及隔离业务逻辑。通过这个项目,我们不仅能掌握 GUI 编程的基础套路,更能体会到“高内聚、低耦合”在实际工程中的价值。无论你是想完成课程作业,还是希望提升 Android 开发的实战能力,这套从环境搭建到功能扩展的完整流程都能为你提供清晰的参考路径。

接下来,我们将一步步拆解这个过程。从最初的项目骨架搭建,到界面组件的精细化设计,再到核心计算引擎的封装与异常处理机制的引入,每一个环节都对应着实际开发中常见的痛点。我们会重点讨论如何让代码更易读、更健壮,并分享一些在联调测试中踩过的坑及解决方案,帮助你避开那些看似微小却足以导致程序崩溃的细节陷阱。

① 项目环境搭建与基础架构规划

在动手写第一行代码之前,先把开发环境准备好。推荐使用 Android Studio(当前稳定版为 Hedgehog 或更高版本),它内置了 Gradle 构建系统、模拟器和布局预览工具,能极大降低新手入门门槛。安装完成后,依次完成以下三步:创建新项目时选择 Empty Views Activity 模板(而非 Compose 模板,便于我们手动控制布局);SDK 版本建议选择 minSdk 24、targetSdk 34,兼顾兼容性与新特性;语言选择 Java,与本文示例保持一致。

项目创建后,默认的 Gradle 目录结构已经帮我们分好了层:app/src/main/java 存放 Java 源码,app/src/main/res 存放布局、字符串和图片资源,app/src/main/AndroidManifest.xml 声明应用组件与权限。对于计算器这种小型项目,我们只需在 java 目录下按包名组织类即可,例如 com.example.calculator 下分别放置 activity、view、engine 三个子包,让代码从一开始就井井有条。

基础架构上,建议采用分层设计思想:视图层(View)负责界面渲染和用户交互,控制层(Controller)处理事件转发,模型层(Model)则专注于数据计算和状态存储。这种 MVC 模式的变体能有效避免将所有逻辑堆砌在同一个类中。例如,可以创建一个 CalculatorActivity 作为入口 Activity,初始化界面;再分别定义 CalculatorLayout 处理布局,CalculatorEngine 负责数学运算。这样的规划能让代码在功能增加时依然保持清晰,不会因为添加一个新功能而牵一发而动全身。

为了让你更直观地理解分层关系,下面是本项目的基础架构图:

mermaid diagram

② 计算器界面布局设计与组件定义

界面是用户直接感知的部分,布局的合理性直接影响体验。在 Android 中,Activity 是主容器,内部通常嵌套 View 或 ViewGroup(如 LinearLayout、GridLayout)来划分区域。我们可以将界面分为上下两部分:上方是显示屏区域,用于展示当前输入和计算结果;下方是键盘区域,排列数字键和运算符。

为了让结构更清晰,建议用 LinearLayout 作为根容器并设置 orientation="vertical",把屏幕纵向分成两块:顶部放一个 TextView 作为显示屏,底部放一个 GridLayout 作为键盘区。这样既符合真实计算器的视觉习惯,也让后续的事件绑定和状态更新更有条理。显示屏建议使用 TextView 而非 EditText,因为计算器只需要展示结果、不需要用户直接编辑文本,TextView 更轻量且不会弹出软键盘干扰操作。

为了保持按钮整齐划一,使用 GridLayout 或 GridLayoutManager 是最直观的选择。例如,键盘区可以设置为 4 行 5 列的网格,依次放置数字 0-9、加减乘除、清空、退格等按钮。显示屏则建议使用 TextView 或 EditText,并设置为右对齐,模拟真实计算器的视觉效果。需要注意的是,组件命名要有语义化,如 btnAdd、txtDisplay,避免使用 button1 这种无意义的默认名,这在后期绑定事件时会大大减少混淆。

在动手写 Java 代码之前,更推荐先用 XML 声明布局。这样既能直观预览效果,也方便后续调整间距和样式。下面是一份可直接套用的 activity_main.xml 骨架,把显示屏和键盘区都定义好:

<!-- res/layout/activity_main.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <!-- 显示屏区域 -->
    <TextView
        android:id="@+id/txtDisplay"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="end"
        android:textSize="40sp"
        android:text="0"
        android:padding="12dp"
        android:background="#EEEEEE" />

    <!-- 键盘区域 -->
    <GridLayout
        android:id="@+id/keyPanel"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:columnCount="5"
        android:rowCount="4" />
</LinearLayout>

这里有几个关键点值得注意:显示屏用 gravity="end" 实现右对齐,textSize="40sp" 保证数字清晰可读;键盘区用 layout_weight="1" 让它占满剩余高度,columnCount="5" 对应 4 行 5 列的按键排布。这样声明好之后,Java 代码里只需 findViewById 拿到 txtDisplay 和 keyPanel 两个引用,再动态填充按钮即可,职责划分非常清晰。

// 示例:初始化键盘区域布局
GridLayout keyPanel = findViewById(R.id.keyPanel);
keyPanel.setColumnCount(5);
keyPanel.setRowCount(4);
String[] keys = {
   "7", "8", "9", "/", "C", 
                 "4", "5", "6", "*", "←",
                 "1", "2", "3", "-", "=",
                 "0", ".", "+", "%", "±"};

for (String key : keys) {
   
    Button btn = new Button(this);
    btn.setText(key);
    btn.setTextSize(18);
    keyPanel.addView(btn);
}

为了让按钮在网格中均匀分布,建议给每个按钮设置统一的 LayoutParams,例如 layout_width="0dp"、layout_weight="1",并加上合适的边距,避免按键挤在一起。此外,←(退格)、C(清空)、=(等号)这类功能键,后续在事件监听阶段需要单独区分处理,建议在创建按钮时就用 setTag 打上标记,例如 btn.setTag("func"),方便 onClick 里快速判断按键类型。

③ 核心变量声明与数据类型管理

计算器的核心在于状态管理。我们需要定义几个关键变量来追踪当前的运算状态:currentInput 存储用户正在输入的数字字符串,previousValue 保存上一次运算的结果,operator 记录当前选定的运算符,以及一个布尔标志 isNewEntry 来判断是否开始新的数字输入。在 Android 中,这些状态通常保存在 Activity 或 ViewModel 中,并通过 savedInstanceState 在配置变更时保留。

下面是一份可直接套用的变量声明示例,放在 CalculatorActivity 中作为成员变量:

// 核心状态变量
private String currentInput = "";   // 当前正在输入的数字字符串
private double previousValue = 0;   // 上一次运算的结果
private String operator = "";       // 当前选定的运算符,如 "+"、"-"
private boolean isNewEntry = true;  // 是否开始新的数字输入
private boolean isError = false;    // 是否处于错误状态(如除以零)

这里有几个设计要点值得注意:currentInput 用字符串而非 double 保存,是为了在用户输入过程中保留原始文本(比如 0. 这种中间态),避免因类型转换丢失精度或产生多余的 0;isNewEntry 用于区分「接着上一个数字继续输入」和「开始输入一个新数字」,这是判断何时触发中间计算的关键;isError 则配合后续的 TipChecker 校验机制,在发生除以零等致命错误时锁定输入,直到用户按下清空键。

数据类型的选择也很讲究。虽然界面显示的是字符串,但在内部计算时,应优先使用 double 或 BigDecimal。double 适合大多数常规场景,性能较好;但如果涉及高精度财务计算,BigDecimal 能避免浮点数精度丢失的问题。此外,还需要一个枚举类型或常量类来定义支持的运算符号,防止魔法字符串散落在代码各处,提高可维护性。

对于运算符号,建议用常量类统一管理,避免在事件监听和计算类中反复写裸字符串:

// 运算符号常量类
public final class Operator {
   
    public static final String ADD = "+";
    public static final String SUB = "-";
    public static final String MUL = "*";
    public static final String DIV = "/";
    public static final String MOD = "%";
    public static final String NEG = "±";
}

这样在 onClick 里判断运算符时,只需 operator.equals(Operator.ADD) 即可,既避免了魔法字符串,也让后续扩展新运算(如平方根、幂运算)时只需在常量类中增加一个字段,改动集中、不易出错。

为了帮你更直观地做选择,下面把 double 与 BigDecimal 在计算器场景下的表现放在一起对比:

对比维度 double BigDecimal
精度 二进制浮点,存在精度丢失(如 0.1 + 0.2 不等于 0.3) 十进制精确表示,可指定精度与舍入规则,无精度丢失
性能 原生 CPU 运算,速度快,适合高频计算 基于对象与字符串运算,开销较大,性能明显偏低
内存占用 8 字节,占用小 对象结构,占用更大
适用场景 普通四则运算、科学计算、对精度不敏感的日常计算器 财务金额、税率、汇率等对精度有严格要求的场景
代码复杂度 直接使用 + - * /,写法简洁 需调用 add、subtract、multiply、divide 等方法,并处理舍入模式
异常处理 除零产生 Infinity 或 NaN,需自行判断 divide 除不尽时需指定舍入模式,否则抛 ArithmeticException

推荐建议:对于本文的计算器项目,日常加减乘除使用 double 即可满足需求,代码更简洁、性能更好;只需在显示结果时做一次格式化(如保留若干位小数)来掩盖精度误差。若你后续要扩展为记账、汇率换算等涉及金额的功能,再在核心计算类 Calc 中改用 BigDecimal,并统一指定 MathContext 与舍入模式,保证结果精确可控。

最后,别忘了在 onSaveInstanceState 中保存这些状态变量,并在 onCreate 中恢复,这样当用户旋转屏幕或应用被系统回收时,计算过程不会丢失:

@Override
protected void onSaveInstanceState(Bundle outState) {
   
    super.onSaveInstanceState(outState);
    outState.putString("currentInput", currentInput);
    outState.putDouble("previousValue", previousValue);
    outState.putString("operator", operator);
    outState.putBoolean("isNewEntry", isNewEntry);
    outState.putBoolean("isError", isError);
}

恢复时只需在 onCreate 中判断 savedInstanceState != null,再依次取回即可。这一小步看似不起眼,却能显著提升应用在配置变更场景下的健壮性,也是 Android 开发中容易被新手忽略的细节。

④ 按钮事件监听与 onClick 实现

Android 应用的灵魂在于事件驱动。所有按钮都需要注册 OnClickListener,当用户点击时触发 onClick 方法。在这个方法中,首先要获取触发事件的源视图(即哪个按钮被 clicked),然后提取其标签文本(如"+"、"5")。

在上一节我们已经用 setTag 给功能键打上了标记,这里正好派上用场。下面是一份完整的按钮绑定与事件分发示例,放在 CalculatorActivity 的 onCreate 中:

// 为键盘区每个按钮统一绑定点击事件
GridLayout keyPanel = findViewById(R.id.keyPanel);
for (int i = 0; i < keyPanel.getChildCount(); i++) {
   
    View child = keyPanel.getChildAt(i);
    if (child instanceof Button) {
   
        child.setOnClickListener(this); // Activity 实现 OnClickListener 接口
    }
}

// 在 onClick 中统一分发处理
@Override
public void onClick(View v) {
   
    Button btn = (Button) v;
    String key = btn.getText().toString();
    String tag = (String) btn.getTag(); // 可能是 "num"、"op"、"func" 或 null

    if ("func".equals(tag)) {
   
        handleFunctionKey(key);   // 处理 C、←、= 等功能键
    } else if ("op".equals(tag)) {
   
        handleOperatorKey(key);   // 处理 +、-、*、/、%、± 等运算符
    } else {
   
        handleNumberKey(key);     // 处理数字和小数点
    }
}

这里的关键在于「统一入口 + 按类型分发」:所有按钮都走同一个 onClick,再根据 tag 或文本快速路由到对应的处理方法。这样主事件方法保持简洁,后续新增按键时只需在对应处理方法里加一个分支,而不用改动事件绑定代码。

逻辑分支要清晰:如果是数字键,则追加到当前输入串;如果是运算符,则触发一次中间计算并将状态标记为等待下一个数字;如果是功能键(如清空),则重置所有状态变量。为了避免大量的 if-else 嵌套,可以使用 switch 语句或者命令模式,将不同类型的按钮行为封装成独立的方法调用,使主事件处理方法保持简洁。

下面分别实现这三个处理方法,注意它们如何与第三节声明的状态变量配合:

// 数字键:追加到当前输入串
private void handleNumberKey(String key) {
   
    if (isError) return;                    // 错误状态下锁定输入
    if (isNewEntry) {
   
        currentInput = key;                 // 开始新数字
        isNewEntry = false;
    } else {
   
        currentInput += key;                // 继续追加
    }
    updateDisplay();
}

// 运算符键:触发中间计算并记录运算符
private void handleOperatorKey(String key) {
   
    if (isError) return;
    if (!currentInput.isEmpty()) {
   
        if (!operator.isEmpty()) {
   
            // 已有运算符,先完成上一次中间计算
            previousValue = Calc.execute(previousValue,
                    Double.parseDouble(currentInput), operator);
        } else {
   
            previousValue = Double.parseDouble(currentInput);
        }
        currentInput = "";
    }
    operator = key;
    isNewEntry = true;
    updateDisplay();
}

// 功能键:清空、退格、等号
private void handleFunctionKey(String key) {
   
    switch (key) {
   
        case "C":   // 清空所有状态
            currentInput = "";
            previousValue = 0;
            operator = "";
            isNewEntry = true;
            isError = false;
            break;
        case "←":   // 退格:去掉最后一个字符
            if (!currentInput.isEmpty()) {
   
                currentInput = currentInput.substring(0, currentInput.length() - 1);
            }
            break;
        case "=":   // 等号:完成最终计算
            if (!operator.isEmpty() && !currentInput.isEmpty()) {
   
                previousValue = Calc.execute(previousValue,
                        Double.parseDouble(currentInput), operator);
                currentInput = String.valueOf(previousValue);
                operator = "";
                isNewEntry = true;
            }
            break;
    }
    updateDisplay();
}

这段代码把「事件分发」和「业务处理」彻底分离:onClick 只负责路由,真正的逻辑落在三个私有方法里。其中 Calc.execute 正是第八节要封装的独立计算类,这里先调用它,体现了「界面只做转发、计算交给模型层」的 MVC 思想。等号键处理完后把结果写回 currentInput 并清空 operator,这样用户可以直接基于结果继续运算,体验更接近真实计算器。

⑤ 屏幕显示更新与 print 函数逻辑

屏幕显示的更新必须及时且准确。每当状态变量发生变化,都需要调用一个统一的更新方法(不妨命名为 updateDisplay)来刷新界面组件。这个方法负责将内部的数值或字符串转换为适合显示的格式,例如去除多余的前导零,或者在处理超长数字时自动切换为科学计数法。

在第四节我们已经看到,数字键、运算符键、功能键的处理方法末尾都调用了 updateDisplay()。下面给出它的完整实现,放在 CalculatorActivity 中:

// 统一刷新显示屏
private void updateDisplay() {
   
    if (isError) {
   
        txtDisplay.setText("Error");   // 错误状态直接显示 Error
        return;
    }
    if (currentInput.isEmpty()) {
   
        txtDisplay.setText("0");       // 空输入显示 0,避免空白
        return;
    }
    txtDisplay.setText(formatNumber(currentInput));
}

// 格式化显示:去除多余前导零、控制小数位数、超长转科学计数法
private String formatNumber(String input) {
   
    // 1. 去除前导零,如 "007" -> "7",但保留 "0." 这种中间态
    if (input.startsWith("0") && input.length() > 1 && input.charAt(1) != '.') {
   
        input = input.replaceFirst("^0+", "");
        if (input.isEmpty()) input = "0";
    }

    // 2. 尝试转成 double 做数值格式化
    try {
   
        double value = Double.parseDouble(input);

        // 3. 超长数字自动切换为科学计数法
        if (Math.abs(value) >= 1e15 || (Math.abs(value) < 1e-9 && value != 0)) {
   
            return String.format("%.6e", value);
        }

        // 4. 整数结果不显示小数点;小数最多保留 8 位并去掉末尾的 0
        if (value == Math.floor(value) && !Double.isInfinite(value)) {
   
            return String.valueOf((long) value);
        }
        String formatted = String.format("%.8f", value);
        formatted = formatted.replaceAll("0+$", "").replaceAll("\\.$", "");
        return formatted;
    } catch (NumberFormatException e) {
   
        return input;   // 解析失败(如刚输入 ".")则原样返回
    }
}

这段代码把「显示」和「计算」彻底解耦:updateDisplay 只负责把内部状态翻译成用户看到的文本,不参与任何运算。其中 formatNumber 处理了三种常见情况——去除前导零、整数不显示小数点、超长数字转科学计数法,这样无论用户输入 007、5. 还是 123456789012345678,显示屏都能给出干净、可读的结果。

所谓的"print 函数逻辑”,在这里并非指控制台输出,而是指将数据“打印”到 UI 组件上。要注意线程安全问题,虽然 Android 的主线程(UI 线程)通常是单线程的,但在涉及耗时操作(虽然计算器很少见)时,仍需确保 UI 更新在主线程执行。此外,显示逻辑应包含格式化规则,比如保留小数点后几位,或者在整数结果时不显示小数点,提升视觉友好度。

关于线程安全,这里补充一个实用原则:所有对 txtDisplay 的写入都必须发生在主线程。由于我们的按钮点击事件天然运行在主线程,直接调用 updateDisplay() 即可,无需额外处理。但如果你将来把计算逻辑放到后台线程(比如接入网络 API 做汇率换算),就不能在子线程里直接改 UI,而应使用 runOnUiThread 或 Handler 切回主线程:

// 后台线程计算结果后,切回主线程刷新显示
new Thread(() -> {
   
    final double result = Calc.execute(previousValue,
            Double.parseDouble(currentInput), operator);
    runOnUiThread(() -> {
   
        currentInput = String.valueOf(result);
        updateDisplay();
    });
}).start();

最后再强调一个细节:updateDisplay 应该成为唯一的 UI 刷新入口。不要在 onClick、handleNumberKey 等各处散落 txtDisplay.setText(...) 调用,否则一旦格式化规则需要调整(比如把小数位数从 8 位改成 6 位),你就得满文件去找。把所有显示逻辑收敛到这一个方法里,后续维护和扩展都会轻松很多。

⑥ 输入合法性校验与 TipChecker 机制

用户输入往往不可控,可能会出现连续输入多个小数点、以运算符开头、或者除以零等情况。因此,建立一个名为 TipChecker 的校验机制非常必要。它不是一个独立的类,而是一组静态校验方法或策略模式的实现,可放在 Android 的工具类或 ViewModel 中。

下面是一份可直接套用的 TipChecker 实现,它把常见的非法输入场景统一收敛成几个静态方法,每个方法返回一个布尔值表示「是否允许本次操作」:

// 输入合法性校验工具类
public final class TipChecker {
   

    // 1. 检查是否允许追加数字/小数点
    public static boolean canAppend(String currentInput, String key) {
   
        if (currentInput.isEmpty()) {
   
            return true;                    // 空输入允许任何数字或小数点
        }
        if (".".equals(key)) {
   
            // 已有小数点则拒绝再次输入
            return !currentInput.contains(".");
        }
        return true;
    }

    // 2. 检查是否允许输入运算符(不能以运算符开头、不能连续输入运算符)
    public static boolean canSetOperator(String currentInput, String operator) {
   
        if (currentInput.isEmpty()) {
   
            return false;                   // 没有数字时不允许输入运算符
        }
        return true;
    }

    // 3. 检查除法是否合法(除数不能为零)
    public static boolean canDivide(double divisor) {
   
        return divisor != 0;
    }
}

这里把校验逻辑收敛成三个语义清晰的方法:canAppend 负责拦截「重复小数点」这类输入格式问题,canSetOperator 防止「以运算符开头」或「连续输入运算符」造成的逻辑错乱,canDivide 则在计算前预判除零风险。每个方法只回答一个「能不能」的问题,职责单一、便于测试。

例如,当用户试图在已有小数点的数字中再次输入"."时,TipChecker 应拦截该操作并忽略此次点击。当检测到除数为零时,它不应抛出异常导致程序崩溃,而是返回一个特定的错误状态码。校验逻辑应在点击事件回调(如 onClick)的早期阶段执行,一旦发现问题立即终止后续流程,防止非法数据污染核心变量。这种防御性编程思维能显著提升软件的鲁棒性。

那么,如何把 TipChecker 接入第四节的事件分发逻辑?关键原则是:在 onClick 路由之后、真正修改状态之前,先做一次校验。下面改造 handleNumberKey 和 handleOperatorKey,让非法输入在源头就被拦截:

// 数字键:先校验再追加
private void handleNumberKey(String key) {
   
    if (isError) return;                    // 错误状态下锁定输入
    if (!TipChecker.canAppend(currentInput, key)) {
   
        TipShow.show("不能重复输入小数点");   // 提示用户,忽略本次点击
        return;
    }
    if (isNewEntry) {
   
        currentInput = key;                 // 开始新数字
        isNewEntry = false;
    } else {
   
        currentInput += key;                // 继续追加
    }
    updateDisplay();
}

// 运算符键:先校验再记录
private void handleOperatorKey(String key) {
   
    if (isError) return;
    if (!TipChecker.canSetOperator(currentInput, key)) {
   
        TipShow.show("请先输入数字");        // 提示用户,忽略本次点击
        return;
    }
    if (!currentInput.isEmpty()) {
   
        if (!operator.isEmpty()) {
   
            previousValue = Calc.execute(previousValue,
                    Double.parseDouble(currentInput), operator);
        } else {
   
            previousValue = Double.parseDouble(currentInput);
        }
        currentInput = "";
    }
    operator = key;
    isNewEntry = true;
    updateDisplay();
}

这样改造后,TipChecker 就像一道「安检门」:所有按键在进入业务逻辑之前都要先过检,不合格的直接被拦下并给出提示,核心状态变量永远不会被非法数据污染。而 TipShow.show(...) 正是第七节要实现的用户提示机制,这里先调用它,把「校验」和「反馈」两个环节衔接起来,形成完整的防御闭环。

⑦ 用户提示信息展示与 TipShow 应用

当校验机制发现潜在问题时,如何反馈给用户?直接弹窗打断操作体验不佳,更好的方式是在显示屏下方或特定区域显示简短的提示信息,这就是 TipShow 的应用场景。

首先,在布局中为提示栏预留一个位置。我们在第二节的 activity_main.xml 里,在显示屏和键盘区之间插入一个轻量级的 TextView,平时保持空白,只在需要时显示提示文字:

<!-- res/layout/activity_main.xml(在显示屏与键盘区之间插入提示栏) -->
<TextView
    android:id="@+id/txtTip"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="14sp"
    android:textColor="#E53935"
    android:paddingHorizontal="12dp"
    android:paddingVertical="4dp"
    android:visibility="gone" />

这里用 visibility="gone" 让提示栏默认隐藏,不占用任何布局空间;textColor="#E53935" 使用醒目的红色,让提示信息一眼就能被用户注意到。这样声明好之后,Java 代码里只需 findViewById 拿到 txtTip 引用即可。

接下来实现 TipShow 的核心逻辑。它不是一个独立的类,而是一组静态方法,统一负责「显示提示」和「自动清除」两个动作。下面是一份可直接套用的实现,放在 CalculatorActivity 中:

// 提示信息展示工具方法
private static final Handler tipHandler = new Handler(Looper.getMainLooper());
private static final Runnable clearTipRunnable = () -> {
   
    TextView tip = findViewById(R.id.txtTip);
    if (tip != null) {
   
        tip.setVisibility(View.GONE);   // 超时后自动隐藏
    }
};

// 显示一条提示,并在 2 秒后自动消失
private static void TipShow(String message) {
   
    TextView tip = findViewById(R.id.txtTip);
    if (tip == null) return;
    tip.setText(message);
    tip.setVisibility(View.VISIBLE);
    tipHandler.removeCallbacks(clearTipRunnable);   // 重置计时
    tipHandler.postDelayed(clearTipRunnable, 2000); // 2 秒后清除
}

这里的关键在于「短提示 + 自动消失」:TipShow 先把文字写到提示栏并设为可见,再用 Handler 延迟 2 秒后隐藏。每次调用前先 removeCallbacks 重置计时,避免连续触发时提示被提前清掉。这样用户既能及时看到反馈,又不会被反复弹窗打断操作节奏。

那么,TipShow 如何与第六节的 TipChecker 配合?其实在第六节改造 handleNumberKey 和 handleOperatorKey 时,我们已经调用了 TipShow.show(...)。这里把「校验」和「反馈」串成完整闭环:TipChecker 负责判断「能不能」,TipShow 负责告诉用户「为什么不能」。例如用户重复输入小数点时,TipChecker.canAppend 返回 false,随即 TipShow.show("不能重复输入小数点") 给出提示,本次点击被忽略,核心状态不受影响。

对于致命错误(如除以零),处理方式略有不同:此时不应走提示栏,而应直接让显示屏显示 "Error",并配合 isError 标志锁定后续输入,直到用户按下清空键。这种「非致命错误走提示栏、致命错误走显示屏」的分级策略,既保证了交互的友好性,又不会让错误状态被轻易忽略,是提升应用专业感的关键细节。

⑧ 封装独立计算类 calc 的核心算法

将计算逻辑从界面代码中彻底剥离是本项目最关键的一步。创建一个独立的 Calc 类(或 CalculatorEngine),专门负责数学运算。该类不包含任何 UI 组件引用,只接收纯数据参数(如两个 double 值和一个操作符),返回计算结果。这样做的最大好处是:界面层永远不需要关心「怎么算」,只需要把用户按下的键翻译成参数交给 Calc,再把结果拿回来显示即可。

为了让 Calc 与第三节定义的 Operator 常量类无缝配合,我们让 execute 方法直接接收 Operator 中的常量字符串作为操作符。这样在第四节的事件分发里,handleOperatorKey 传入的 operator 变量就能原封不动地交给 Calc,无需任何转换,也彻底杜绝了魔法字符串散落在计算逻辑中。

在这个类中,实现基础的加减乘除方法,并处理特殊的数学边界情况。例如,平方根运算需检查负数,取模运算需检查除数。通过封装,即使未来需要支持更复杂的函数(如三角函数、对数),也只需修改这个类,而无需触动界面代码。这种设计使得单元测试成为可能,你可以单独针对 Calc 类编写测试用例,验证算法的准确性,而不必启动整个 Android 界面。

下面是一份可直接套用的完整实现。它把键盘上出现的所有运算(加、减、乘、除、取模、取反)都收敛到 execute 一个入口,并针对除零、负数开方等边界情况做了防御性处理:

// 独立计算类:只负责数学运算,不依赖任何 UI 组件
public final class Calc {
   

    // 统一入口:根据操作符执行二元运算
    public static double execute(double a, double b, String op) {
   
        switch (op) {
   
            case Operator.ADD:
                return a + b;
            case Operator.SUB:
                return a - b;
            case Operator.MUL:
                return a * b;
            case Operator.DIV:
                if (b == 0) {
   
                    throw new ArithmeticException("Divide by zero");
                }
                return a / b;
            case Operator.MOD:
                if (b == 0) {
   
                    throw new ArithmeticException("Modulo by zero");
                }
                return a % b;
            default:
                // 未知操作符:返回第二个操作数,避免界面崩溃
                return b;
        }
    }

    // 一元运算:取反(± 键)
    public static double negate(double value) {
   
        return -value;
    }

    // 一元运算:平方根(扩展用),负数直接抛异常
    public static double sqrt(double value) {
   
        if (value < 0) {
   
            throw new ArithmeticException("Sqrt of negative number");
        }
        return Math.sqrt(value);
    }
}

这里有几个设计要点值得注意:execute 用 switch 配合 Operator 常量,比裸字符串更安全——一旦在常量类里改动了某个符号,所有调用处自动生效,不会出现「界面传 +、计算类却只认 ADD」的错位问题。除零和取模除零都主动抛出 ArithmeticException,由上层(第六节的 TipChecker 或 handleFunctionKey)捕获后转为 Error 显示,而不是让程序静默产生 Infinity 或 NaN。negate 和 sqrt 作为一元运算单独拆出,方便后续在功能键里直接调用,也让类的职责边界更清晰。

⑨ 功能联调测试与常见报错排查

当各个模块完成后,联调测试是发现问题的关键环节。常见问题包括:连续点击运算符导致逻辑错乱、清空后状态未完全重置、长数字显示溢出等。在测试过程中,要模拟用户的非常规操作路径,比如快速连续点击、混合输入非法字符等。下面是一份可直接套用的测试用例清单,覆盖了计算器最常见的边界场景:

测试场景 操作步骤 预期结果
基础四则运算 依次输入 12 + 34 = 显示 46
连续运算 输入 5 + 3 * 2 = 按顺序计算显示 16(本项目不引入优先级)
连续点击运算符 输入 8 + + - 2 = 后一个运算符覆盖前一个,最终显示 6
重复小数点 输入 3 . . 5 第二次 . 被拦截并提示,显示 3.5
除以零 输入 9 / 0 = 显示屏显示 Error,后续输入被锁定
清空重置 在 Error 状态下按 C 状态全部重置,显示 0,可继续输入
退格 输入 123 后按 ← 显示 12
长数字溢出 连续输入 20 位数字 自动切换为科学计数法显示
取反 输入 5 后按 ± 显示 -5
取模 输入 10 % 3 = 显示 1

建议在真机或模拟器上逐条跑通上述用例,并额外做一轮「乱序点击」压力测试:随机快速点击数字、运算符、功能键,观察是否出现状态错乱或崩溃。只有把这些非常规路径都覆盖到,才能确认应用在真实用户手中足够健壮。

排查报错时,日志是关键。可以在关键逻辑节点加入调试日志(开发阶段),记录状态变量的变化。例如在 handleOperatorKey 和 handleFunctionKey 中打印当前状态,能快速定位「哪一步状态被改错了」:

// 调试日志示例:在关键节点打印状态变化
private void logState(String tag) {
   
    Log.d("Calculator", tag
            + " | currentInput=" + currentInput
            + " | previousValue=" + previousValue
            + " | operator=" + operator
            + " | isNewEntry=" + isNewEntry
            + " | isError=" + isError);
}

// 在 handleOperatorKey 末尾调用
logState("handleOperatorKey");
// 在 handleFunctionKey 的 "=" 分支计算后调用
logState("handleFunctionKey =");

如果遇到界面冻结,检查是否在主线程/UI 线程执行了耗时操作;如果数据显示不对,重点检查字符串转数值的解析逻辑以及浮点数精度处理。对于空指针异常,通常是因为组件初始化顺序错误或事件监听器未正确绑定。下面把最常见的几类报错整理成排查对照表,方便你按图索骥:

报错现象 可能原因 排查方向
NullPointerException findViewById 返回 null,或按钮未正确绑定监听器 检查 onCreate 中布局是否先 setContentView 再取引用;确认 keyPanel 里的按钮都调用了 setOnClickListener
界面冻结 / ANR 主线程执行了耗时操作 确认计算逻辑没有放在主线程;必要时用 runOnUiThread 切回主线程刷新
显示 Infinity 或 NaN 除零未被拦截,Calc.execute 抛出的异常未捕获 在 handleFunctionKey 的 = 分支用 try-catch 捕获 ArithmeticException,置 isError = true
连续运算符逻辑错乱 handleOperatorKey 未处理「已有运算符」的覆盖逻辑 检查是否在记录新运算符前先完成上一次中间计算
清空后仍显示旧结果 C 分支未重置全部状态变量 确认 currentInput、previousValue、operator、isNewEntry、isError 全部复位
长数字显示溢出 formatNumber 未处理超长数字 检查是否在 Math.abs(value) >= 1e15 时切换科学计数法
旋转屏幕后计算丢失 未实现 onSaveInstanceState / onCreate 恢复 对照第三节的保存与恢复代码逐项核对

通过系统性的回归测试,确保每一个按钮在不同状态下都能按预期工作。建议把上面这份排查表打印出来贴在工位旁,遇到问题先对照「可能原因」缩小范围,再结合 logState 打印的状态变化定位具体是哪一步出错,往往能比盲目改代码快得多。

⑩ 代码优化技巧与扩展功能思路

项目基本功能完成后,代码优化能让其更上一层楼。首先,提取重复代码,将相似的 View 创建逻辑、事件处理逻辑抽象为私有方法。例如,第二节里动态创建按钮的循环可以封装成一个 createButton 方法,统一设置文本、字号、布局参数和点击标记,避免在多个地方重复写 new Button(this) 和 setText:

// 封装按钮创建逻辑,统一样式与标记
private Button createButton(String text, String tag) {
   
    Button btn = new Button(this);
    btn.setText(text);
    btn.setTextSize(18);
    btn.setTag(tag);   // "num"、"op"、"func"
    GridLayout.LayoutParams params = new GridLayout.LayoutParams();
    params.width = 0;
    params.height = GridLayout.LayoutParams.WRAP_CONTENT;
    params.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f);
    params.rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f);
    params.setMargins(4, 4, 4, 4);
    btn.setLayoutParams(params);
    return btn;
}

// 初始化键盘时直接复用
for (String key : keys) {
   
    keyPanel.addView(createButton(key, classifyKey(key)));
}

其次,利用常量池管理所有的按钮标签和配置参数,避免硬编码。可以把 "num"、"op"、"func" 这类标记以及 "C"、"←"、"=" 等按键文本统一收敛到常量类中,配合第三节的 Operator 常量一起使用,让整个项目的魔法字符串彻底消失:

// 按键类型与文本常量池
public final class KeyTag {
   
    public static final String NUM = "num";
    public static final String OP = "op";
    public static final String FUNC = "func";

    public static final String CLEAR = "C";
    public static final String BACKSPACE = "←";
    public static final String EQUALS = "=";
}

// 根据按键文本判断类型,供 createButton 使用
private String classifyKey(String key) {
   
    if (key.equals(KeyTag.CLEAR) || key.equals(KeyTag.BACKSPACE) || key.equals(KeyTag.EQUALS)) {
   
        return KeyTag.FUNC;
    }
    if (key.equals(Operator.ADD) || key.equals(Operator.SUB)
            || key.equals(Operator.MUL) || key.equals(Operator.DIV)
            || key.equals(Operator.MOD) || key.equals(Operator.NEG)) {
   
        return KeyTag.OP;
    }
    return KeyTag.NUM;
}

还可以引入观察者模式,让界面自动监听数据模型的变化,进一步解耦。对于计算器这种小型项目,最轻量的做法是定义一个简单的状态回调接口,当 Calc 计算出新结果时通知界面刷新,而不是在 handleFunctionKey 里手动调用 updateDisplay。这样界面层只负责「订阅」,模型层只负责「发布」,两者彻底解耦,后续无论是换 UI 框架还是加新功能,改动面都更小。

关于扩展功能,思路非常广阔。下面挑三个最实用、也最容易上手的扩展方向,给出具体的实现思路,你可以按需挑选:

1. 历史记录功能:在 Calc 之外维护一个 List<String> 保存每次等号计算后的表达式与结果,例如 "12 + 34 = 46"。界面底部加一个可折叠的 RecyclerView 或简单的 ListView 展示历史,点击某条历史可把结果回填到显示屏继续运算。注意历史记录也要在 onSaveInstanceState 中持久化,避免旋转屏幕后丢失。

2. 键盘快捷键输入:在 Activity 中重写 onKeyDown,把物理键盘的数字键、+ - * /、回车(等号)、退格、Esc(清空)映射到已有的 handleNumberKey、handleOperatorKey、handleFunctionKey 方法。这样在模拟器或外接键盘上也能快速操作,代码量很小但体验提升明显:

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
   
    switch (keyCode) {
   
        case KeyEvent.KEYCODE_0: case KeyEvent.KEYCODE_1:
        case KeyEvent.KEYCODE_2: case KeyEvent.KEYCODE_3:
        case KeyEvent.KEYCODE_4: case KeyEvent.KEYCODE_5:
        case KeyEvent.KEYCODE_6: case KeyEvent.KEYCODE_7:
        case KeyEvent.KEYCODE_8: case KeyEvent.KEYCODE_9:
            handleNumberKey(String.valueOf(keyCode - KeyEvent.KEYCODE_0));
            return true;
        case KeyEvent.KEYCODE_ENTER:
            handleFunctionKey(KeyTag.EQUALS);
            return true;
        case KeyEvent.KEYCODE_DEL:
            handleFunctionKey(KeyTag.BACKSPACE);
            return true;
        case KeyEvent.KEYCODE_ESCAPE:
            handleFunctionKey(KeyTag.CLEAR);
            return true;
        default:
            return super.onKeyDown(keyCode, event);
    }
}

3. 主题切换:把颜色、字号等视觉参数抽到 res/values/themes.xml 中定义多套主题,运行时通过 setTheme 或 AppCompatDelegate.setDefaultNightMode 切换。更轻量的做法是维护一个 ThemeConfig 常量类,保存背景色、按钮色、文字色,切换时统一调用 updateDisplay 和按钮刷新方法重设颜色,避免散落各处的硬编码色值。

如果技术栈允许,还可以尝试接入网络 API,实现单位换算或汇率计算等高级功能。这些扩展不仅丰富了应用功能,也是深入理解软件架构演进的绝佳实践机会。建议先从「历史记录」和「键盘快捷键」这两个改动小、见效快的方向入手,跑通后再挑战主题切换和网络功能,循序渐进地提升自己的 Android 工程能力。

⑪ 完整实战代码:一键跑通整个项目

前面十节我们把计算器的各个模块拆开讲解,这里把它们整合成一份可直接复制运行的完整项目。你只需按第一节的方式新建一个 Empty Views Activity 项目(包名 com.example.calculator,语言 Java),然后把下面三个文件分别替换到对应位置,即可在真机或模拟器上跑通整个计算器。

1. 布局文件 res/layout/activity_main.xml

把显示屏、提示栏和键盘区一次性定义好,键盘按钮用 GridLayout 动态填充:

<!-- res/layout/activity_main.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <!-- 显示屏区域 -->
    <TextView
        android:id="@+id/txtDisplay"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="end"
        android:textSize="40sp"
        android:text="0"
        android:padding="12dp"
        android:background="#EEEEEE" />

    <!-- 提示栏(默认隐藏,仅在需要时显示) -->
    <TextView
        android:id="@+id/txtTip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textColor="#E53935"
        android:paddingHorizontal="12dp"
        android:paddingVertical="4dp"
        android:visibility="gone" />

    <!-- 键盘区域 -->
    <GridLayout
        android:id="@+id/keyPanel"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:columnCount="5"
        android:rowCount="4" />
</LinearLayout>

2. 运算符号常量类 Operator.java

放在 com.example.calculator 包下,统一管理所有运算符,杜绝魔法字符串:

// 运算符号常量类
public final class Operator {
   
    public static final String ADD = "+";
    public static final String SUB = "-";
    public static final String MUL = "*";
    public static final String DIV = "/";
    public static final String MOD = "%";
    public static final String NEG = "±";
}

3. 独立计算类 Calc.java

只负责数学运算,不依赖任何 UI 组件,除零等边界情况主动抛异常:

// 独立计算类:只负责数学运算,不依赖任何 UI 组件
public final class Calc {
   

    // 统一入口:根据操作符执行二元运算
    public static double execute(double a, double b, String op) {
   
        switch (op) {
   
            case Operator.ADD:
                return a + b;
            case Operator.SUB:
                return a - b;
            case Operator.MUL:
                return a * b;
            case Operator.DIV:
                if (b == 0) {
   
                    throw new ArithmeticException("Divide by zero");
                }
                return a / b;
            case Operator.MOD:
                if (b == 0) {
   
                    throw new ArithmeticException("Modulo by zero");
                }
                return a % b;
            default:
                // 未知操作符:返回第二个操作数,避免界面崩溃
                return b;
        }
    }

    // 一元运算:取反(± 键)
    public static double negate(double value) {
   
        return -value;
    }

    // 一元运算:平方根(扩展用),负数直接抛异常
    public static double sqrt(double value) {
   
        if (value < 0) {
   
            throw new ArithmeticException("Sqrt of negative number");
        }
        return Math.sqrt(value);
    }
}

4. 输入校验工具类 TipChecker.java

把「重复小数点」「以运算符开头」「除零」等非法输入统一拦截在源头:

// 输入合法性校验工具类
public final class TipChecker {
   

    // 1. 检查是否允许追加数字/小数点
    public static boolean canAppend(String currentInput, String key) {
   
        if (currentInput.isEmpty()) {
   
            return true;                    // 空输入允许任何数字或小数点
        }
        if (".".equals(key)) {
   
            // 已有小数点则拒绝再次输入
            return !currentInput.contains(".");
        }
        return true;
    }

    // 2. 检查是否允许输入运算符(不能以运算符开头、不能连续输入运算符)
    public static boolean canSetOperator(String currentInput, String operator) {
   
        if (currentInput.isEmpty()) {
   
            return false;                   // 没有数字时不允许输入运算符
        }
        return true;
    }

    // 3. 检查除法是否合法(除数不能为零)
    public static boolean canDivide(double divisor) {
   
        return divisor != 0;
    }
}

5. 主界面 CalculatorActivity.java

把状态管理、事件分发、显示更新、提示机制全部整合在一起,实现 OnClickListener 接口:

// 主界面:整合状态管理、事件分发、显示更新与提示机制
public class CalculatorActivity extends AppCompatActivity implements View.OnClickListener {
   

    // 核心状态变量
    private String currentInput = "";   // 当前正在输入的数字字符串
    private double previousValue = 0;   // 上一次运算的结果
    private String operator = "";       // 当前选定的运算符,如 "+"、"-"
    private boolean isNewEntry = true;  // 是否开始新的数字输入
    private boolean isError = false;    // 是否处于错误状态(如除以零)

    private TextView txtDisplay;
    private TextView txtTip;

    // 提示信息自动清除
    private static final Handler tipHandler = new Handler(Looper.getMainLooper());
    private static final Runnable clearTipRunnable = () -> {
   
        TextView tip = findViewById(R.id.txtTip);
        if (tip != null) {
   
            tip.setVisibility(View.GONE);   // 超时后自动隐藏
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
   
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        txtDisplay = findViewById(R.id.txtDisplay);
        txtTip = findViewById(R.id.txtTip);

        // 恢复状态(旋转屏幕或系统回收后)
        if (savedInstanceState != null) {
   
            currentInput = savedInstanceState.getString("currentInput", "");
            previousValue = savedInstanceState.getDouble("previousValue", 0);
            operator = savedInstanceState.getString("operator", "");
            isNewEntry = savedInstanceState.getBoolean("isNewEntry", true);
            isError = savedInstanceState.getBoolean("isError", false);
        }

        initKeyPanel();
        updateDisplay();
    }

    // 初始化键盘:动态创建按钮并绑定点击事件
    private void initKeyPanel() {
   
        GridLayout keyPanel = findViewById(R.id.keyPanel);
        String[] keys = {
   "7", "8", "9", "/", "C",
                         "4", "5", "6", "*", "←",
                         "1", "2", "3", "-", "=",
                         "0", ".", "+", "%", "±"};

        for (String key : keys) {
   
            Button btn = createButton(key, classifyKey(key));
            keyPanel.addView(btn);
        }
    }

    // 封装按钮创建逻辑,统一样式与标记
    private Button createButton(String text, String tag) {
   
        Button btn = new Button(this);
        btn.setText(text);
        btn.setTextSize(18);
        btn.setTag(tag);   // "num"、"op"、"func"
        btn.setOnClickListener(this);
        GridLayout.LayoutParams params = new GridLayout.LayoutParams();
        params.width = 0;
        params.height = GridLayout.LayoutParams.WRAP_CONTENT;
        params.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f);
        params.rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f);
        params.setMargins(4, 4, 4, 4);
        btn.setLayoutParams(params);
        return btn;
    }

    // 根据按键文本判断类型
    private String classifyKey(String key) {
   
        if (key.equals("C") || key.equals("←") || key.equals("=")) {
   
            return "func";
        }
        if (key.equals(Operator.ADD) || key.equals(Operator.SUB)
                || key.equals(Operator.MUL) || key.equals(Operator.DIV)
                || key.equals(Operator.MOD) || key.equals(Operator.NEG)) {
   
            return "op";
        }
        return "num";
    }

    // 统一事件分发入口
    @Override
    public void onClick(View v) {
   
        Button btn = (Button) v;
        String key = btn.getText().toString();
        String tag = (String) btn.getTag();

        if ("func".equals(tag)) {
   
            handleFunctionKey(key);   // 处理 C、←、= 等功能键
        } else if ("op".equals(tag)) {
   
            handleOperatorKey(key);   // 处理 +、-、*、/、%、± 等运算符
        } else {
   
            handleNumberKey(key);     // 处理数字和小数点
        }
    }

    // 数字键:先校验再追加
    private void handleNumberKey(String key) {
   
        if (isError) return;                    // 错误状态下锁定输入
        if (!TipChecker.canAppend(currentInput, key)) {
   
            TipShow("不能重复输入小数点");       // 提示用户,忽略本次点击
            return;
        }
        if (isNewEntry) {
   
            currentInput = key;                 // 开始新数字
            isNewEntry = false;
        } else {
   
            currentInput += key;                // 继续追加
        }
        updateDisplay();
    }

    // 运算符键:先校验再记录
    private void handleOperatorKey(String key) {
   
        if (isError) return;
        if (!TipChecker.canSetOperator(currentInput, key)) {
   
            TipShow("请先输入数字");            // 提示用户,忽略本次点击
            return;
        }
        if (!currentInput.isEmpty()) {
   
            if (!operator.isEmpty()) {
   
                // 已有运算符,先完成上一次中间计算
                previousValue = Calc.execute(previousValue,
                        Double.parseDouble(currentInput), operator);
            } else {
   
                previousValue = Double.parseDouble(currentInput);
            }
            currentInput = "";
        }
        operator = key;
        isNewEntry = true;
        updateDisplay();
    }

    // 功能键:清空、退格、等号
    private void handleFunctionKey(String key) {
   
        switch (key) {
   
            case "C":   // 清空所有状态
                currentInput = "";
                previousValue = 0;
                operator = "";
                isNewEntry = true;
                isError = false;
                break;
            case "←":   // 退格:去掉最后一个字符
                if (!currentInput.isEmpty()) {
   
                    currentInput = currentInput.substring(0, currentInput.length() - 1);
                }
                break;
            case "=":   // 等号:完成最终计算
                if (!operator.isEmpty() && !currentInput.isEmpty()) {
   
                    try {
   
                        previousValue = Calc.execute(previousValue,
                                Double.parseDouble(currentInput), operator);
                        currentInput = String.valueOf(previousValue);
                        operator = "";
                        isNewEntry = true;
                    } catch (ArithmeticException e) {
   
                        isError = true;         // 除零等致命错误
                    }
                }
                break;
        }
        updateDisplay();
    }

    // 统一刷新显示屏
    private void updateDisplay() {
   
        if (isError) {
   
            txtDisplay.setText("Error");   // 错误状态直接显示 Error
            return;
        }
        if (currentInput.isEmpty()) {
   
            txtDisplay.setText("0");       // 空输入显示 0,避免空白
            return;
        }
        txtDisplay.setText(formatNumber(currentInput));
    }

    // 格式化显示:去除多余前导零、控制小数位数、超长转科学计数法
    private String formatNumber(String input) {
   
        // 1. 去除前导零,如 "007" -> "7",但保留 "0." 这种中间态
        if (input.startsWith("0") && input.length() > 1 && input.charAt(1) != '.') {
   
            input = input.replaceFirst("^0+", "");
            if (input.isEmpty()) input = "0";
        }

        // 2. 尝试转成 double 做数值格式化
        try {
   
            double value = Double.parseDouble(input);

            // 3. 超长数字自动切换为科学计数法
            if (Math.abs(value) >= 1e15 || (Math.abs(value) < 1e-9 && value != 0)) {
   
                return String.format("%.6e", value);
            }

            // 4. 整数结果不显示小数点;小数最多保留 8 位并去掉末尾的 0
            if (value == Math.floor(value) && !Double.isInfinite(value)) {
   
                return String.valueOf((long) value);
            }
            String formatted = String.format("%.8f", value);
            formatted = formatted.replaceAll("0+$", "").replaceAll("\\.$", "");
            return formatted;
        } catch (NumberFormatException e) {
   
            return input;   // 解析失败(如刚输入 ".")则原样返回
        }
    }

    // 显示一条提示,并在 2 秒后自动消失
    private void TipShow(String message) {
   
        if (txtTip == null) return;
        txtTip.setText(message);
        txtTip.setVisibility(View.VISIBLE);
        tipHandler.removeCallbacks(clearTipRunnable);   // 重置计时
        tipHandler.postDelayed(clearTipRunnable, 2000); // 2 秒后清除
    }

    // 保存状态:旋转屏幕或系统回收时保留计算过程
    @Override
    protected void onSaveInstanceState(Bundle outState) {
   
        super.onSaveInstanceState(outState);
        outState.putString("currentInput", currentInput);
        outState.putDouble("previousValue", previousValue);
        outState.putString("operator", operator);
        outState.putBoolean("isNewEntry", isNewEntry);
        outState.putBoolean("isError", isError);
    }
}

6. 运行与验证

把上面五个文件分别放到对应位置后,直接点击 Android Studio 的 Run 按钮即可。建议先按第九节的测试用例清单逐条验证:12 + 34 = 显示 46、9 / 0 = 显示 Error、连续输入 20 位数字自动切换科学计数法。全部跑通后,你的 Android 计算器就完整落地了。

至此,我们从环境搭建、界面布局、状态管理、事件监听、显示更新、输入校验、提示机制、计算封装,到联调测试与代码优化,完整走完了一个 Android 计算器的开发全流程。希望这份完整代码能成为你动手实践时的「对照答案」,也欢迎在此基础上继续扩展历史记录、键盘快捷键、主题切换等功能,把学到的架构思想真正用起来。

相关文章
|
22天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9304 29
|
9天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
21天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2437 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
7天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
603 1
|
2天前
|
存储 缓存 测试技术
QWT 3D 模块重构记:旧渲染管线改进介绍及实时瀑布图示例
QWT 3D模块完成重大重构:解耦窗口与图形(Plot+Item),摒弃OpenGL 1.x固定管线,迁至Core Profile着色器渲染;支持多图元同场景叠加,并以实时3D瀑布图验证——滑动窗口、固定色域、64帧×256点达61fps。兼容Qt 5.12+/6,LGPL协议。
154 120
|
10天前
|
人工智能 Linux Windows
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面
千问办公(QwenWork)是阿里云推出的AI智能办公平台,支持网页端直接使用及Windows/Mac/Linux客户端下载。提供PPT生成、财报分析、网页搭建等AI功能,个人版免费,企业版198元/席/月。详情见官网qwenwork.cn或阿里云产品页。
1167 0
千问办公(QwenWork)官网入口:其实有2个,一个是网页端千问办公,一个是介绍指南页面
|
14天前
|
Windows
【保姆级】.NET Framework 3.5离线安装教程(2026最新实测)
.NET Framework 3.5是微软经典运行库,专为兼容老软件、老游戏设计。Win7自带,Win10/11需手动启用(在线或离线安装)。它与4.x系列并存不冲突,缺它会导致“找不到组件”报错。本文详解两种安装方式及常见问题解决,小白也能轻松上手。(239字)
1760 0
|
4天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型全解析:核心原理拆解,性能实战测评与落地部署教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为System One Model(系统一模型),对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
380 0