Java 课程设计 - Android 计算器

Android 计算器 - Shuai-Xie - Github

一、设计分析

1.1 题目重述

本次实验为了实现一个保存计算过程的计算器,主要有以下三个要求:

  1. 仿照真实的计算器实现其功能。
  2. 在左上方的文本框中显示当前的计算过程,在右边的文本区中显示以往的计算过程。
  3. 单击“保存”按钮可以将文本区中的全部计算过程保存到文件;单击“复制”按钮可以将文本区中选中的文本复制到剪贴本;单击“清除”按钮可以清除文本区的全部内容。

1.2 设计思路

考虑到计算器的美观和易用性,我决定把计算器做在 Android 端,Android 系统的 App 的底层功能由 Java 实现,所以工作主要分为两部分:

  1. 设计计算器界面 (activity_main.xml)
    计算器界面用xml文件编写,包括手机竖屏和横屏两个布局文件:
    竖屏:activity_main.xml 布局为 portrait
    横屏:activity_main_land.xml 布局为 landscape
    竖屏模式可以完成基本的四则运算,不涉及科学计算
    横屏模式除了完成基本的四则运算,还添加了科学运算

  2. 编写计算接口 (ScienceCalculator.java)
    ScienceCalculator 可以完成包含科学运算函数的 math,先实现可以完成基本四则运算的 BaseCalculator,在此基础上,实现 ScienceCalculator。
    运算的思路是先通过 ScienceCalculator 完成math中需要科学计算函数的部分,再用这些部分计算的结果替换原 math 中的这些部分,使包含科学计算函数的 math 转变成可用 BaseCalculator 计算的 math。

二、程序结构

Java 课程设计 - Android 计算器_第1张图片
图2.1 程序结构流程图
Java 课程设计 - Android 计算器_第2张图片
图2.2 横屏程序界面

三、各模块的功能及程序说明

3.1 计算器界面设计

3.1.1 竖屏界面

包含控件

  1. 文本框 TextView:tvNowt,vPast 分别显示当前和过去的运算过程;
  2. 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,复制,清空tvPast中的运算过程;
  3. 数字 Button:0-9,小数点
  4. 运算符 Button:+ - × / ( ) =
  5. 运算器基本 Button:btn_del 退格,btn_clc 清空当前math

成员变量

  1. String mathPast,用于存储过去的运算过程
  2. String mathNow,用于存储当前的运算过程,即用户正在输入的部分
  3. int precision,设置默认精度为6位小数
  4. int equal_flag,设置flag值判断是否需要清空mathNow进行新的运算
  5. ScienceCalculator scienceCalculator,实例化一个科学计算器
Java 课程设计 - Android 计算器_第3张图片
图3.1 竖屏界面

3.1.2 横屏界面

包含控件

  1. 文本框 TextView:tvNow, tvPast 分别显示当前和过去的运算过程;
  2. 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,复制,清空tvPast中的运算过程;
  3. 数字Button:0-9,小数点
  4. 基本运算符Button:+ - × / ( ) =
  5. 科学运算符Button:(12个)
    sin,cos,tan,√x,e,π,1/x,ln,log,x2,ex,xy
  6. 运算器基本Button:btn_del退格,btn_clc清空当前math
  7. 文本框切换按钮 tvRad,tvDeg 实现弧度制和角度值的切换
  8. 精度选择器 NumberPicker

成员常量

  1. final int DEG = 0,DEG 表示角度制
  2. final int RAD = 1,RAD 表示弧度制

成员变量

  1. String mathPast,用于存储过去的运算过程
  2. String mathNow,用于存储当前的运算过程,即用户正在输入的部分
  3. int precision,设置默认精度为6位小数,通过NumberPicker返回用户设置的精度值
  4. int equal_flag,设置flag值判断是否需要清空mathNow进行新的运算
  5. ScienceCalculator scienceCalculator,实例化一个科学计算器
  6. int angle_metric,角度制参数,默认为DEG
Java 课程设计 - Android 计算器_第4张图片
图3.2 横屏界面

3.2 界面各模块功能

由于横评界面包括了竖屏界面所有的模块,下文代码功能描述按照 LandActivity.java 文件,即横评界面对应的 Activity。

3.2.1 初始化 tvPast

tvPast 用于存储过去的运算过程

public void initTvPast() {

    //设置tvPast一些属性
    tvPast.setMovementMethod(ScrollingMovementMethod.getInstance()); //内容自动滚动到最新的一行
    tvPast.setTextIsSelectable(true); //长按复制

    //获取界面切换的tvPast的内容
    Intent intent = this.getIntent();
    String tvPastContent = intent.getStringExtra("main");

    //如果当前的界面是启动界面,不是从MainActivity切换来的,上面的mathPast就为null了,要处理这种异常
    if (tvPastContent == null) {
        tvPast.setText("");
    } else {
        String[] maths = tvPastContent.split("\n");
        int i;
        for (i = 0; i < maths.length - 1; i++) {
            tvPast.append(maths[i] + "\n");
        }
        tvPast.append(maths[i]); //最后一个math不用加换行
    }
}

响应场景设置:

  1. 因为tvPast文本框高度有限,为了使用户每次都可以看到最新的运算过程,设置 setMovementMethod(ScrollingMovementMethod.getInstance()) 方法使内容自动滚动到最新的一行;
tvPast.setMovementMethod(ScrollingMovementMethod.getInstance());
  1. Android系统集成了很好的文本框内容复制功能,设置 setTextIsSelectable(true) 即可实现文本框的长按复制功能;
tvPast.setTextIsSelectable(true);
  1. 由于计算器具有2个界面,当前的界面可能是从竖屏界面切换来(如果当前界面是竖屏,界面也有可能是从横屏界面切换而来),通过Intent类在两个Activity间传递tvPast的内容,至于用for循环逐行添加过去的运算过程是为了满足(1)使内容自动滚动到最新的一行。
//获取界面切换的tvPast的内容
Intent intent = this.getIntent();
String tvPastContent = intent.getStringExtra("main");
//如果当前的界面是启动界面,不是从MainActivity切换来的,上面的mathPast就为null了,要处理这种异常
if (tvPastContent == null) {
    tvPast.setText("");
} else {
    String[] maths = tvPastContent.split("\n");
    int i;
    for (i = 0; i < maths.length - 1; i++) {
        tvPast.append(maths[i] + "\n");
    }
    tvPast.append(maths[i]); //最后一个math不用加换行
}

3.2.2 初始化 NumButtons:0-9,小数点

按钮需要设置监听事件的应用场景,是为了避免一些错误的math格式。因为不同的数字有不同的处理方式。主要归为以下几类:

1. btn_0

btn_0 根据响应事件场景在当前 math 表达式中添加 0

btn0.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        //如果flag=1,表示要输入新的运算式,清空mathNow并设置flag=0
        if (equal_flag == 1) {
            mathNow = "";
            equal_flag = 0;
        }

        if (mathNow.length() == 0) {                    //1.mathNow为空,+0
            mathNow += "0";
        } else if (mathNow.length() == 1) {             //2.mathNow 长度为1

            if (mathNow.charAt(0) == '0') {                 //2.1 如果该字符为0,不加
                mathNow += "";
            } else if (isNum(mathNow.charAt(0))) {          //2.2 如果该字符为1-9,+0
                mathNow += "0";
            }

        } else if (!isNum(mathNow.charAt(mathNow.length() - 2)) && mathNow.charAt(mathNow.length() - 1) == '0') {
            mathNow += "";                              //3.属于2.1的一般情况,在math中间出现 比如:×0 +0
        } else {                                        //4.除此之外,+0
            mathNow += "0";
        }
        tvNow.setText(mathNow);
    }
});

响应场景设置:

  • 设置 flag 值判断是否需要清空 mathNow 进行新的运算,该功能是为了方便用户的输入,用户在完成一次计算之后,不需要点击清空按钮就可以直接输入新的运算过程,当 equal_flag 为1时表示刚刚完成一次运算,可以直接输入新的运算式了,此时完成 mathNow 清空操作,并重置 equal_flag 为 0;

  • 是否添加0的场景设置:

    • mathNow 长度为0,添加0
    • mathNow 长度为1,当前输入1个char了
      如果当前 char 为0,不添加0
      如果当前 char 为1-9,添加0
    • mathNow 长度 >1,if中的条件是2.1的一般情况,即在 math 中间出现了,mathNow 的倒数第2个 char 不是 Num 并且 mathNow 的最后一个 char 是0,
      如 2 + 3 ×0 ,此时也不添加0
    • 除此之外,添加0
2. btn_[1-9]

btn_1 ~ btn_9 的响应场景相同,根据响应事件场景在当前 math 表达式添加 [1-9]

btn1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        if (equal_flag == 1) {
            mathNow = "";
            equal_flag = 0;
        }

        if (mathNow.length() == 0) {
            mathNow += "1";
        } else {

            //math的最后一个字符是:1-9, oper, (, .
            char ch = mathNow.charAt(mathNow.length() - 1);
            if (isNum(ch) && ch != '0' || isOper(ch) || ch == '(' || ch == '.')
                mathNow += "1";
        }
        tvNow.setText(mathNow);
    }
});

响应场景设置:

  • equal_flag 同 btn_0;
  • mathNow 长度为0,添加[1-9];
  • mathNow 最后一个 char 是 [0-9],oper,(,小数点 这4种情况时,+[1-9];
  • 除此之外,不 +[1-9]
3. btn_dot 小数点

小数点操作要比普通数字要多一点,有时点击添加的是“0.”
btn_dot 根据响应事件场景在当前math表达式中添加“.”或者“0.”

btnDot.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (equal_flag == 1) {
            mathNow = "";
            equal_flag = 0;
        }
        if (mathNow.length() == 0) {                                //1.mathNow为空,+0.
            mathNow += "0.";
        } else if (isOper(mathNow.charAt(mathNow.length() - 1))) {  //2.mathNow的最后一个字符为oper,+0.
            mathNow += "0.";
        } else if (isNum(mathNow.charAt(mathNow.length() - 1))) {   //3.mathNow的最后一个字符为num,+.
            mathNow += ".";
        } else {                                                    //4.除此之外,不加
            mathNow += "";
        }
        tvNow.setText(mathNow);
    }
});

响应场景设置:

  • equal_flag 同 btn_0;
  • mathNow 长度为0,添加“0.”
  • mathNow 的最后一个 char 为 oper,添加“0.”
  • mathNow 的最后一个字符为 num,添加“.”
  • 除此之外,不添加

3.2.3 初始化 BaseOperButtons

包括 + - × / ( ) =

1. btn_add +
btnAdd.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mathNow.length() == 0) {
            mathNow += "+";
        } else {
            if (isNum(mathNow.charAt(mathNow.length() - 1))
                    || mathNow.charAt(mathNow.length() - 1) == ')'
                    || mathNow.charAt(mathNow.length() - 1) == '('
                    || mathNow.charAt(mathNow.length() - 1) == 'π'
                    || mathNow.charAt(mathNow.length() - 1) == 'e')
                mathNow += "+";
        }
        tvNow.setText(mathNow);
        equal_flag = 0; //可能用运算结果直接运算,flag直接设0
    }
});

响应场景设置:

  1. mathNow长度为0,添加“+”,表示正数
  2. 以下5种场景都可以添加“+”,设char是mathNow的最后一个char:
    • char是Num
    • char是“)”
    • char是“(”
    • char是“π”
    • char是“e”,自然指数
2. btn_sub -
btnSub.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mathNow.length() == 0) {
            mathNow += "-";
        } else {
            if (isNum(mathNow.charAt(mathNow.length() - 1))
                    || mathNow.charAt(mathNow.length() - 1) == ')'
                    || mathNow.charAt(mathNow.length() - 1) == '('
                    || mathNow.charAt(mathNow.length() - 1) == 'π'
                    || mathNow.charAt(mathNow.length() - 1) == 'e')
                mathNow += "-";
        }
        tvNow.setText(mathNow);
        equal_flag = 0;
    }
});

响应场景设置:

  1. mathNow 长度为0,添加“-”,表示正数
  2. 同 btn_add
3. btn_mul ×
btnMul.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mathNow.length() != 0) {
            if (isNum(mathNow.charAt(mathNow.length() - 1))
                    || mathNow.charAt(mathNow.length() - 1) == ')'
                    || mathNow.charAt(mathNow.length() - 1) == 'π'
                    || mathNow.charAt(mathNow.length() - 1) == 'e')
                mathNow += "×";
        }
        tvNow.setText(mathNow);
        equal_flag = 0;
    }
});

响应场景设置:

  1. × 不能出现在math表达式的首位,所以场景限制在mathNow长度不为0
  2. 以下4种场景都可以添加 “×”,设 char 是 mathNow 的最后一个 char:
    • char是Num
    • char是“)”
    • char是“π”
    • char是“e”,自然指数
4. btn_div /

响应场景设置同 btn_mul

btnDiv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mathNow.length() != 0) {
            if (isNum(mathNow.charAt(mathNow.length() - 1))
                    || mathNow.charAt(mathNow.length() - 1) == ')'
                    || mathNow.charAt(mathNow.length() - 1) == 'π'
                    || mathNow.charAt(mathNow.length() - 1) == 'e')
                mathNow += "/";
        }
        tvNow.setText(mathNow);
        equal_flag = 0;
    }
});
5. btn_bracket ( )
btnBracket.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (equal_flag == 1) {
            mathNow = "";
            equal_flag = 0;
        }
        if (mathNow.length() == 0) {                                //1.mathNow为空,+(
            mathNow += "(";
        } else if (isOper(mathNow.charAt(mathNow.length() - 1))) {  //2.mathNow最后一个字符是oper,+(
            mathNow += "(";
        } else if (isNum(mathNow.charAt(mathNow.length() - 1))      //3.mathNow最后一个字符是num, π, e
                || mathNow.charAt(mathNow.length() - 1) == 'π'
                || mathNow.charAt(mathNow.length() - 1) == 'e') {
            if (!hasLeftBracket(mathNow))                               //3.1 没有(, 加 ×(
                mathNow += "×(";
            else                                                        //3.2 已有(, 加 )
                mathNow += ")";
        } else if (mathNow.charAt(mathNow.length() - 1) == ')') {   //4.mathNow最后一个字符是),说明用户是在补全右括号,+)
            mathNow += ')';
        }
        tvNow.setText(mathNow);
    }
});

响应场景设置

  1. equal_flag同btn_0;
  2. mathNow长度为0,+“(”
  3. mathNow最后一个字符是oper,+“(”
  4. mathNow最后一个字符是num, π, e
    • 如果mathNow没有“(”, 加“×(”
    • 如果mathNow已有“(”, 加“(”
  5. mathNow最后一个字符是“)”,说明用户是在补全右括号,+“)”
6. btn_equal =
btnEqual.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        //右括号自动补全
        int leftNum = 0;
        int rightNum = 0;
        for (int i = 0; i < mathNow.length(); i++) {
            if (mathNow.charAt(i) == '(')
                leftNum++;
            if (mathNow.charAt(i) == ')')
                rightNum++;
        }
        int missingNum = leftNum - rightNum; //缺失的 ) 数量
        while (missingNum > 0) {
            mathNow += ')';
            missingNum--;
        }
        tvNow.setText(mathNow);

        mathPast = "\n" + mathNow; //使得呈现的mathPast自动换行

        double result = scienceCalculator.cal(mathNow, precision, angle_metric); //调用科学计算器
        if (result == Double.MAX_VALUE)
            mathNow = "Math Error";
        else {
            mathNow = String.valueOf(result);
            System.out.println(mathNow);
            if (mathNow.charAt(mathNow.length() - 2) == '.' && mathNow.charAt(mathNow.length() - 1) == '0') {
                mathNow = mathNow.substring(0, mathNow.length() - 2);
            }
        }

        mathPast = mathPast + "=" + mathNow;

        //用tvPast.set(mathPast)不能实现自动滚动到最新运算过程
        tvPast.append(mathPast); //添加新的运算过程

        //tvPast滚动到最新的运算过程
        int offset = tvPast.getLineCount() * tvPast.getLineHeight();
        if (offset > tvPast.getHeight()) {
            tvPast.scrollTo(0, offset - tvPast.getHeight());
        }
        tvNow.setText(mathNow);

        equal_flag = 1; //设置flag=1
    }
});
  1. 右括号自动补全,通过计算 mathNow 中 “(” 和 “)” 个数的差值,添加右括号,补全当前的 mathNow
//右括号自动补全
int leftNum = 0;
int rightNum = 0;
for (int i = 0; i < mathNow.length(); i++) {
    if (mathNow.charAt(i) == '(')
        leftNum++;
    if (mathNow.charAt(i) == ')')
        rightNum++;
}
int missingNum = leftNum - rightNum; //缺失的 ) 数量
while (missingNum > 0) {
    mathNow += ')';
    missingNum--;
}
tvNow.setText(mathNow);

mathPast = "\n" + mathNow; //使得呈现的mathPast自动换行
  1. mathNow 预处理后进行计算,调用 ScienceCalculator 的 cal 方法计算,并根据返回值情况设定 mathNow 的结果显示为 Math Error 或者正常结果。
double result = scienceCalculator.cal(mathNow, precision, angle_metric); //调用科学计算器
if (result == Double.MAX_VALUE)
    mathNow = "Math Error";
else {
    mathNow = String.valueOf(result);
    System.out.println(mathNow);
    if (mathNow.charAt(mathNow.length() - 2) == '.' && mathNow.charAt(mathNow.length() - 1) == '0') {
        mathNow = mathNow.substring(0, mathNow.length() - 2);
    }
}
  1. tvPast 添加新的 mathPast 到文本框
mathPast = mathPast + "=" + mathNow;
//用tvPast.set(mathPast)不能实现自动滚动到最新运算过程
tvPast.append(mathPast); //添加新的运算过程
  1. 获取 tvPast 文本框属性并滚动到最新的一行
//tvPast滚动到最新的运算过程
int offset = tvPast.getLineCount() * tvPast.getLineHeight();
if (offset > tvPast.getHeight()) {
    tvPast.scrollTo(0, offset - tvPast.getHeight());
}
tvNow.setText(mathNow);
  1. equal_flag设为1
equal_flag = 1; //设置flag=1

3.2.4 初始化 ScienceOperButtons

除了x2,xy,其他 ScienceOpers 都要设置 equal_flag,同btn_0。

1. btn_sin
btnSin.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (equal_flag == 1) {
            mathNow = "";
            equal_flag = 0;
        }
        if (mathNow.length() == 0) {
            mathNow += "sin(";
        } else {
            //oper, (, 加 sin(
            char ch = mathNow.charAt(mathNow.length() - 1);
            if (isOper(ch) || ch == '(')
                mathNow += "sin(";
        }
        tvNow.setText(mathNow);
    }
});

响应场景设置:

  1. mathNow 长度为0,添加“sin(”
  2. mathNow 最后一个 char 是 base opers,(,添加“sin(”
2. btn_cos
btnCos.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (equal_flag == 1) {
            mathNow = "";
            equal_flag = 0;
        }
        if (mathNow.length() == 0) {
            mathNow += "cos(";
        } else {
            char ch = mathNow.charAt(mathNow.length() - 1);
            if (isOper(ch) || ch == '(')
                mathNow += "cos(";
        }
        tvNow.setText(mathNow);
    }

除了 x2,xy,其他 ScienceOper 的场景都和 btn_sin 相同

3. btnX2
btnX2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        //要求mathNow不为空并且最后一个字符:num, ), e, π
        if (mathNow.length() > 0) {
            char ch = mathNow.charAt(mathNow.length() - 1);
            if (isNum(ch) || ch == ')' || ch == 'e' || ch == 'π')
                mathNow += "^2";
        }
        tvNow.setText(mathNow);
    }
});

响应场景设置:

  1. mathNow 不为空,并且最后一个字符是:Num,),e,π

5. btnXy

btnXy.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        //条件同btnX2
        if (mathNow.length() > 0) {
            char ch = mathNow.charAt(mathNow.length() - 1);
            if (isNum(ch) || ch == ')' || ch == 'e' || ch == 'π')
                mathNow += "^(";
        }
        tvNow.setText(mathNow);
    }
});

响应事件场景同 btnX2。

3.2.5 初始化 tvDeg,tvRad

用法:点击 Deg 之后,angle_metric 设置为 DEG,角度制,界面上 DEG 变为蓝色,RAD 变为灰色,RAD 同样是这样。

public void initDegRad() {
    tvDeg.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            tvDeg.setTextColor(Color.parseColor("#3FA2F0"));
            tvRad.setTextColor(Color.parseColor("#AAAAAA"));
            angle_metric = DEG;
        }
    });

    tvRad.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            tvRad.setTextColor(Color.parseColor("#3FA2F0"));
            tvDeg.setTextColor(Color.parseColor("#AAAAAA"));
            angle_metric = RAD;
        }
    });
}

3.2.6 初始化精度选择器

属性设置:

  1. 设置精度最大为12位,最小为0位,默认设置值为6
  2. NumberPicker 监听事件将用户选择的精度值传给成员变量 precision
//初始化精度选择器
public void initPrecisionPicker() {
    precisionPicker.setMaxValue(12); //最多保留12位
    precisionPicker.setMinValue(0);
    precisionPicker.setValue(6);
    precisionPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
        @Override
        public void onValueChange(NumberPicker numberPicker, int oldVal, int newVal) {
            precision = newVal;
        }
    });
}

3.2.7 初始化功能 Button

包括 btn_save,btn_copy,btn_clear

1. btn_save 保存
//保存
btnSave.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        //保存文件到sd卡 manifest文件中也要添加2个permission
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String path = Environment.getExternalStorageDirectory().getPath() + "/math.txt"; //设置保存路径和文件名
            try {
                FileOutputStream outputStream = new FileOutputStream(path);
                outputStream.write(tvPast.getText().toString().getBytes()); //写字节
                outputStream.close(); //关闭输出流
            } catch (Exception e) {
                e.printStackTrace();
            }
            Toast.makeText(LandActivity.this, "保存到" + path, Toast.LENGTH_SHORT).show();
        }
    }
});

通过字节流将 tvPast 的内容写道 storage/emulated/0/maht.txt 文件中

2. btn_copy 复制
//复制
btnCopy.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); //采用ClipboardManager类
        cm.setText(tvPast.getText());
        Toast.makeText(LandActivity.this, "已复制到剪切板", Toast.LENGTH_SHORT).show();
    }
});

调用 ClipboardManager 类 setText 方法复制 tvPast 文本框中过去的运算过程。

3. btn_clear 清空
//清空
btnClear.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        mathPast = "";
        tvPast.setText(mathPast);
        Toast.makeText(LandActivity.this, "计算过程已经清空", Toast.LENGTH_SHORT).show();
    }
});

很好实现,将 tvPast 的内容置为空即可。

3.2.8 初始化计算器基本Buttons

包括 btn_del,btn_clc

1. btn_del 退格
btnDel.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (mathNow.length() != 0) {
            mathNow = mathNow.substring(0, mathNow.length() - 1);
            tvNow.setText(mathNow);
        }
    }
});

截取掉mathNow的最后一个char即可

2. btn_clc 清空mathNow
btnClc.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        mathNow = "";
        tvNow.setText(mathNow);
    }
});

mathNow = “” 即可

3.3 ScienceCalculator接口

3.3.1 预处理 math

去掉 math 中的空格,替换 π,替换自然指数 e

//(1)预处理math
math = math.replace(" ", "");   //去掉math中的所有空格
math = math.replace("π", String.valueOf(Math.PI));   //替换π
math = math.replace("e", String.valueOf(Math.exp(1))); //替换自然指数e

3.3.2 pow 运算,包含 ^ 的 ScienceOpers

获取 ^ 左右两边参数进行 Math.pow 计算,如果参数是 Math 表达式,需要调用 BaseCalculator 方法,然后用运算结果替换科学运算式部分

//(2)计算指数(pow)运算并替换,包括(x)^(y)
while (math.contains("^")) {

    //1.中间寻找的点
    int midIndex = math.lastIndexOf("^");

    //2.获取左边参数
    double leftNum; //左边的数
    String leftStr; //左边math字符串
    int leftIndex = midIndex - 1;

    if (math.charAt(leftIndex) == ')') {        //1.左边是一个表达式,即左边用括号括起来
        int i = leftIndex - 1;
        while (math.charAt(i) != '(') {
            i--;
        }
        String subLeftMath = math.substring(i + 1, leftIndex);
        leftNum = baseCalculator.cal(subLeftMath);
        if (leftNum == Double.MAX_VALUE) //每次计算要判断是否出现 math error
            return Double.MAX_VALUE;

        leftStr = "(" + subLeftMath + ")";
    } else {                                    //2.左边是一个数

        //注意:判定index范围一定要在左边,否则可能出现IndexOutOfRange异常
        while (leftIndex >= 0 && !isOper(math.charAt(leftIndex))) {
            leftIndex--;
        }
        leftStr = math.substring(leftIndex + 1, midIndex);
        leftNum = Double.parseDouble(leftStr);
    }

    //3.获取右边参数
    double rightNum;
    String rightStr;
    int rightIndex = midIndex + 1;

    if (math.charAt(rightIndex) == '(') {
        int i = rightIndex + 1;
        while (math.charAt(i) != ')') {
            i++;
        }
        String subRightMath = math.substring(rightIndex + 1, i);
        rightNum = baseCalculator.cal(subRightMath);
        if (rightNum == Double.MAX_VALUE)
            return Double.MAX_VALUE;
        rightStr = "(" + subRightMath + ")";
    } else {
        while (rightIndex < math.length() && !isOper(math.charAt(rightIndex))) {
            rightIndex++;
        }
        rightStr = math.substring(midIndex + 1, rightIndex);
        rightNum = Double.parseDouble(rightStr);
    }

    //4.得到完整的运算式并运算和替换
    String wholeMath = leftStr + "^" + rightStr;
    double result = Math.pow(leftNum, rightNum);
    math = math.replace(wholeMath, String.valueOf(result));
}

3.3.3 计算剩下的科学运算

包括:sin,cos,tan,ln,log,√

通过获取括号位置,如 sin(cos(90°)),先获取 cos(90°) 完成计算,再用 Math.sin 计算,根据 angle_metric 的情况选择 DEG 或者 RAD。

//(3)计算其他的科学运算符
while (math.contains("sin")
        || math.contains("cos")
        || math.contains("tan")
        || math.contains("ln")
        || math.contains("log")
        || math.contains("√")) {

    //1.获取()内运算式并计算出结果,此时假设()不再包含复杂的科学运算
    int beginIndex = math.lastIndexOf("(");
    int endIndex = getRightBracket(math, beginIndex);
    String subMath = math.substring(beginIndex + 1, endIndex);
    double subResult = baseCalculator.cal(subMath);
    if (subResult == Double.MAX_VALUE) //每次计算要判断是否出现 math error
        return Double.MAX_VALUE;

    //2.获取scienceOper字符串
    int i = beginIndex - 1;
    while (i >= 0 && !isOper(math.charAt(i))) { //向左寻找
        i--;
    }
    String scienceOper = math.substring(i + 1, beginIndex);

    //3.匹配scienceOper进行科学运算,并替换相应部分
    String tempMath;
    double tempResult;
    int DEG = 0; //判断角度制
    switch (scienceOper) {
        case "sin":
            tempMath = "sin(" + subMath + ")";
            if (angle_metric == DEG) {
                tempResult = Math.sin(subResult / 180 * Math.PI); //将默认的 Rad → Deg
            } else {
                tempResult = Math.sin(subResult);
            }
            math = math.replace(tempMath, String.valueOf(tempResult));
            break;
        case "cos":
            tempMath = "cos(" + subMath + ")";
            if (angle_metric == DEG) {
                tempResult = Math.cos(subResult / 180 * Math.PI);
            } else {
                tempResult = Math.cos(subResult);
            }
            math = math.replace(tempMath, String.valueOf(tempResult));
            break;
        case "tan":
            tempMath = "tan(" + subMath + ")";
            if (angle_metric == DEG) {
                tempResult = Math.tan(subResult / 180 * Math.PI);
            } else {
                tempResult = Math.tan(subResult);
            }
            math = math.replace(tempMath, String.valueOf(tempResult));
            break;
        case "ln":
            tempMath = "ln(" + subMath + ")";
            tempResult = Math.log(subResult);
            math = math.replace(tempMath, String.valueOf(tempResult));
            break;
        case "log":
            tempMath = "log(" + subMath + ")";
            tempResult = Math.log10(subResult);
            math = math.replace(tempMath, String.valueOf(tempResult));
            break;
        case "√":
            tempMath = "√(" + subMath + ")";
            tempResult = Math.sqrt(subResult);
            math = math.replace(tempMath, String.valueOf(tempResult));
            break;
        default:
            break;
    }
}

3.3.4 BaseCalculaor 运算并格式化 result

采用 BigDecimal 类四舍五入保留小数位数

//(4)此时的math已经替换到BaseCalculator可处理的形式
if (baseCalculator.cal(math) == Double.MAX_VALUE)
    return Double.MAX_VALUE;
else {
    BigDecimal b = new BigDecimal(baseCalculator.cal(math));
    return b.setScale(precision, BigDecimal.ROUND_HALF_UP).doubleValue(); //四舍五入保留相应位数小数
}

3.4 BaseCalculator 接口

主要是栈实现四则运算,采用了逆波兰式和运算符优先级表。

3.4.1 operSet 和 operMap

用 Map 是为了方便取运算符下标

private final char[] operSet = {'+', '-', '×', '/', '(', ')', '#'};

//Map结构方便后面取运算符的下标
private final Map operMap = new HashMap() {{
    put('+', 0);
    put('-', 1);
    put('×', 2);
    put('/', 3);
    put('(', 4);
    put(')', 5);
    put('#', 6);
}};

3.4.2 operPrior 运算符优先级表

//运算符优先级表,operPrior[oper1下标][oper2下标]
private final char[][] operPrior = {
   /* (o1,o2)  +    -    ×    /    (    )    # */
   /*  +  */ {'>', '>', '<', '<', '<', '>', '>'},
   /*  -  */ {'>', '>', '<', '<', '<', '>', '>'},
   /*  ×  */ {'>', '>', '>', '>', '<', '>', '>'},
   /*  /  */ {'>', '>', '>', '>', '<', '>', '>'},
   /*  (  */ {'<', '<', '<', '<', '<', '=', ' '},
   /*  )  */ {'>', '>', '>', '>', ' ', '>', '>'},
   /*  #  */ {'<', '<', '<', '<', '<', ' ', '='},
};

通过 getPrior 方法获取2个运算符优先级比较的结果

//返回2个运算符优先级比较的结果'<','=','>'
private char getPrior(char oper1, char oper2) {
    return operPrior[operMap.get(oper1)][operMap.get(oper2)]; //Map.get方法获取运算符的下标
}

3.4.3 栈实现四则运算

遍历 math 表达式,num 入 numStack 栈,oper 入 operStack 栈,oper 在入栈时比较其与当前栈顶 oper 的优先级:

  1. “<”:栈顶 oper 优先级低,新 oper 入栈
  2. “=”:说明要入栈的 oper 为 “)”,而栈顶 oper 为 “(”,去掉 “(”,其实也是 math 去括号的过程
  3. “>”: 栈顶 oper 优先级高,oper 出栈,并将 num 运算结果 push 进 numStack
    直到最后numStack的栈顶元素为计算结果。

在运算过程中涉及了负数的处理,即不将负数的 “-” 视为oper。

private double calSubmath(String math) {
    if (math.length() == 0) {
        return Double.MAX_VALUE;
    } else {
        if (!hasOper(math.substring(1, math.length())) || math.contains("E-")) {
            return Double.parseDouble(math);
        }

        //设置flag用于存储math开始位置的负数,如-3-5中的-3,避免-被识别成运算符而出错
        int flag = 0;
        if (math.charAt(0) == '-') {
            flag = 1;
            math = math.substring(1, math.length());
        }

        Stack operStack = new Stack<>(); //oper栈
        Stack numStack = new Stack<>();     //num栈

        operStack.push('#'); //设置栈底元素
        math += "#";

        String tempNum = ""; //暂存数字str

        //计算math
        for (int i = 0; i < math.length(); i++) {

            char charOfMath = math.charAt(i); //遍历math中的char

            //(1)num进栈
            if (!isOper(charOfMath)         //1.不是oper
             || charOfMath == '-' && math.charAt(i - 1) == '(') {              //2.是'-'并且'-'左边有'(',说明是在math中间用负数
                tempNum += charOfMath;

                //1.1 获取下一个char
                i++;
                charOfMath = math.charAt(i);

                //1.2 判断下一个char是不是oper,如果是oper,就将num压入numStack
                if (isOper(charOfMath)) {   //此条件成功时,下次for循环就直接跳到else语句了
                    double num = Double.parseDouble(tempNum);
                    if (flag == 1) {        //恢复math首位的负数
                        num = -num;
                        flag = 0;
                    }
                    numStack.push(num); //push num
                    tempNum = ""; //重置tempNum
                }

                //1.3 //回退,以免下次循环for语句自身的i++使得跳过了这个char
                i--;
            }

            //(2)oper进栈
            else {

                switch (getPrior(operStack.peek(), charOfMath)) {

                    //2.1 栈顶oper优先级低,新oper入栈
                    case '<':
                        operStack.push(charOfMath);
                        break;

                    //2.2 说明当前的charOfMath为')',而栈顶oper为'(',去掉'(',其实也是math去括号的过程
                    case '=':
                        operStack.pop();
                        break;

                    //2.3 栈顶oper优先级高,oper出栈,并将num运算结果push进numStack
                    case '>':
                        char oper = operStack.pop();
                        double b = numStack.pop();
                        double a = numStack.pop();
                        if (operate(a, oper, b) == Double.MAX_VALUE)
                            return Double.MAX_VALUE;
                        numStack.push(operate(a, oper, b));
                        i--; //继续比较该oper与栈顶oper的关系
                        break;
                }
            }
        }
        return numStack.peek(); //最后的math变成一个num了
    }
}

//计算math,添加了一些特殊math的处理
double cal(String math) {
    if (math.length() == 0) { //处理异常
        return Double.MAX_VALUE;
    } else {
        //运算式只是数字的特征:从第2个char开始math中没有oper
        if (!hasOper(math.substring(1, math.length())) || math.contains("E-")) {
            return Double.parseDouble(math);
        }
        //普通运算
        else {
            return calSubmath(math);
        }
    }
}

四、操作流程

4.1 操作流程图

Java 课程设计 - Android 计算器_第5张图片
图4.1 计算器操作流程图

4.2 操作流程步骤

  1. 程序开始;
  2. 在手机上点击计算器APP,进入默认的计算器竖屏界面,通过点击按钮输入math表达式,按钮设置了响应事件的场景,避免了一些math 表达式的格式错误,最后完成math 表达式的输入;
  3. 点击 = 按钮进行计算,如果运算过程中出现除以0的情况或者格式错误的math表达式,输出Math Error,正常情况下完成math计算,输出计算结果;
  4. 此时用户有5个选择:
    • 继续输入math表达式计算
    • 点击保存按钮将文本区中的全部计算过程保存到文件
    • 点击复制按钮将文本区中选中的文本复制到剪贴本
    • 点击清除按钮将文本区的全部内容清除
    • 点击系统返回键退出计算器
  5. 用户在完成(3)中的1,2,3,4任意一个之后均可以点击系统返回键退出计算器;
  6. 用户将手机横屏,App切换到科学计算器的界面,同样完成(1),(2),(3),(4)操作;
  7. 程序结束。

五、测试

5.1 弧度角度运算

Java 课程设计 - Android 计算器_第6张图片
Java 课程设计 - Android 计算器_第7张图片

5.2 数学表达式

Java 课程设计 - Android 计算器_第8张图片

5.3 包含科学计算的数学表达式

Java 课程设计 - Android 计算器_第9张图片

5.4 保留相应小数位数

Java 课程设计 - Android 计算器_第10张图片

5.5 处理异常

Java 课程设计 - Android 计算器_第11张图片

5.6 保存运算过程到文件

Java 课程设计 - Android 计算器_第12张图片
Java 课程设计 - Android 计算器_第13张图片
math.txt 文件

六、实验心得

本次实验不经锻炼了我编写Java程序的能力,而且使我对Android系统App设计有了更深的认识。

用 Java 做计算器,主要是处理 String 类型的 math 表达式,灵活运用 String 的方法,通过截取原始的 math 分治结果问题:

  1. 先预处理 math,去掉影响计算的空格等
  2. 再替换 π,e
  3. 再计算科学运算式
  4. 最后把 math 替换成 BaseCalculator 即可计算的类型
  5. 再利用栈实现四则运算的方法计算出最终结果

对于Android程序设计,我学会了以下几点:

  1. Android横竖屏切换
  2. 保存文件到手机本地
  3. 灵活运用layout布局设计App界面,掌握了基本的自适应
  4. 自定义控件如NumberPicker,Button边框等,会设计圆形的Button按钮
  5. 通过butterknife设置BindView方便初始化控件

总的来说,本次实验我收获很多,基本上理解了编写一个 Java 应用的基本架构,先编写好接口,再设计界面,最后把响应事件与接口联系起来,做成一个体验很好的计算器。

但我也认识到计算器面临的 math 表达式的类型有很多,在 NumButtons 和 OperButtons 中添加的响应场景可能还不完善,为此,我把项目传上了GitHub,希望开源之后,大家可以更好地改进我的计算器。

你可能感兴趣的:(Java 课程设计 - Android 计算器)