Java 課程設(shè)計 - Android 計算器

Android 計算器 - Shuai-Xie - Github

一、設(shè)計分析

1.1 題目重述

本次實驗為了實現(xiàn)一個保存計算過程的計算器,主要有以下三個要求:

  1. 仿照真實的計算器實現(xiàn)其功能。
  2. 在左上方的文本框中顯示當(dāng)前的計算過程,在右邊的文本區(qū)中顯示以往的計算過程。
  3. 單擊“保存”按鈕可以將文本區(qū)中的全部計算過程保存到文件;單擊“復(fù)制”按鈕可以將文本區(qū)中選中的文本復(fù)制到剪貼本;單擊“清除”按鈕可以清除文本區(qū)的全部內(nèi)容。

1.2 設(shè)計思路

考慮到計算器的美觀和易用性,我決定把計算器做在 Android 端,Android 系統(tǒng)的 App 的底層功能由 Java 實現(xiàn),所以工作主要分為兩部分:

  1. 設(shè)計計算器界面 (activity_main.xml)
    計算器界面用xml文件編寫,包括手機豎屏和橫屏兩個布局文件:
    豎屏:activity_main.xml 布局為 portrait
    橫屏:activity_main_land.xml 布局為 landscape
    豎屏模式可以完成基本的四則運算,不涉及科學(xué)計算
    橫屏模式除了完成基本的四則運算,還添加了科學(xué)運算

  2. 編寫計算接口 (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)

圖2.1 程序結(jié)構(gòu)流程圖
圖2.2 橫屏程序界面

三、各模塊的功能及程序說明

3.1 計算器界面設(shè)計

3.1.1 豎屏界面

包含控件

  1. 文本框 TextView:tvNowt,vPast 分別顯示當(dāng)前和過去的運算過程;
  2. 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,復(fù)制,清空tvPast中的運算過程;
  3. 數(shù)字 Button:0-9,小數(shù)點
  4. 運算符 Button:+ - × / ( ) =
  5. 運算器基本 Button:btn_del 退格,btn_clc 清空當(dāng)前math

成員變量

  1. String mathPast,用于存儲過去的運算過程
  2. String mathNow,用于存儲當(dāng)前的運算過程,即用戶正在輸入的部分
  3. int precision,設(shè)置默認精度為6位小數(shù)
  4. int equal_flag,設(shè)置flag值判斷是否需要清空mathNow進行新的運算
  5. ScienceCalculator scienceCalculator,實例化一個科學(xué)計算器
圖3.1 豎屏界面

3.1.2 橫屏界面

包含控件

  1. 文本框 TextView:tvNow, tvPast 分別顯示當(dāng)前和過去的運算過程;
  2. 功能 Buttion:btn_save,btn_copy,btn_clear 用于保存,復(fù)制,清空tvPast中的運算過程;
  3. 數(shù)字Button:0-9,小數(shù)點
  4. 基本運算符Button:+ - × / ( ) =
  5. 科學(xué)運算符Button:(12個)
    sin,cos,tan,√x,e,π,1/x,ln,log,x2,ex,xy
  6. 運算器基本Button:btn_del退格,btn_clc清空當(dāng)前math
  7. 文本框切換按鈕 tvRad,tvDeg 實現(xiàn)弧度制和角度值的切換
  8. 精度選擇器 NumberPicker

成員常量

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

成員變量

  1. String mathPast,用于存儲過去的運算過程
  2. String mathNow,用于存儲當(dāng)前的運算過程,即用戶正在輸入的部分
  3. int precision,設(shè)置默認精度為6位小數(shù),通過NumberPicker返回用戶設(shè)置的精度值
  4. int equal_flag,設(shè)置flag值判斷是否需要清空mathNow進行新的運算
  5. ScienceCalculator scienceCalculator,實例化一個科學(xué)計算器
  6. int angle_metric,角度制參數(shù),默認為DEG
圖3.2 橫屏界面

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è)置:

  1. 因為tvPast文本框高度有限,為了使用戶每次都可以看到最新的運算過程,設(shè)置 setMovementMethod(ScrollingMovementMethod.getInstance()) 方法使內(nèi)容自動滾動到最新的一行;
tvPast.setMovementMethod(ScrollingMovementMethod.getInstance());
  1. Android系統(tǒng)集成了很好的文本框內(nèi)容復(fù)制功能,設(shè)置 setTextIsSelectable(true) 即可實現(xiàn)文本框的長按復(fù)制功能;
tvPast.setTextIsSelectable(true);
  1. 由于計算器具有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è)置:

  1. mathNow長度為0,添加“+”,表示正數(shù)
  2. 以下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è)置:

  1. mathNow 長度為0,添加“-”,表示正數(shù)
  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;
    }
});

響應(yīng)場景設(shè)置:

  1. × 不能出現(xiàn)在math表達式的首位,所以場景限制在mathNow長度不為0
  2. 以下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è)置

  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; //缺失的 ) 數(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
    }
});
  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自動換行
  1. 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);
    }
}
  1. tvPast 添加新的 mathPast 到文本框
mathPast = mathPast + "=" + mathNow;
//用tvPast.set(mathPast)不能實現(xiàn)自動滾動到最新運算過程
tvPast.append(mathPast); //添加新的運算過程
  1. 獲取 tvPast 文本框?qū)傩圆L動到最新的一行
//tvPast滾動到最新的運算過程
int offset = tvPast.getLineCount() * tvPast.getLineHeight();
if (offset > tvPast.getHeight()) {
    tvPast.scrollTo(0, offset - tvPast.getHeight());
}
tvNow.setText(mathNow);
  1. 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è)置:

  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);
    }
});

響應(yīng)場景設(shè)置:

  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);
    }
});

響應(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è)置:

  1. 設(shè)置精度最大為12位,最小為0位,默認設(shè)置值為6
  2. 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)先級:

  1. “<”:棧頂 oper 優(yōu)先級低,新 oper 入棧
  2. “=”:說明要入棧的 oper 為 “)”,而棧頂 oper 為 “(”,去掉 “(”,其實也是 math 去括號的過程
  3. “>”: 棧頂 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.1 計算器操作流程圖

4.2 操作流程步驟

  1. 程序開始;
  2. 在手機上點擊計算器APP,進入默認的計算器豎屏界面,通過點擊按鈕輸入math表達式,按鈕設(shè)置了響應(yīng)事件的場景,避免了一些math 表達式的格式錯誤,最后完成math 表達式的輸入;
  3. 點擊 = 按鈕進行計算,如果運算過程中出現(xiàn)除以0的情況或者格式錯誤的math表達式,輸出Math Error,正常情況下完成math計算,輸出計算結(jié)果;
  4. 此時用戶有5個選擇:
    • 繼續(xù)輸入math表達式計算
    • 點擊保存按鈕將文本區(qū)中的全部計算過程保存到文件
    • 點擊復(fù)制按鈕將文本區(qū)中選中的文本復(fù)制到剪貼本
    • 點擊清除按鈕將文本區(qū)的全部內(nèi)容清除
    • 點擊系統(tǒng)返回鍵退出計算器
  5. 用戶在完成(3)中的1,2,3,4任意一個之后均可以點擊系統(tǒng)返回鍵退出計算器;
  6. 用戶將手機橫屏,App切換到科學(xué)計算器的界面,同樣完成(1),(2),(3),(4)操作;
  7. 程序結(jié)束。

五、測試

5.1 弧度角度運算

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

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

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

5.5 處理異常

5.6 保存運算過程到文件

math.txt 文件

六、實驗心得

本次實驗不經(jīng)鍛煉了我編寫Java程序的能力,而且使我對Android系統(tǒng)App設(shè)計有了更深的認識。

用 Java 做計算器,主要是處理 String 類型的 math 表達式,靈活運用 String 的方法,通過截取原始的 math 分治結(jié)果問題:

  1. 先預(yù)處理 math,去掉影響計算的空格等
  2. 再替換 π,e
  3. 再計算科學(xué)運算式
  4. 最后把 math 替換成 BaseCalculator 即可計算的類型
  5. 再利用棧實現(xiàn)四則運算的方法計算出最終結(jié)果

對于Android程序設(shè)計,我學(xué)會了以下幾點:

  1. Android橫豎屏切換
  2. 保存文件到手機本地
  3. 靈活運用layout布局設(shè)計App界面,掌握了基本的自適應(yīng)
  4. 自定義控件如NumberPicker,Button邊框等,會設(shè)計圓形的Button按鈕
  5. 通過butterknife設(shè)置BindView方便初始化控件

總的來說,本次實驗我收獲很多,基本上理解了編寫一個 Java 應(yīng)用的基本架構(gòu),先編寫好接口,再設(shè)計界面,最后把響應(yīng)事件與接口聯(lián)系起來,做成一個體驗很好的計算器。

但我也認識到計算器面臨的 math 表達式的類型有很多,在 NumButtons 和 OperButtons 中添加的響應(yīng)場景可能還不完善,為此,我把項目傳上了GitHub,希望開源之后,大家可以更好地改進我的計算器。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,697評論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,853評論 18 399
  • 國家電網(wǎng)公司企業(yè)標準(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 12,537評論 6 13
  • robotium的一個缺點就是不能跨應(yīng)用,但是現(xiàn)在的應(yīng)用幾乎都會有分享的功能,要不就是第三方登錄;還有就是拍照這種...
    tyoko閱讀 1,508評論 0 1
  • 2016年9月,工作日的午后醒來,看著電腦屏幕有些刺眼,聽著音樂瞇了一會,換歌。看了下情況,沒有工作要處理,繼續(xù)迷...
    此_時_彼_刻閱讀 1,064評論 0 0

友情鏈接更多精彩內(nèi)容