基于Kotlin+Compose完成一个简易的计算器

一个简易的能完成四则运算的计算器

在这里插入图片描述

完成之后大概是这样,下面是我的详细步骤

1、创建项目,初始化内部函数

  • 先创建一个Empty Compose Activity,并将他的Greeting函数命名为Calculator,同时修改主项目中的函数名

  • @Composable
    fun CalculatorApp() {
    }
    

2、创建按钮组件,便于复用

  • 由上图可知,本计算器12个按钮有相同的样式,故可以将他们封装在一个函数里面复用,减少代码堆叠
// 自定义按钮组件
@Composable
fun CalculatorButton(
    symbol: String,    // 按钮显示文本
    onClick: () -> Unit // 点击回调
) {
    Button(
        onClick = onClick,
        modifier = Modifier
            .size(75.dp)  // 按钮尺寸
            .clip(CircleShape)  // 圆形形状
    ) {
        Text(text = symbol, fontSize = 30.sp)  // 按钮文字
    }
}

这里自定义函数传入了文本以及点击的回调事件,设置了按钮尺寸、形状以及文字尺寸等

3、创建界面UI

接下来是完整页面的创建

  • 第一步:定义状态变量

  • @Composable
    fun CalculatorApp() {
    	var input by remember { mutableStateOf("0") }      // 输入表达式
        var result by remember { mutableStateOf(0.0) }    // 计算结果
        // 按钮布局定义(二维列表)
        val buttonLists = listOf(
            listOf("7", "8", "9", "/"),
            listOf("4", "5", "6", "*"),
            listOf("1", "2", "3", "-"),
            listOf("AC", "0", "=", "+")
        )
    }
    

首先定义了两个状态变量input和result,分别用于存储用户输入的值和等于之后的计算结果,通过remember和mutableStateOf确保重组后变量能够保留

input之所以定义为字符串,是因为用户输入的文本需要不断追加,使用字符串便于追加

同时定义了一个二维列表用于存储按钮的文本,为后面的循环做准备

  • 第二步:通过Column布局定义文本及按钮

  • Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            // 结果显示区域
            Text(
                text = input,
                fontSize = 48.sp,
                modifier = Modifier
                    .fillMaxWidth(),
                textAlign = TextAlign.End  // 右对齐
            )
        }
    

    首先定义了一个顶部文本区域,显示用户输入的内容,同时定义了一些布局,例如边距、文字对齐方式等

// 按钮区域列
        Column(verticalArrangement = Arrangement.spacedBy(30.dp)) {
            // 遍历按钮行
            buttonLists.forEach { row ->
                Row(horizontalArrangement = Arrangement.spacedBy(27.dp)) {
                    // 遍历行中的每个按钮
                    row.forEach { btn ->
                        CalculatorButton(symbol = btn) {
                            
                        }
                    }
                }
            }
        }

然后是代码的核心区域,按钮布局

我通过两次遍历,第一次遍历出四行按钮,第二次遍历实现每一个按钮,此时你的页面应该已经出现了如上图所示的按钮界面,只是还不能正常点击,接下来是关于注册点击时间

// 按钮区域列
        Column(verticalArrangement = Arrangement.spacedBy(30.dp)) {
            // 遍历按钮行
            buttonLists.forEach { row ->
                Row(horizontalArrangement = Arrangement.spacedBy(27.dp)) {
                    // 遍历行中的每个按钮
                    row.forEach { btn ->
                        CalculatorButton(symbol = btn) {
                            // 处理按钮点击事件
                            when(btn) {
                                "0","1","2","3","4","5","6","7","8","9" ->
                                    // 数字处理:如果当前是0则替换,否则追加
                                    if (input == "0") input = btn else input += btn
                                "AC" -> input = "0"  // 清空输入
                                "=" -> {
                                    
                                }
                                else -> input += btn  // 运算符处理
                            }
                        }
                    }
                }
            }
        }

这里我通过when处理不同按钮以实现不同的点击事件

首先,当按钮输入0-9时,先判断文本是否为0:如果为0,则将用户输入的数字替换掉零;否则在原本文本后面追加

当用户输入AC时,清空输入恢复成0

用户输入=时,我们需要做计算操作,我将单独定义一个函数计算操作结果

用户输入其他,即±*/时,直接在文本后面追加即可

3、定义计算函数

fun evaluateExpression(expr: String): Double {
    // 定义运算符优先级
    val precedence = mapOf(
        '+' to 1,
        '-' to 1,
        '*' to 2,
        '/' to 2
    )

    // 运算数栈和运算符栈
    val nums = mutableListOf<Double>()
    val ops = mutableListOf<Char>()
    var i = 0
    var allowUnary = true  // 允许负号作为一元运算符

    while (i < expr.length) {
        when (val c = expr[i]) {
            // 处理数字
            in '0'..'9' -> {
                var num = 0
                // 连续读取数字字符
                while (i < expr.length && expr[i] in '0'..'9') {
                    num = num * 10 + (expr[i++] - '0')
                }
                nums.add(num.toDouble())
                allowUnary = false
            }

            // 处理运算符
            '+', '-', '*', '/' -> {
                // 处理一元负号(当负号出现在表达式开头或运算符后)
                if (c == '-' && allowUnary) {
                    nums.add(0.0)
                }

                // 执行优先级更高的已有运算
                while (ops.isNotEmpty() && precedence[ops.last()]!! >= precedence[c]!!) {
                    compute(nums, ops)
                }

                ops.add(c)
                i++
                allowUnary = true
            }

            // 跳过空格
            ' ' -> i++

            // 非法字符处理
            else -> throw IllegalArgumentException("无效字符: $c")
        }
    }

    // 执行剩余运算
    while (ops.isNotEmpty()) {
        compute(nums, ops)
    }

    return nums.last()
}

/**
 * 执行单次运算
 */
private fun compute(nums: MutableList<Double>, ops: MutableList<Char>) {
    val b = nums.removeAt(nums.lastIndex)
    val a = nums.removeAt(nums.lastIndex)
    when (ops.removeAt(ops.lastIndex)) {
        '+' -> nums.add(a + b)
        '-' -> nums.add(a - b)
        '*' -> nums.add(a * b)
        '/' -> {
            if (b == 0.0) throw ArithmeticException("除零错误")
            nums.add(a / b)
        }
    }
}

该函数通过中缀表达式计算结果,需要我们传入文本此时的值,同时返回一个Double类型的结果

  • 函数解析:

    1、首先定义运算符优先级,乘除优先级高于加减

    // 定义运算符优先级
        val precedence = mapOf(
            '+' to 1,
            '-' to 1,
            '*' to 2,
            '/' to 2
        )
    

    2、定义数字栈和符号栈,同时加入是否存在负数的判断

    // 运算数栈和运算符栈
        val nums = mutableListOf<Double>()
        val ops = mutableListOf<Char>()
        var i = 0
        var allowUnary = true  // 允许负号作为一元运算符
    

    3、通过while循环处理用户输入的表达式,将其压入栈

    • 如果输入为数字,将其压入数字栈(如果是多位数需要*10连续计算)
    • 当输入为运算符时,先进行优先级判断,如果优先级高于栈中最后一个字符,则将其压入栈,否则执行计算,先计算优先级更高的运算(具体函数在下面,此举为了保证栈会最先弹出优先计算的运算符)
    • 如果为空格直接跳过
    • 执行剩余运算
    while (i < expr.length) {
            when (val c = expr[i]) {
                // 处理数字
                in '0'..'9' -> {
                    var num = 0
                    // 连续读取数字字符
                    while (i < expr.length && expr[i] in '0'..'9') {
                        num = num * 10 + (expr[i++] - '0')
                    }
                    nums.add(num.toDouble())
                    allowUnary = false
                }
                // 处理运算符
                '+', '-', '*', '/' -> {
                    // 处理一元负号(当负号出现在表达式开头或运算符后)
                    if (c == '-' && allowUnary) {
                        nums.add(0.0)
                    }
                    // 执行优先级更高的已有运算
                    while (ops.isNotEmpty() && precedence[ops.last()]!! >= precedence[c]!!) {
                        compute(nums, ops)
                    }
                    ops.add(c)
                    i++
                    allowUnary = true
                }
                // 跳过空格
                ' ' -> i++
                // 非法字符处理
                else -> throw IllegalArgumentException("无效字符: $c")
            }
        }
    // 执行剩余运算
        while (ops.isNotEmpty()) {
            compute(nums, ops)
        }
        return nums.last()
    

    4、定义单次计算函数

    • 弹出数字栈的两个数字,同时弹出运算符栈中的运算符,此运算符应为优先级最高的运算符
    • 运算结束之后,将运算结果压入数字栈
    // 执行单次计算
    private fun compute(nums: MutableList<Double>, ops: MutableList<Char>) {
        val b = nums.removeAt(nums.lastIndex)
        val a = nums.removeAt(nums.lastIndex)
        when (ops.removeAt(ops.lastIndex)) {
            '+' -> nums.add(a + b)
            '-' -> nums.add(a - b)
            '*' -> nums.add(a * b)
            '/' -> {
                if (b == 0.0) throw ArithmeticException("除零错误")
                nums.add(a / b)
            }
        }
    }
    

4、将该函数应用于按钮中

"=" -> {
                                    try {
                                        // 计算结果
                                        result = evaluateExpression(input)
                                        input = result.toString()
                                    } catch (e: Exception) {
                                        input = "Error"  // 错误处理
                                    }
                                }

如果输入正确,则将函数的结果赋给result,同时转换成字符串传给input,便于用户后面继续追加计算

如果输入错误,则直接将文本替换成Error

此时,一个简易的计算器已经搭建完成,他可以执行简单的四则运算。你也可以继续完善其功能,如增加小数点、括号、开根号、美化界面等等

完整代码如下:

package com.example.calculator

import android.graphics.Paint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.calculator.ui.theme.CalculatorTheme
import java.lang.Exception

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CalculatorTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    CalculatorApp()  // 调用主界面组件
                }
            }
        }
    }
}

@Composable
fun CalculatorApp() {
    // 定义状态变量
    var input by remember { mutableStateOf("0") }      // 输入表达式
    var result by remember { mutableStateOf(0.0) }    // 计算结果
    // 按钮布局定义(二维列表)
    val buttonLists = listOf(
        listOf("7", "8", "9", "/"),
        listOf("4", "5", "6", "*"),
        listOf("1", "2", "3", "-"),
        listOf("AC", "0", "=", "+")
    )

    // 主布局列
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // 结果显示区域
        Text(
            text = input,
            fontSize = 48.sp,
            modifier = Modifier
                .fillMaxWidth(),
            textAlign = TextAlign.End  // 右对齐
        )

        // 按钮区域列
        Column(verticalArrangement = Arrangement.spacedBy(30.dp)) {
            // 遍历按钮行
            buttonLists.forEach { row ->
                Row(horizontalArrangement = Arrangement.spacedBy(27.dp)) {
                    // 遍历行中的每个按钮
                    row.forEach { btn ->
                        CalculatorButton(symbol = btn) {
                            // 处理按钮点击事件
                            when(btn) {
                                "0","1","2","3","4","5","6","7","8","9" ->
                                    // 数字处理:如果当前是0则替换,否则追加
                                    if (input == "0") input = btn else input += btn
                                "AC" -> input = "0"  // 清空输入
                                "=" -> {
                                    try {
                                        // 计算结果
                                        result = evaluateExpression(input)
                                        input = result.toString()
                                    } catch (e: Exception) {
                                        input = "Error"  // 错误处理
                                    }
                                }
                                else -> input += btn  // 运算符处理
                            }
                        }
                    }
                }
            }
        }
    }
}

// 自定义按钮组件
@Composable
fun CalculatorButton(
    symbol: String,    // 按钮显示文本
    onClick: () -> Unit // 点击回调
) {
    Button(
        onClick = onClick,
        modifier = Modifier
            .size(75.dp)  // 按钮尺寸
            .clip(CircleShape)  // 圆形形状
    ) {
        Text(text = symbol, fontSize = 30.sp)  // 按钮文字
    }
}

fun evaluateExpression(expr: String): Double {
    // 定义运算符优先级
    val precedence = mapOf(
        '+' to 1,
        '-' to 1,
        '*' to 2,
        '/' to 2
    )

    // 运算数栈和运算符栈
    val nums = mutableListOf<Double>()
    val ops = mutableListOf<Char>()
    var i = 0
    var allowUnary = true  // 允许负号作为一元运算符

    while (i < expr.length) {
        when (val c = expr[i]) {
            // 处理数字
            in '0'..'9' -> {
                var num = 0
                // 连续读取数字字符
                while (i < expr.length && expr[i] in '0'..'9') {
                    num = num * 10 + (expr[i++] - '0')
                }
                nums.add(num.toDouble())
                allowUnary = false
            }

            // 处理运算符
            '+', '-', '*', '/' -> {
                // 处理一元负号(当负号出现在表达式开头或运算符后)
                if (c == '-' && allowUnary) {
                    nums.add(0.0)
                }

                // 执行优先级更高的已有运算
                while (ops.isNotEmpty() && precedence[ops.last()]!! >= precedence[c]!!) {
                    compute(nums, ops)
                }

                ops.add(c)
                i++
                allowUnary = true
            }

            // 跳过空格
            ' ' -> i++

            // 非法字符处理
            else -> throw IllegalArgumentException("无效字符: $c")
        }
    }

    // 执行剩余运算
    while (ops.isNotEmpty()) {
        compute(nums, ops)
    }

    return nums.last()
}

/**
 * 执行单次运算
 */
private fun compute(nums: MutableList<Double>, ops: MutableList<Char>) {
    val b = nums.removeAt(nums.lastIndex)
    val a = nums.removeAt(nums.lastIndex)
    when (ops.removeAt(ops.lastIndex)) {
        '+' -> nums.add(a + b)
        '-' -> nums.add(a - b)
        '*' -> nums.add(a * b)
        '/' -> {
            if (b == 0.0) throw ArithmeticException("除零错误")
            nums.add(a / b)
        }
    }
}

// 预览组件
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    CalculatorTheme {
        CalculatorApp()
    }
}


Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐