1.界面设计

使用的ConstraintLayout作为主布局容器。

关于ConstrainLayout如何使用 结果如下

Android计算器代码 安卓计算器代码分析_优先级

设计代码如下

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/output"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:gravity="bottom|right"
        style="@style/OutputTextStyle"
        android:textStyle="bold"
        android:hint="0"
        android:padding="10dp"
        app:layout_constraintBottom_toTopOf="@id/line"
        app:layout_constraintLeft_toLeftOf="parent"/>
    <View
        android:id="@+id/line"
        android:layout_width="match_parent"
        android:layout_height="3dp"
        app:layout_constraintBottom_toTopOf="@id/add"
        android:background="@color/black"/>

    <Button
        android:id="@+id/one"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:text="1"
        android:layout_marginLeft="5dp"
        app:layout_constraintBottom_toTopOf="@id/dot"
        app:layout_constraintRight_toLeftOf="@id/two"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/four"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="1.0"
        android:onClick="onClick" />

    <Button
        android:id="@+id/two"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:text="2"
        app:layout_constraintBottom_toTopOf="@id/zero"
        app:layout_constraintLeft_toRightOf="@+id/one"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/three"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="3"
        app:layout_constraintBottom_toTopOf="@id/equal"
        app:layout_constraintLeft_toRightOf="@id/two"
        android:onClick="onClick"
        />
    <Button
        android:id="@+id/del"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="C"
        app:layout_constraintBottom_toTopOf="@id/nine"
        app:layout_constraintLeft_toRightOf="@id/rightParenthesis"
        android:onClick="onClick"
        />

    <Button
        android:id="@+id/four"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:layout_marginLeft="5dp"
        android:text="4"
        app:layout_constraintBottom_toTopOf="@id/one"
        app:layout_constraintStart_toStartOf="parent"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/five"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:text="5"
        app:layout_constraintStart_toEndOf="@id/four"
        app:layout_constraintBottom_toTopOf="@id/two"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/six"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="6"
        app:layout_constraintStart_toEndOf="@id/five"
        app:layout_constraintBottom_toTopOf="@id/three"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/seven"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:layout_marginLeft="5dp"
        android:text="7"
        app:layout_constraintBottom_toTopOf="@id/four"
        app:layout_constraintLeft_toLeftOf="parent"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/eight"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:text="8"
        app:layout_constraintBottom_toTopOf="@id/five"
        app:layout_constraintLeft_toRightOf="@id/seven"
        android:onClick="onClick"
         />

    <Button
        android:id="@+id/nine"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="9"
        app:layout_constraintBottom_toTopOf="@id/six"
        app:layout_constraintLeft_toRightOf="@id/eight"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/dot"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:layout_marginLeft="5dp"
        android:text="."
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/leftParenthesis"
        style="@style/ButtonStyle"
        android:layout_width="100dp"
        android:layout_height="80dp"
        android:layout_marginLeft="5dp"
        android:text="("
        app:layout_constraintBottom_toTopOf="@id/seven"
        app:layout_constraintStart_toStartOf="parent"
        android:onClick="onClick"/>

    <Button
        android:id="@+id/rightParenthesis"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text=")"
        app:layout_constraintLeft_toRightOf="@id/leftParenthesis"
        app:layout_constraintBottom_toTopOf="@id/eight"
        android:onClick="onClick"/>
    <Button
        android:id="@+id/equal"
        android:layout_width="200dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="="
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/zero"
        android:onClick="onClick"/>
    <Button
        android:id="@+id/zero"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="0"
        app:layout_constraintLeft_toRightOf="@id/dot"
        app:layout_constraintBottom_toBottomOf="parent"
        android:onClick="onClick"/>
    <Button
        android:id="@+id/add"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="+"
        app:layout_constraintLeft_toRightOf="@id/del"
        app:layout_constraintBottom_toTopOf="@id/sub"
        android:onClick="onClick"/>
    <Button
        android:id="@+id/sub"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="-"
        app:layout_constraintBottom_toTopOf="@id/mul"
        app:layout_constraintLeft_toRightOf="@id/nine"
        android:onClick="onClick"/>
    <Button
        android:id="@+id/mul"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="*"
        app:layout_constraintBottom_toTopOf="@id/div"
        app:layout_constraintLeft_toRightOf="@id/six"
        android:onClick="onClick"/>
    <Button
        android:id="@+id/div"
        android:layout_width="100dp"
        android:layout_height="80dp"
        style="@style/ButtonStyle"
        android:text="/"
        app:layout_constraintBottom_toTopOf="@id/equal"
        app:layout_constraintLeft_toRightOf="@id/three"
        android:onClick="onClick"/>


</androidx.constraintlayout.widget.ConstraintLayout>

使用到的values文件,colors.xml和styles.xml

Android计算器代码 安卓计算器代码分析_xml_02


Android计算器代码 安卓计算器代码分析_优先级_03

2.功能需求

能正确计算加、减、乘、除、包含小括号,小数,负数

3.具体实现

用到的技术点
①中缀转后缀生成后缀表达式(计算器的核心)
中最转后缀算法:
需要两个栈:符号栈和数字栈
1.符号栈为空,当前元素直接入栈;或者当前字符为"(“,直接入栈
2.遇到“)”,将符号栈里内容一一出栈,直到遇到”("
3.其他字符的情况,比较当前字符与符号栈顶字符的优先级,若当前元素的优先级大于栈顶元素的优先级,直接入栈;否则,一直出栈,直到符合当前元素的优先级大于栈顶元素优先级的要求,或者遇到“(”
如下是代码实现,包含了后缀表达式的运算

public Result SuffixesToSuffixes(String expression) throws CalException, ArithmeticException {
        String resExpression = "";
        Stack<Character> stack = new Stack<>();
        Stack<BigDecimal> numberStack = new Stack<>();
        Result result = new Result();

        boolean isMinus = false; //假设不是负数
        for(int i = 0 ; i < expression.length();  ){
            String str1 = "";

            //判断是负数
            if(expression.charAt(i) == '-' && (i + 1) < expression.length() && isNumber(expression.charAt(i + 1))){
                if(i == 0 || isOperator(expression.charAt(i - 1))){
                    isMinus = true;
                    i++;
                    continue;
                }
            }

            //是数字组成部分
            while (i < expression.length() && isNumberOrDot(expression.charAt(i))) {
                str1 = str1 + expression.charAt(i++);
            }

            //数字内容处理
            if(!str1.equals("")){
                resExpression += str1;
                //检查当前数字是否合法
                service.checkNumber(str1);
                //将String转换为对应的BigDecimal对象
                BigDecimal bd = new BigDecimal(str1);
                //转换成负数
                if(isMinus){
                    BigDecimal bd1 = new BigDecimal("-1");
                    bd = bd.multiply(bd1);
                    isMinus = false;
                }
                //为了显式好看,处理double类型尾部是0的情况,直接转换成对应整数
                if(bd.doubleValue() == bd.intValue()){
                    bd = new BigDecimal(bd.intValue());
                }else{
                    bd = new BigDecimal(bd.doubleValue());
                }
                //将BigDecimal对象压栈
                numberStack.push(bd);
                //其他字符处理
            }else{
                //1.栈为空,当前元素直接入栈;或者当前字符为"(",直接入栈
                if(stack.isEmpty() || expression.charAt(i) == '('){
                    stack.push(expression.charAt(i));
                //2.遇到“)”,将栈里内容一一出栈,直到遇到"("
                }else if(expression.charAt(i) == ')'){
                    while (stack.peek() != '('){
                        char c = stack.pop();
                        resExpression += c;
                        service.calculate(numberStack, c);
                    }
                    //“(”出栈
                    stack.pop();
                }else{
                    //3.其他字符的情况,比较当前字符与栈顶字符的优先级,若当前元素的优先级大于栈顶元素的优先级,直接入栈;否则,一直出栈,
                    // 直到符合要求,或者遇到“(”
                    if(operatorPrecedent(expression.charAt(i)) > operatorPrecedent(stack.peek())){
                        stack.push(expression.charAt(i));
                    }else{
                        while(!stack.isEmpty() && stack.peek() != '('
                                && operatorPrecedent(expression.charAt(i)) <= operatorPrecedent(stack.peek())) {
                            char c = stack.pop();
                            resExpression += c;
                            service.calculate(numberStack, c);
                        }
                        stack.push(expression.charAt(i));
                    }
                }
                i++;
            }
        }
        while(!stack.isEmpty()){
            char c = stack.pop();
            resExpression += c ;
            service.calculate(numberStack, c);
        }

        result.setNumberStack(numberStack);
        result.setPostfixExpression(resExpression);
        return result;
    }

②运算符优先级比较

int operatorPrecedent(char c){
        if(c == '*' || c == '/')
            return 2;
        if(c == '+' || c == '-')
            return 1;
        return -1;
    }

③判断是数字

boolean isNumber(char c){
        if(c == '0' || c == '1' || c == '2' || c=='3' || c=='4'
                || c=='5' || c=='6' || c=='7' || c=='8' || c=='9'){
            return true;
        }
        return false;
    }

④判断是数字的组成部分(比数字多了一个小数点)

static boolean isNumberOrDot(char c){
        if(isNumber(c) || c == '.'){
            return true;
        }
        return false;
    }

⑤判断是合法小数

void checkNumber(String str) throws CalException {
        int dotCount = 0;
        for(int i = 0 ; i < str.length(); i++){
            if(str.charAt(i) == '.')
                dotCount++;
        }
        if(dotCount > 1){
            throw new CalException("表达式输入有误!");
        }
    }

⑥判断是操作数符号

boolean isOperator(char ch){
        if(ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch =='(')
            return true;
        return false;
    }

⑦负数正确转换成数字
在中缀转后缀表达式的算法中有包含:
判断是负数,则isMinus标志设为true,后期数字转换的过程中判断,如果标志为true则给转换出来的数字乘-1,转换成负数。

//判断是负数
            if(expression.charAt(i) == '-' && (i + 1) < expression.length() && isNumber(expression.charAt(i + 1))){
                if(i == 0 || isOperator(expression.charAt(i - 1))){
                    isMinus = true;
                    i++;
                    continue;
                }
            }

⑧大数运算
数字使用BigDecimal

4.可能出现的闪退情况

除0异常
表达式错误(包含乱写表达式,数字输入有问题,符号或者数字多了少了)
大数除法需要指定精度

5.处理闪退情况

自定义异常类,在处理表达式的过程中抛异常,在调用该方法的地方处理异常

6.考虑是否使用架构mvc、mvp、mvvm等…

没考虑

7.可以优化的地方

关于小数点的问题,可以在用户按下小数点,自动补即按 ‘.’ 显示“0.”,并在按’.‘的逻辑中不让用户对一个数字可以按出两个及以上的’.',这样也就不需要检查数字的合法性了。

运行结果截图

1.表达式"-1*2+(-1+2)"

Android计算器代码 安卓计算器代码分析_java_04


Android计算器代码 安卓计算器代码分析_Android计算器代码_05


Android计算器代码 安卓计算器代码分析_优先级_06

2.表达式"1.1+23(4.4*5+6)*7"

Android计算器代码 安卓计算器代码分析_android_07


Android计算器代码 安卓计算器代码分析_xml_08


Android计算器代码 安卓计算器代码分析_Android计算器代码_09

代码及简要讲解

我写了四个类和一个Android主界面类来实现此功能:
①CalException:自定义的异常类
②CalService:处理计算过程和检查数字合法性
③ExpressionTool:完成中缀转后缀以及用到的一些判断方法
④Result:自定义返回的数据类型,属性包含:一个栈(用来保存数字)和一个String(用来保存中缀转后缀,生成的后缀表达式)
⑤MainActivity.java:主界面逻辑代码

其中,最主要的是ExpressionTool类,它完成了中缀转后缀的整个过程.
代码如下:
①CalException.java 这是一个自定义的异常类

public class CalException extends  Exception{
    static final long serialVersionUID = -3381275169931242948L;

    public CalException(String msg){
        super(msg);
    }
}

②CalService.java 这个类用于计算

package com.example.mycalculate;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Stack;

public class CalService {

    //检查数字(小数)
    void checkNumber(String str) throws CalException {
        int dotCount = 0;
        for(int i = 0 ; i < str.length(); i++){
            if(str.charAt(i) == '.')
                dotCount++;
        }
        if(dotCount > 1){
            throw new CalException("表达式输入有误!");
        }
    }

    //计算两个数的某种运算
    public void  calculate(Stack<BigDecimal> numberStack , char ch){
        BigDecimal num2 = numberStack.pop();
        BigDecimal num1 = numberStack.pop();
        BigDecimal res = calculate(num1, num2, ch);
        numberStack.push(res);
    }

    //具体计算
    public BigDecimal calculate(BigDecimal num1, BigDecimal num2, char c) throws ArithmeticException{
        BigDecimal res = new BigDecimal("0");
        switch (c){
            case '+': res = num1.add(num2);
                break;
            case '-': res = num1.subtract(num2);
                break;
            case '*': res = num1.multiply(num2);
                break;
            case '/':
                if(num2.equals(0))
                    throw new ArithmeticException();
                res = num1.divide(num2,16 ,RoundingMode.HALF_UP);//注意相除产生无线小数的情况,需要指明精度,以及舍入模式
                break;
        }

        return res;
    }
}

③ExpressionTool.java
主要讲一下方法SuffixesToSuffixes(String expression),这个方法中完成了中缀转后缀并计算,传入一个String类型的表达式。
首先需要两个栈,数字栈numberStack,运算符号栈stack.
表达式处理逻辑为:

package com.example.mycalculate;
import java.math.BigDecimal;
import java.util.Stack;

public class ExpressionTool {
    CalService service = new CalService();

    /**
     * 中缀转后缀表达式
     * @param expression
     * @return
     */
    public Result SuffixesToSuffixes(String expression) throws CalException, ArithmeticException {
        String resExpression = "";
        Stack<Character> stack = new Stack<>();
        Stack<BigDecimal> numberStack = new Stack<>();
        Result result = new Result();

        boolean isMinus = false; //假设不是负数
        for(int i = 0 ; i < expression.length();  ){
            String str1 = "";

            //判断是负数
            if(expression.charAt(i) == '-' && (i + 1) < expression.length() && isNumber(expression.charAt(i + 1))){
                if(i == 0 || isOperator(expression.charAt(i - 1))){
                    isMinus = true;
                    i++;
                    continue;
                }
            }

            //是数字组成部分
            while (i < expression.length() && isNumberOrDot(expression.charAt(i))) {
                str1 = str1 + expression.charAt(i++);
            }

            //数字内容处理
            if(!str1.equals("")){
                resExpression += str1;
                //检查当前数字是否合法
                service.checkNumber(str1);
                //将String转换为对应的BigDecimal对象
                BigDecimal bd = new BigDecimal(str1);
                //转换成负数
                if(isMinus){
                    BigDecimal bd1 = new BigDecimal("-1");
                    bd = bd.multiply(bd1);
                    isMinus = false;
                }
                //为了显式好看,处理double类型尾部是0的情况,直接转换成对应整数
                if(bd.doubleValue() == bd.intValue()){
                    bd = new BigDecimal(bd.intValue());
                }else{
                    bd = new BigDecimal(bd.doubleValue());
                }
                //将BigDecimal对象压栈
                numberStack.push(bd);
                //其他字符处理
            }else{
                //1.栈为空,当前元素直接入栈;或者当前字符为"(",直接入栈
                if(stack.isEmpty() || expression.charAt(i) == '('){
                    stack.push(expression.charAt(i));
                //2.遇到“)”,将栈里内容一一出栈,直到遇到"("
                }else if(expression.charAt(i) == ')'){
                    while (stack.peek() != '('){
                        char c = stack.pop();
                        resExpression += c;
                        service.calculate(numberStack, c);
                    }
                    //“(”出栈
                    stack.pop();
                }else{
                    //3.其他字符的情况,比较当前字符与栈顶字符的优先级,若当前元素的优先级大于栈顶元素的优先级,直接入栈;否则,一直出栈,
                    // 直到符合要求,或者遇到“(”
                    if(operatorPrecedent(expression.charAt(i)) > operatorPrecedent(stack.peek())){
                        stack.push(expression.charAt(i));
                    }else{
                        while(!stack.isEmpty() && stack.peek() != '('
                                && operatorPrecedent(expression.charAt(i)) <= operatorPrecedent(stack.peek())) {
                            char c = stack.pop();
                            resExpression += c;
                            service.calculate(numberStack, c);
                        }
                        stack.push(expression.charAt(i));
                    }
                }
                i++;
            }
        }
        while(!stack.isEmpty()){
            char c = stack.pop();
            resExpression += c ;
            service.calculate(numberStack, c);
        }

        result.setNumberStack(numberStack);
        result.setPostfixExpression(resExpression);
        return result;
    }


    //判断是否是操作符
    static boolean isOperator(char ch){
        if(ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch =='(')
            return true;
        return false;
    }

    //判断是否是数字
    static boolean isNumber(char c){
        if(c == '0' || c == '1' || c == '2' || c=='3' || c=='4'
                || c=='5' || c=='6' || c=='7' || c=='8' || c=='9'){
            return true;
        }
        return false;
    }


    /**
     * 返回操作符的优先级,“*”和“/”的优先级为2,“+”和“-”的优先级为“1”
     * @param c
     * @return
     */
    int operatorPrecedent(char c){
        if(c == '*' || c == '/')
            return 2;
        if(c == '+' || c == '-')
            return 1;
        return -1;
    }

    //是数字或者'.'
    static boolean isNumberOrDot(char c){
        if(isNumber(c) || c == '.'){
            return true;
        }
        return false;
    }
}

④Result.java 自定义的返回类型,返回一个数字栈用于存中缀转后缀中的数字和一个表达式String用于返回中缀转后缀的最终后缀表达式

import java.math.BigDecimal;
import java.util.Stack;

public class Result {
    Stack<BigDecimal> numberStack = null;
    String postfixExpression = "";
    public Result(){
        numberStack = new Stack<>();
    }

    public void setPostfixExpression(String postfixExpression) {
        this.postfixExpression = postfixExpression;
    }

    public String getPostfixExpression() {
        return postfixExpression;
    }

    public Stack<BigDecimal> getNumberStack() {
        return numberStack;
    }

    public void setNumberStack(Stack<BigDecimal> numberStack) {
        this.numberStack = numberStack;
    }
}