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。

二、程序结构

图2.1 程序结构流程图
图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,实例化一个科学计算器
图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
图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<Character, Integer> operMap = new HashMap<Character, Integer>() {{
    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<Character> operStack = new Stack<>(); //oper栈
        Stack<Double> 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 操作流程图

图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 弧度角度运算

5.2 数学表达式

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

5.4 保留相应小数位数

5.5 处理异常

5.6 保存运算过程到文件

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,希望开源之后,大家可以更好地改进我的计算器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,652评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,625评论 18 399
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,965评论 6 13
  • robotium的一个缺点就是不能跨应用,但是现在的应用几乎都会有分享的功能,要不就是第三方登录;还有就是拍照这种...
    tyoko阅读 1,328评论 0 1
  • 2016年9月,工作日的午后醒来,看着电脑屏幕有些刺眼,听着音乐眯了一会,换歌。看了下情况,没有工作要处理,继续迷...
    此_时_彼_刻阅读 905评论 0 0