Android 计算器 - Shuai-Xie - Github
一、设计分析
1.1 题目重述
本次实验为了实现一个保存计算过程的计算器,主要有以下三个要求:
- 仿照真实的计算器实现其功能。
- 在左上方的文本框中显示当前的计算过程,在右边的文本区中显示以往的计算过程。
- 单击“保存”按钮可以将文本区中的全部计算过程保存到文件;单击“复制”按钮可以将文本区中选中的文本复制到剪贴本;单击“清除”按钮可以清除文本区的全部内容。
1.2 设计思路
考虑到计算器的美观和易用性,我决定把计算器做在 Android 端,Android 系统的 App 的底层功能由 Java 实现,所以工作主要分为两部分:
设计计算器界面 (activity_main.xml)
计算器界面用xml文件编写,包括手机竖屏和横屏两个布局文件:
竖屏:activity_main.xml 布局为 portrait
横屏:activity_main_land.xml 布局为 landscape
竖屏模式可以完成基本的四则运算,不涉及科学计算
横屏模式除了完成基本的四则运算,还添加了科学运算编写计算接口 (ScienceCalculator.java)
ScienceCalculator 可以完成包含科学运算函数的 math,先实现可以完成基本四则运算的 BaseCalculator,在此基础上,实现 ScienceCalculator。
运算的思路是先通过 ScienceCalculator 完成math中需要科学计算函数的部分,再用这些部分计算的结果替换原 math 中的这些部分,使包含科学计算函数的 math 转变成可用 BaseCalculator 计算的 math。
二、程序结构
三、各模块的功能及程序说明
3.1 计算器界面设计
3.1.1 竖屏界面
包含控件
- 文本框 TextView:tvNowt,vPast 分别显示当前和过去的运算过程;
- 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,复制,清空tvPast中的运算过程;
- 数字 Button:0-9,小数点
- 运算符 Button:+ - × / ( ) =
- 运算器基本 Button:btn_del 退格,btn_clc 清空当前math
成员变量
- String mathPast,用于存储过去的运算过程
- String mathNow,用于存储当前的运算过程,即用户正在输入的部分
- int precision,设置默认精度为6位小数
- int equal_flag,设置flag值判断是否需要清空mathNow进行新的运算
- ScienceCalculator scienceCalculator,实例化一个科学计算器
3.1.2 横屏界面
包含控件
- 文本框 TextView:tvNow, tvPast 分别显示当前和过去的运算过程;
- 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,复制,清空tvPast中的运算过程;
- 数字Button:0-9,小数点
- 基本运算符Button:+ - × / ( ) =
- 科学运算符Button:(12个)
sin,cos,tan,√x,e,π,1/x,ln,log,x2,ex,xy - 运算器基本Button:btn_del退格,btn_clc清空当前math
- 文本框切换按钮 tvRad,tvDeg 实现弧度制和角度值的切换
- 精度选择器 NumberPicker
成员常量
- final int DEG = 0,DEG 表示角度制
- final int RAD = 1,RAD 表示弧度制
成员变量
- String mathPast,用于存储过去的运算过程
- String mathNow,用于存储当前的运算过程,即用户正在输入的部分
- int precision,设置默认精度为6位小数,通过NumberPicker返回用户设置的精度值
- int equal_flag,设置flag值判断是否需要清空mathNow进行新的运算
- ScienceCalculator scienceCalculator,实例化一个科学计算器
- int angle_metric,角度制参数,默认为DEG
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不用加换行
}
}
响应场景设置:
- 因为tvPast文本框高度有限,为了使用户每次都可以看到最新的运算过程,设置 setMovementMethod(ScrollingMovementMethod.getInstance()) 方法使内容自动滚动到最新的一行;
tvPast.setMovementMethod(ScrollingMovementMethod.getInstance());
- Android系统集成了很好的文本框内容复制功能,设置 setTextIsSelectable(true) 即可实现文本框的长按复制功能;
tvPast.setTextIsSelectable(true);
- 由于计算器具有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
}
});
响应场景设置:
- mathNow长度为0,添加“+”,表示正数
- 以下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;
}
});
响应场景设置:
- mathNow 长度为0,添加“-”,表示正数
- 同 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;
}
});
响应场景设置:
- × 不能出现在math表达式的首位,所以场景限制在mathNow长度不为0
- 以下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);
}
});
响应场景设置
- equal_flag同btn_0;
- mathNow长度为0,+“(”
- mathNow最后一个字符是oper,+“(”
- mathNow最后一个字符是num, π, e
- 如果mathNow没有“(”, 加“×(”
- 如果mathNow已有“(”, 加“(”
- 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
}
});
- 右括号自动补全,通过计算 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自动换行
- 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);
}
}
- tvPast 添加新的 mathPast 到文本框
mathPast = mathPast + "=" + mathNow;
//用tvPast.set(mathPast)不能实现自动滚动到最新运算过程
tvPast.append(mathPast); //添加新的运算过程
- 获取 tvPast 文本框属性并滚动到最新的一行
//tvPast滚动到最新的运算过程
int offset = tvPast.getLineCount() * tvPast.getLineHeight();
if (offset > tvPast.getHeight()) {
tvPast.scrollTo(0, offset - tvPast.getHeight());
}
tvNow.setText(mathNow);
- 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);
}
});
响应场景设置:
- mathNow 长度为0,添加“sin(”
- 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);
}
});
响应场景设置:
- 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 初始化精度选择器
属性设置:
- 设置精度最大为12位,最小为0位,默认设置值为6
- 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 的优先级:
- “<”:栈顶 oper 优先级低,新 oper 入栈
- “=”:说明要入栈的 oper 为 “)”,而栈顶 oper 为 “(”,去掉 “(”,其实也是 math 去括号的过程
- “>”: 栈顶 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 操作流程图
4.2 操作流程步骤
- 程序开始;
- 在手机上点击计算器APP,进入默认的计算器竖屏界面,通过点击按钮输入math表达式,按钮设置了响应事件的场景,避免了一些math 表达式的格式错误,最后完成math 表达式的输入;
- 点击 = 按钮进行计算,如果运算过程中出现除以0的情况或者格式错误的math表达式,输出Math Error,正常情况下完成math计算,输出计算结果;
- 此时用户有5个选择:
- 继续输入math表达式计算
- 点击保存按钮将文本区中的全部计算过程保存到文件
- 点击复制按钮将文本区中选中的文本复制到剪贴本
- 点击清除按钮将文本区的全部内容清除
- 点击系统返回键退出计算器
- 用户在完成(3)中的1,2,3,4任意一个之后均可以点击系统返回键退出计算器;
- 用户将手机横屏,App切换到科学计算器的界面,同样完成(1),(2),(3),(4)操作;
- 程序结束。
五、测试
5.1 弧度角度运算
5.2 数学表达式
5.3 包含科学计算的数学表达式
5.4 保留相应小数位数
5.5 处理异常
5.6 保存运算过程到文件
六、实验心得
本次实验不经锻炼了我编写Java程序的能力,而且使我对Android系统App设计有了更深的认识。
用 Java 做计算器,主要是处理 String 类型的 math 表达式,灵活运用 String 的方法,通过截取原始的 math 分治结果问题:
- 先预处理 math,去掉影响计算的空格等
- 再替换 π,e
- 再计算科学运算式
- 最后把 math 替换成 BaseCalculator 即可计算的类型
- 再利用栈实现四则运算的方法计算出最终结果
对于Android程序设计,我学会了以下几点:
- Android横竖屏切换
- 保存文件到手机本地
- 灵活运用layout布局设计App界面,掌握了基本的自适应
- 自定义控件如NumberPicker,Button边框等,会设计圆形的Button按钮
- 通过butterknife设置BindView方便初始化控件
总的来说,本次实验我收获很多,基本上理解了编写一个 Java 应用的基本架构,先编写好接口,再设计界面,最后把响应事件与接口联系起来,做成一个体验很好的计算器。
但我也认识到计算器面临的 math 表达式的类型有很多,在 NumButtons 和 OperButtons 中添加的响应场景可能还不完善,为此,我把项目传上了GitHub,希望开源之后,大家可以更好地改进我的计算器。