Android 計算器 - Shuai-Xie - Github
一、設(shè)計分析
1.1 題目重述
本次實驗為了實現(xiàn)一個保存計算過程的計算器,主要有以下三個要求:
- 仿照真實的計算器實現(xiàn)其功能。
- 在左上方的文本框中顯示當(dāng)前的計算過程,在右邊的文本區(qū)中顯示以往的計算過程。
- 單擊“保存”按鈕可以將文本區(qū)中的全部計算過程保存到文件;單擊“復(fù)制”按鈕可以將文本區(qū)中選中的文本復(fù)制到剪貼本;單擊“清除”按鈕可以清除文本區(qū)的全部內(nèi)容。
1.2 設(shè)計思路
考慮到計算器的美觀和易用性,我決定把計算器做在 Android 端,Android 系統(tǒng)的 App 的底層功能由 Java 實現(xiàn),所以工作主要分為兩部分:
設(shè)計計算器界面 (activity_main.xml)
計算器界面用xml文件編寫,包括手機豎屏和橫屏兩個布局文件:
豎屏:activity_main.xml 布局為 portrait
橫屏:activity_main_land.xml 布局為 landscape
豎屏模式可以完成基本的四則運算,不涉及科學(xué)計算
橫屏模式除了完成基本的四則運算,還添加了科學(xué)運算編寫計算接口 (ScienceCalculator.java)
ScienceCalculator 可以完成包含科學(xué)運算函數(shù)的 math,先實現(xiàn)可以完成基本四則運算的 BaseCalculator,在此基礎(chǔ)上,實現(xiàn) ScienceCalculator。
運算的思路是先通過 ScienceCalculator 完成math中需要科學(xué)計算函數(shù)的部分,再用這些部分計算的結(jié)果替換原 math 中的這些部分,使包含科學(xué)計算函數(shù)的 math 轉(zhuǎn)變成可用 BaseCalculator 計算的 math。
二、程序結(jié)構(gòu)


三、各模塊的功能及程序說明
3.1 計算器界面設(shè)計
3.1.1 豎屏界面
包含控件
- 文本框 TextView:tvNowt,vPast 分別顯示當(dāng)前和過去的運算過程;
- 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,復(fù)制,清空tvPast中的運算過程;
- 數(shù)字 Button:0-9,小數(shù)點
- 運算符 Button:+ - × / ( ) =
- 運算器基本 Button:btn_del 退格,btn_clc 清空當(dāng)前math
成員變量
- String mathPast,用于存儲過去的運算過程
- String mathNow,用于存儲當(dāng)前的運算過程,即用戶正在輸入的部分
- int precision,設(shè)置默認精度為6位小數(shù)
- int equal_flag,設(shè)置flag值判斷是否需要清空mathNow進行新的運算
- ScienceCalculator scienceCalculator,實例化一個科學(xué)計算器

3.1.2 橫屏界面
包含控件
- 文本框 TextView:tvNow, tvPast 分別顯示當(dāng)前和過去的運算過程;
- 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,復(fù)制,清空tvPast中的運算過程;
- 數(shù)字Button:0-9,小數(shù)點
- 基本運算符Button:+ - × / ( ) =
- 科學(xué)運算符Button:(12個)
sin,cos,tan,√x,e,π,1/x,ln,log,x2,ex,xy - 運算器基本Button:btn_del退格,btn_clc清空當(dāng)前math
- 文本框切換按鈕 tvRad,tvDeg 實現(xiàn)弧度制和角度值的切換
- 精度選擇器 NumberPicker
成員常量
- final int DEG = 0,DEG 表示角度制
- final int RAD = 1,RAD 表示弧度制
成員變量
- String mathPast,用于存儲過去的運算過程
- String mathNow,用于存儲當(dāng)前的運算過程,即用戶正在輸入的部分
- int precision,設(shè)置默認精度為6位小數(shù),通過NumberPicker返回用戶設(shè)置的精度值
- int equal_flag,設(shè)置flag值判斷是否需要清空mathNow進行新的運算
- ScienceCalculator scienceCalculator,實例化一個科學(xué)計算器
- int angle_metric,角度制參數(shù),默認為DEG

3.2 界面各模塊功能
由于橫評界面包括了豎屏界面所有的模塊,下文代碼功能描述按照 LandActivity.java 文件,即橫評界面對應(yīng)的 Activity。
3.2.1 初始化 tvPast
tvPast 用于存儲過去的運算過程
public void initTvPast() {
//設(shè)置tvPast一些屬性
tvPast.setMovementMethod(ScrollingMovementMethod.getInstance()); //內(nèi)容自動滾動到最新的一行
tvPast.setTextIsSelectable(true); //長按復(fù)制
//獲取界面切換的tvPast的內(nèi)容
Intent intent = this.getIntent();
String tvPastContent = intent.getStringExtra("main");
//如果當(dāng)前的界面是啟動界面,不是從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不用加換行
}
}
響應(yīng)場景設(shè)置:
- 因為tvPast文本框高度有限,為了使用戶每次都可以看到最新的運算過程,設(shè)置 setMovementMethod(ScrollingMovementMethod.getInstance()) 方法使內(nèi)容自動滾動到最新的一行;
tvPast.setMovementMethod(ScrollingMovementMethod.getInstance());
- Android系統(tǒng)集成了很好的文本框內(nèi)容復(fù)制功能,設(shè)置 setTextIsSelectable(true) 即可實現(xiàn)文本框的長按復(fù)制功能;
tvPast.setTextIsSelectable(true);
- 由于計算器具有2個界面,當(dāng)前的界面可能是從豎屏界面切換來(如果當(dāng)前界面是豎屏,界面也有可能是從橫屏界面切換而來),通過Intent類在兩個Activity間傳遞tvPast的內(nèi)容,至于用for循環(huán)逐行添加過去的運算過程是為了滿足(1)使內(nèi)容自動滾動到最新的一行。
//獲取界面切換的tvPast的內(nèi)容
Intent intent = this.getIntent();
String tvPastContent = intent.getStringExtra("main");
//如果當(dāng)前的界面是啟動界面,不是從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,小數(shù)點
按鈕需要設(shè)置監(jiān)聽事件的應(yīng)用場景,是為了避免一些錯誤的math格式。因為不同的數(shù)字有不同的處理方式。主要歸為以下幾類:
1. btn_0
btn_0 根據(jù)響應(yīng)事件場景在當(dāng)前 math 表達式中添加 0
btn0.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//如果flag=1,表示要輸入新的運算式,清空mathNow并設(shè)置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中間出現(xiàn) 比如:×0 +0
} else { //4.除此之外,+0
mathNow += "0";
}
tvNow.setText(mathNow);
}
});
響應(yīng)場景設(shè)置:
設(shè)置 flag 值判斷是否需要清空 mathNow 進行新的運算,該功能是為了方便用戶的輸入,用戶在完成一次計算之后,不需要點擊清空按鈕就可以直接輸入新的運算過程,當(dāng) equal_flag 為1時表示剛剛完成一次運算,可以直接輸入新的運算式了,此時完成 mathNow 清空操作,并重置 equal_flag 為 0;
-
是否添加0的場景設(shè)置:
- mathNow 長度為0,添加0
- mathNow 長度為1,當(dāng)前輸入1個char了
如果當(dāng)前 char 為0,不添加0
如果當(dāng)前 char 為1-9,添加0 - mathNow 長度 >1,if中的條件是2.1的一般情況,即在 math 中間出現(xiàn)了,mathNow 的倒數(shù)第2個 char 不是 Num 并且 mathNow 的最后一個 char 是0,
如 2 + 3 ×0 ,此時也不添加0 - 除此之外,添加0
2. btn_[1-9]
btn_1 ~ btn_9 的響應(yīng)場景相同,根據(jù)響應(yīng)事件場景在當(dāng)前 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);
}
});
響應(yīng)場景設(shè)置:
- equal_flag 同 btn_0;
- mathNow 長度為0,添加[1-9];
- mathNow 最后一個 char 是 [0-9],oper,(,小數(shù)點 這4種情況時,+[1-9];
- 除此之外,不 +[1-9]
3. btn_dot 小數(shù)點
小數(shù)點操作要比普通數(shù)字要多一點,有時點擊添加的是“0.”
btn_dot 根據(jù)響應(yīng)事件場景在當(dāng)前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);
}
});
響應(yīng)場景設(shè)置:
- 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; //可能用運算結(jié)果直接運算,flag直接設(shè)0
}
});
響應(yīng)場景設(shè)置:
- mathNow長度為0,添加“+”,表示正數(shù)
- 以下5種場景都可以添加“+”,設(shè)char是mathNow的最后一個char:
- char是Num
- char是“)”
- char是“(”
- char是“π”
- char是“e”,自然指數(shù)
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;
}
});
響應(yīng)場景設(shè)置:
- mathNow 長度為0,添加“-”,表示正數(shù)
- 同 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;
}
});
響應(yīng)場景設(shè)置:
- × 不能出現(xiàn)在math表達式的首位,所以場景限制在mathNow長度不為0
- 以下4種場景都可以添加 “×”,設(shè) char 是 mathNow 的最后一個 char:
- char是Num
- char是“)”
- char是“π”
- char是“e”,自然指數(shù)
4. btn_div /
響應(yīng)場景設(shè)置同 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);
}
});
響應(yīng)場景設(shè)置
- 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; //缺失的 ) 數(shù)量
while (missingNum > 0) {
mathNow += ')';
missingNum--;
}
tvNow.setText(mathNow);
mathPast = "\n" + mathNow; //使得呈現(xiàn)的mathPast自動換行
double result = scienceCalculator.cal(mathNow, precision, angle_metric); //調(diào)用科學(xué)計算器
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)不能實現(xiàn)自動滾動到最新運算過程
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; //設(shè)置flag=1
}
});
- 右括號自動補全,通過計算 mathNow 中 “(” 和 “)” 個數(shù)的差值,添加右括號,補全當(dāng)前的 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; //缺失的 ) 數(shù)量
while (missingNum > 0) {
mathNow += ')';
missingNum--;
}
tvNow.setText(mathNow);
mathPast = "\n" + mathNow; //使得呈現(xiàn)的mathPast自動換行
- mathNow 預(yù)處理后進行計算,調(diào)用 ScienceCalculator 的 cal 方法計算,并根據(jù)返回值情況設(shè)定 mathNow 的結(jié)果顯示為 Math Error 或者正常結(jié)果。
double result = scienceCalculator.cal(mathNow, precision, angle_metric); //調(diào)用科學(xué)計算器
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)不能實現(xiàn)自動滾動到最新運算過程
tvPast.append(mathPast); //添加新的運算過程
- 獲取 tvPast 文本框?qū)傩圆L動到最新的一行
//tvPast滾動到最新的運算過程
int offset = tvPast.getLineCount() * tvPast.getLineHeight();
if (offset > tvPast.getHeight()) {
tvPast.scrollTo(0, offset - tvPast.getHeight());
}
tvNow.setText(mathNow);
- equal_flag設(shè)為1
equal_flag = 1; //設(shè)置flag=1
3.2.4 初始化 ScienceOperButtons
除了x2,xy,其他 ScienceOpers 都要設(shè)置 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);
}
});
響應(yīng)場景設(shè)置:
- 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);
}
});
響應(yīng)場景設(shè)置:
- 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);
}
});
響應(yīng)事件場景同 btnX2。
3.2.5 初始化 tvDeg,tvRad
用法:點擊 Deg 之后,angle_metric 設(shè)置為 DEG,角度制,界面上 DEG 變?yōu)樗{色,RAD 變?yōu)榛疑琑AD 同樣是這樣。
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 初始化精度選擇器
屬性設(shè)置:
- 設(shè)置精度最大為12位,最小為0位,默認設(shè)置值為6
- NumberPicker 監(jiān)聽事件將用戶選擇的精度值傳給成員變量 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"; //設(shè)置保存路徑和文件名
try {
FileOutputStream outputStream = new FileOutputStream(path);
outputStream.write(tvPast.getText().toString().getBytes()); //寫字節(jié)
outputStream.close(); //關(guān)閉輸出流
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(LandActivity.this, "保存到" + path, Toast.LENGTH_SHORT).show();
}
}
});
通過字節(jié)流將 tvPast 的內(nèi)容寫道 storage/emulated/0/maht.txt 文件中
2. btn_copy 復(fù)制
//復(fù)制
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, "已復(fù)制到剪切板", Toast.LENGTH_SHORT).show();
}
});
調(diào)用 ClipboardManager 類 setText 方法復(fù)制 tvPast 文本框中過去的運算過程。
3. btn_clear 清空
//清空
btnClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mathPast = "";
tvPast.setText(mathPast);
Toast.makeText(LandActivity.this, "計算過程已經(jīng)清空", Toast.LENGTH_SHORT).show();
}
});
很好實現(xiàn),將 tvPast 的內(nèi)容置為空即可。
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 預(yù)處理 math
去掉 math 中的空格,替換 π,替換自然指數(shù) e
//(1)預(yù)處理math
math = math.replace(" ", ""); //去掉math中的所有空格
math = math.replace("π", String.valueOf(Math.PI)); //替換π
math = math.replace("e", String.valueOf(Math.exp(1))); //替換自然指數(shù)e
3.3.2 pow 運算,包含 ^ 的 ScienceOpers
獲取 ^ 左右兩邊參數(shù)進行 Math.pow 計算,如果參數(shù)是 Math 表達式,需要調(diào)用 BaseCalculator 方法,然后用運算結(jié)果替換科學(xué)運算式部分
//(2)計算指數(shù)(pow)運算并替換,包括(x)^(y)
while (math.contains("^")) {
//1.中間尋找的點
int midIndex = math.lastIndexOf("^");
//2.獲取左邊參數(shù)
double leftNum; //左邊的數(shù)
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) //每次計算要判斷是否出現(xiàn) math error
return Double.MAX_VALUE;
leftStr = "(" + subLeftMath + ")";
} else { //2.左邊是一個數(shù)
//注意:判定index范圍一定要在左邊,否則可能出現(xiàn)IndexOutOfRange異常
while (leftIndex >= 0 && !isOper(math.charAt(leftIndex))) {
leftIndex--;
}
leftStr = math.substring(leftIndex + 1, midIndex);
leftNum = Double.parseDouble(leftStr);
}
//3.獲取右邊參數(shù)
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 計算剩下的科學(xué)運算
包括:sin,cos,tan,ln,log,√
通過獲取括號位置,如 sin(cos(90°)),先獲取 cos(90°) 完成計算,再用 Math.sin 計算,根據(jù) angle_metric 的情況選擇 DEG 或者 RAD。
//(3)計算其他的科學(xué)運算符
while (math.contains("sin")
|| math.contains("cos")
|| math.contains("tan")
|| math.contains("ln")
|| math.contains("log")
|| math.contains("√")) {
//1.獲取()內(nèi)運算式并計算出結(jié)果,此時假設(shè)()不再包含復(fù)雜的科學(xué)運算
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) //每次計算要判斷是否出現(xiàn) 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進行科學(xué)運算,并替換相應(yīng)部分
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 類四舍五入保留小數(shù)位數(shù)
//(4)此時的math已經(jīng)替換到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(); //四舍五入保留相應(yīng)位數(shù)小數(shù)
}
3.4 BaseCalculator 接口
主要是棧實現(xiàn)四則運算,采用了逆波蘭式和運算符優(yōu)先級表。
3.4.1 operSet 和 operMap
用 Map 是為了方便取運算符下標
private final char[] operSet = {'+', '-', '×', '/', '(', ')', '#'};
//Map結(jié)構(gòu)方便后面取運算符的下標
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 運算符優(yōu)先級表
//運算符優(yōu)先級表,operPrior[oper1下標][oper2下標]
private final char[][] operPrior = {
/* (o1,o2) + - × / ( ) # */
/* + */ {'>', '>', '<', '<', '<', '>', '>'},
/* - */ {'>', '>', '<', '<', '<', '>', '>'},
/* × */ {'>', '>', '>', '>', '<', '>', '>'},
/* / */ {'>', '>', '>', '>', '<', '>', '>'},
/* ( */ {'<', '<', '<', '<', '<', '=', ' '},
/* ) */ {'>', '>', '>', '>', ' ', '>', '>'},
/* # */ {'<', '<', '<', '<', '<', ' ', '='},
};
通過 getPrior 方法獲取2個運算符優(yōu)先級比較的結(jié)果
//返回2個運算符優(yōu)先級比較的結(jié)果'<','=','>'
private char getPrior(char oper1, char oper2) {
return operPrior[operMap.get(oper1)][operMap.get(oper2)]; //Map.get方法獲取運算符的下標
}
3.4.3 棧實現(xiàn)四則運算
遍歷 math 表達式,num 入 numStack 棧,oper 入 operStack 棧,oper 在入棧時比較其與當(dāng)前棧頂 oper 的優(yōu)先級:
- “<”:棧頂 oper 優(yōu)先級低,新 oper 入棧
- “=”:說明要入棧的 oper 為 “)”,而棧頂 oper 為 “(”,去掉 “(”,其實也是 math 去括號的過程
-
“>”: 棧頂 oper 優(yōu)先級高,oper 出棧,并將 num 運算結(jié)果 push 進 numStack
直到最后numStack的棧頂元素為計算結(jié)果。
在運算過程中涉及了負數(shù)的處理,即不將負數(shù)的 “-” 視為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);
}
//設(shè)置flag用于存儲math開始位置的負數(shù),如-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('#'); //設(shè)置棧底元素
math += "#";
String tempNum = ""; //暫存數(shù)字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中間用負數(shù)
tempNum += charOfMath;
//1.1 獲取下一個char
i++;
charOfMath = math.charAt(i);
//1.2 判斷下一個char是不是oper,如果是oper,就將num壓入numStack
if (isOper(charOfMath)) { //此條件成功時,下次for循環(huán)就直接跳到else語句了
double num = Double.parseDouble(tempNum);
if (flag == 1) { //恢復(fù)math首位的負數(shù)
num = -num;
flag = 0;
}
numStack.push(num); //push num
tempNum = ""; //重置tempNum
}
//1.3 //回退,以免下次循環(huán)for語句自身的i++使得跳過了這個char
i--;
}
//(2)oper進棧
else {
switch (getPrior(operStack.peek(), charOfMath)) {
//2.1 棧頂oper優(yōu)先級低,新oper入棧
case '<':
operStack.push(charOfMath);
break;
//2.2 說明當(dāng)前的charOfMath為')',而棧頂oper為'(',去掉'(',其實也是math去括號的過程
case '=':
operStack.pop();
break;
//2.3 棧頂oper優(yōu)先級高,oper出棧,并將num運算結(jié)果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--; //繼續(xù)比較該oper與棧頂oper的關(guān)系
break;
}
}
}
return numStack.peek(); //最后的math變成一個num了
}
}
//計算math,添加了一些特殊math的處理
double cal(String math) {
if (math.length() == 0) { //處理異常
return Double.MAX_VALUE;
} else {
//運算式只是數(shù)字的特征:從第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表達式,按鈕設(shè)置了響應(yīng)事件的場景,避免了一些math 表達式的格式錯誤,最后完成math 表達式的輸入;
- 點擊 = 按鈕進行計算,如果運算過程中出現(xiàn)除以0的情況或者格式錯誤的math表達式,輸出Math Error,正常情況下完成math計算,輸出計算結(jié)果;
- 此時用戶有5個選擇:
- 繼續(xù)輸入math表達式計算
- 點擊保存按鈕將文本區(qū)中的全部計算過程保存到文件
- 點擊復(fù)制按鈕將文本區(qū)中選中的文本復(fù)制到剪貼本
- 點擊清除按鈕將文本區(qū)的全部內(nèi)容清除
- 點擊系統(tǒng)返回鍵退出計算器
- 用戶在完成(3)中的1,2,3,4任意一個之后均可以點擊系統(tǒng)返回鍵退出計算器;
- 用戶將手機橫屏,App切換到科學(xué)計算器的界面,同樣完成(1),(2),(3),(4)操作;
- 程序結(jié)束。
五、測試
5.1 弧度角度運算


5.2 數(shù)學(xué)表達式

5.3 包含科學(xué)計算的數(shù)學(xué)表達式

5.4 保留相應(yīng)小數(shù)位數(shù)

5.5 處理異常

5.6 保存運算過程到文件


六、實驗心得
本次實驗不經(jīng)鍛煉了我編寫Java程序的能力,而且使我對Android系統(tǒng)App設(shè)計有了更深的認識。
用 Java 做計算器,主要是處理 String 類型的 math 表達式,靈活運用 String 的方法,通過截取原始的 math 分治結(jié)果問題:
- 先預(yù)處理 math,去掉影響計算的空格等
- 再替換 π,e
- 再計算科學(xué)運算式
- 最后把 math 替換成 BaseCalculator 即可計算的類型
- 再利用棧實現(xiàn)四則運算的方法計算出最終結(jié)果
對于Android程序設(shè)計,我學(xué)會了以下幾點:
- Android橫豎屏切換
- 保存文件到手機本地
- 靈活運用layout布局設(shè)計App界面,掌握了基本的自適應(yīng)
- 自定義控件如NumberPicker,Button邊框等,會設(shè)計圓形的Button按鈕
- 通過butterknife設(shè)置BindView方便初始化控件
總的來說,本次實驗我收獲很多,基本上理解了編寫一個 Java 應(yīng)用的基本架構(gòu),先編寫好接口,再設(shè)計界面,最后把響應(yīng)事件與接口聯(lián)系起來,做成一個體驗很好的計算器。
但我也認識到計算器面臨的 math 表達式的類型有很多,在 NumButtons 和 OperButtons 中添加的響應(yīng)場景可能還不完善,為此,我把項目傳上了GitHub,希望開源之后,大家可以更好地改進我的計算器。