Android开发系列
前言
在Android开发中,常用的两种开发方式有声明式(View)和组合式(Compose)。本文使用compose开发一款Android 计算器。最终结果如下所示
Android 计算器 演示
项目创建
后面的自行项目名称即可
默认的启动页面是 Kotlin。但是Kotlin是可以与Java混合使用的。后面按照Java开发就行。
界面设计
- 首先根据计算器操作数,需要将各按键罗列出来,
利用Kotlin的 数据类创建数据(也可以用Java创建),
按键我包含了按键符号、图标、颜色
data class Operators(var symbol: String, var icon: ImageVector? =null, var color: Color = Color.Black)
val operators = arrayListOf(
arrayListOf(Operators("0"), Operators("1"), Operators("2"), Operators("3")),
arrayListOf(Operators("4"), Operators("5"), Operators("6"), Operators("7")),
arrayListOf(Operators("8"), Operators("9"), Operators("«", icon = Icons.Default.ArrowBack), Operators("=")),
arrayListOf(Operators("("), Operators(")"), Operators("C"), Operators("%", color = Color(0xFF2196F3))),
arrayListOf(Operators("+", color = Color(0xFF03A9F4)), Operators("-", color = Color(0xFF03A9F4)), Operators("*", color = Color(0xFF03A9F4)), Operators("/", color = Color(0xFF03A9F4))),
)
定义历史表达式字符串和当前表达式字符串。这种定义就等于Java中的setter与getter
// 历史表达式
var historyExpression by mutableStateOf("")
// 当前表达式
var expression by mutableStateOf("请输入表达式")
- 去掉初始的Greeting这个Compose,新建一个Compose开始进行界面布局。
@Composable
fun CalculatorUi() {}
分为历史记录栏、当前表达式栏、操作符栏
首先创建一个盒子,并获取当前屏幕的长宽。里面放一个列布局盒子。
BoxWithConstraints {
val screenWidth = maxWidth
val screenHeight = maxHeight
Column(
modifier = Modifier
.fillMaxSize()
) {}
}
在列布局盒子中,放我们的表达式和操作符
// 历史表达式
Text(
text = historyExpression,
modifier = Modifier
.fillMaxWidth()
.height(screenHeight / (2*2))
.wrapContentSize(Alignment.BottomEnd),
fontSize = 30.sp
)
// 当前表达式
Text(
text = expression,
modifier = Modifier
.fillMaxWidth()
.height(screenHeight / (2*2))
.wrapContentSize(Alignment.BottomEnd),
fontSize = 30.sp
)
// 操作符
for (i in operators) {
Row {
for (j in i)
Surface(
modifier = Modifier
.width(screenWidth / 4)
.height(screenHeight / (2 * operators.size))
.padding(5.dp)
.clickable {
expression = computer(expression, j.symbol)
},
color = Gray23,
shape = RoundedCornerShape(10.dp)
) {
if (j.icon == null)
Text(
text = j.symbol,
modifier = Modifier
.wrapContentSize(Alignment.Center),
textAlign = TextAlign.Center,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = j.color
)
)
else
Icon(
imageVector = j.icon!!,
contentDescription = null,
modifier = Modifier
.wrapContentSize(Alignment.Center)
)
}
}
}
提出点击事件
// 历史表达式
var historyExpression by mutableStateOf("")
// 当前表达式
var expression by mutableStateOf("请输入表达式")
/**
* @param text 表达式
* @param j 运算符
*/
fun computer (text: String, j: String): String {
if (text == "请输入表达式" || text == "表达式错误") {
return if (j != "«" && j != "C" && j!= "=") j
else "请输入表达式"
}
return if (j == "«") {
if (text.isEmpty()) "请输入表达式"
else text.substring(0, text.length - 1)
} else if (j == "=") {
historyExpression = text
calculate("$text#")
} else if (j == "C") {
"请输入表达式"
} else {
text+j
}
}
计算原理
前缀、中缀、后缀表达式
- 前缀表达式:运算符在运算对象之前,如:
+a*b
- 中缀表达式:运算符在运算对象之间,如:
a+b*c
- 后缀表达式:运算符在运算对象之后,如:
a*b+c
相互转换
eg. 中缀表达式 a+b*c-(d+e)
前缀表达式:
先将表达式按优先级括起来 ((a+(b*c))-(d+e))
再将符号提在括号前面 -(+(a*(bc))+(de) --> -+a*bc+de
转回去则是遇到一个符号添加括号
后缀表达式:
同理,只不过需要将符号提在后面
((a(bc)*+)(de)+-) --> abc*+de+-
优先级表
栈顶\当前 | + | - | * | / | ( | ) | #(结束) |
---|---|---|---|---|---|---|---|
+ | > | > | < | < | < | > | > |
- | > | > | < | < | < | > | > |
* | > | > | > | > | < | > | > |
/ | > | > | > | > | < | > | > |
( | < | < | < | < | < | = | 错误 |
) | > | > | > | > | 错误 | > | > |
#(开始) | < | < | < | < | < | 错误 | = |
基本思想
算数表达式 以“#…#”的格式
从左到右一次读入
-
数字 -> 入操作数栈
-
字符(默认把开头的‘#’入了操作符栈)
eg: 当前栈顶为’#‘, 当前字符为’+‘, 根据表得’<',则表示 当前字符 > 当前栈顶
-
当前字符 > 栈顶字符 -> 入操作符栈
-
当前字符 < 栈顶字符 -> 数字栈出两个,操作符栈出一个,后出数字 操作符 先出数字;继续用当前字符比对字符栈栈顶字符
-
当前字符 = 栈顶字符 -> 出操作符栈
-
当前字符 ‘’ 栈顶字符 -> 表达式错误
-
-
对于非个位数或带小数点的数,可以在读到操作符时才作为一个数,进行入数字栈操作
eg. 2+3*5
- 数字栈:2; 字符栈:#
- 数字栈:2;字符栈:#,+
- 数字栈:2,3;字符栈:#,+
- 数字栈:2,3;字符栈:#,+,*
- 数字栈:2,3,5; 字符栈:#,+,*
- 数字栈:2,15;字符栈:#,+ ----> 数字栈:17;字符栈:# ----> 数字栈:17;字符栈:
注意最后字符必须是空的,否则表达式错误
优先级表的关系由来
- 配对(=)
-
像’(‘、’)'或‘开始’、‘结束’这种必须成对存在的,当‘)’遇到‘(’或 ‘结束’遇到‘开始’这种就是 ‘=’
- 表达式错误
-
上述配对的,若类似于当前字符为’结束’,当前栈顶为’(',这种,未能完成配对,则错误
-
同理当前为’(‘,栈顶为’开始’,也未完成配置对,则错误
-
其实按理来说’)‘压根没有入栈的机会,’('那一行应该都为错误
-
‘>’
- 主要看运算符,比自己优先级小或相等的 就是 ‘>’
-
‘<’
- 同理,比自己优先级大
自己添加优先级表
eg. 添加上 ‘%’, ‘%‘与’*’、'/'同一优先级
因此
栈顶\当前 | + | - | * | / | % | ( | ) | #(结束) |
---|---|---|---|---|---|---|---|---|
+ | > | > | < | < | < | < | > | > |
- | > | > | < | < | < | < | > | > |
* | > | > | > | > | > | < | > | > |
/ | > | > | > | > | > | < | > | > |
% | > | > | > | > | > | < | > | > |
( | < | < | < | < | < | < | = | 错误 |
) | 错误 | 错误 | 错误 | 错误 | 错误 | 错误 | > | > |
#(开始) | < | < | < | < | < | < | 错误 | = |
代码实现
public class Calculate {
private static String Opr = "+-*%/()#";
private static HashMap<Character, Integer> OprPriority = new HashMap<Character, Integer>();
// '+', '-', '*', '/', '%', '(', ')', '#',
private static Character[][] OprRelation = {
{'>', '>', '<', '<', '<', '<', '>', '>'}, //'+'
{'>', '>', '<', '<', '<', '<', '>', '>'}, //'-'
{'>', '>', '>', '>', '>', '<', '>', '>'}, //'*'
{'>', '>', '>', '>', '>', '<', '>', '>'}, //'/'
{'>', '>', '>', '>', '>', '<', '>', '>'}, //'%'
{'<', '<', '<', '<', '<', '<', '=', ' '}, //'('
{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}, //')'
{'<', '<', '<', '<', '<', '<', ' ', '='} //'#'
};
/**
* 存放操作数的栈
*/
static Stack<Double> values = new Stack<>();
/**
* 存放操作符的栈
*/
static Stack<Character> ops = new Stack<>();
/**
* 这个是栈的第二个数 opera 栈顶的数
*/
private static void count(Double num_2, Double num_1, char opr) {
switch (opr) {
case '+':
values.push(num_1 + num_2);
break;
case '-':
values.push(num_1 - num_2);
break;
case '*':
values.push(num_1 * num_2);
break;
case '/':
values.push(num_1 / num_2);
break;
case '%':
values.push(num_1 % num_2);
break;
}
}
private static String operate(char ch) {
// 判断当前操作符与栈顶操作符的关系,并操作
char oprRelation = OprRelation[OprPriority.get(ops.peek())][OprPriority.get(ch)];
if (oprRelation == '<') {
// 栈顶操作符优先级低于当前操作符,当前操作符进栈
ops.push(ch);
return null;
} else if (oprRelation == '>') {
// 栈顶操作符优先级高于当前操作符,则弹出栈顶操作符,数组栈中去两个数字,并将当前操作符压入栈顶
count(values.pop(), values.pop(), ops.pop());
operate(ch);
} else if (oprRelation == '=') {
ops.pop();
return null;
} else if (oprRelation == ' ') {
return "表达式错误";
}
return null;
}
public static String calculate(String calculator) {
// 操作符栈里需初始化一个'#'
ops.push('#');
// 初始化
for (int i = 0; i < Opr.length(); i++) {
OprPriority.put(Opr.charAt(i), i);
}
// 这里需要考虑非个位数,数字只考虑0-9,不考虑'*'可省略的情况
String num = "";
for (int i=0;i<calculator.length();i++)
{
// 判断当前字符是否在Opr
if (Opr.contains(String.valueOf(calculator.charAt(i))))
{
// 数字进栈
if (!num.equals("")) {
values.push(Double.parseDouble(num));
num = "";
}
String res = operate(calculator.charAt(i));
if (res != null) return res;
} else {
num += calculator.charAt(i);
}
}
if (ops.isEmpty())
return values.peek().toString();
else return "表达式错误";
}
}
点击震动与音效
在点击时,手机的震动或者音效能带来更好的体验。
- 震动
//初始化振动器
vibrator = getSystemService(Service.VIBRATOR_SERVICE) as Vibrator?
// 在点击事件中添加
vibrator?.vibrate(longArrayOf(100, 200), -1)
- 音效
// 初始化播放器
soundPool = SoundPool.Builder().build()
// 初始化资源ID
soundID = soundPool!!.load(this, R.raw.duang, 1)
// 在点击事件中添加 (这里不能把加载弄在 .play中,音频资源加载一次就够了,且需要时间)
soundPool?.play(
soundID, 1F, 1F, 0, 0, 1.0F
);
哎~~这是第一次在CSDN上发布,有些玩意不会弄。有问题私信。