前言

计算器是每个手机自带的必备功能,生活中常用来计算比较大型复杂的数据,今天我准备自己写一个计算器APP出来,以巩固Android学习。


分析与设计

对于整个计算器软件的编写,我们分为两大部分,第一部分是UI设计,另一部分是逻辑实现。

UI设计

通常程序猿不需要懂太多的UI设计,但是简单的UI设计我们还是需要学会。计算器我们见得多了,最常见的就是顶部一个方形的显示屏,接着下面就是10多个按键,这些按键大多是圆形的,有了这些常识,我们倒不妨画个图。

这就是整个计算器大概的样子。

确定布局

那么接下来就是真正实现UI界面的时候了。
个人觉得现在constraintLayout(约束布局)最好用,因为动动手指就能实时为你展现UI界面,特别直观与方便,因此根标签就是constraintlayout了。

整个布局是这样的

确定视图种类和数目

显示屏就是一个TextView,而其余的按键都是Button,总的来说就是需要16个Button和3个icon图标以及一个TextView,为每个Button和icon以及TextView添加监听事件,实现相应的逻辑即可。

实现圆形Button

为了能改变Button的颜色以及样式,我们需要在两个 theme 中将 style 中的 parent 改为

<style name="Theme.xxx" parent="Theme.MaterialComponents.DayNight.NoActionBar.Bridge"></style>

res -> drawable 文件夹中新建一个xml文件,添加如下代码,即可实现圆形Button

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<corners android:radius="25dp" /> <!-- 设置圆角弧度 -->
<solid android:color="#fafafa" /> <!-- 设置背景颜色 -->
<size
android:width="40dp"
android:height="40dp" /> <!-- 设置大小 -->
<stroke
android:width="0dp"
android:color="#fff" /> <!-- 设置描边大小与颜色 -->
</shape>

设置渐变色

同样也在 res -> drawable 文件夹中新建xml文件,添加如下代码

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#74b981" <!-- 起始色彩 -->
android:endColor="#a1d47e" <!-- 结束色彩 -->
android:angle="45"
/>
</shape>

更改字体样式

首先下载一个字体压缩包,将 xxx.ttf 解压到 res -> font (如果没有font文件夹就创建一个),然后在 activity_main.xml 中使用即可,如

1
2
3
<Button
...
android:fontFamily="@font/myfont"/>

引入iconfont图标

在iconfont官网下载对应的图标并加到项目中

进入iconfont官网,选择合适的图标添加至项目,然后下载压缩包到本地,最后将整个解压后的文件加到项目中的 app -> src -> main -> assets中(如果没有assets文件就新建一个)

编写一个帮助类

在将 iconfont 添加至项目之后,我们需要编写一个帮助类才能使图标正常在屏幕上显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.myapp;

import android.content.Context;
import android.graphics.Typeface;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class FontManager {
//根据地址,找到iconfont中的iconfont.ttf文件
public static final String ROOT = "iconfonts/",ICONFONTS = ROOT + "iconfont.ttf";

public static Typeface getTypeface(Context context, String font) {
return Typeface.createFromAsset(context.getAssets(), font);
}

//图标一般都是包含在一个ViewGroup中,比如constraintlayout
//我们可以写一个方法,遍历指定xml parent 并且递归的覆盖每个TextView的字体
public static void markAsIconContainer(View v, Typeface typeface) {
if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
View child = vg.getChildAt(i);
markAsIconContainer(child, typeface);
}
} else if (v instanceof TextView) {
((TextView) v).setTypeface(typeface);
}
}
}
在xml文件中添加字段

values -> icons.xml(如果没有就创建一个),将对应的iconfont图标的Unicode编码用字段名储存起来,以便在 activity_main.xml 中引用。

1
2
3
4
5
<resources>
<string name="return0">&#xe63b;</string>
<string name="calender">&#xe8b4;</string>
<string name="record">&#xe600;</string>
</resources>

activity_main.xml 应该是这样子的

1
2
3
<TextView
// ...
android:text="@string/calender" />
使用help类显示图标

所有准备工作都做完了,接下来就是调用help类了,在 MainActivity 中写入如下代码

1
2
3
//helper帮助类, 加载iconfonts的必要操作
Typeface iconFont = FontManager.getTypeface(getApplicationContext(), FontManager.ICONFONTS);
FontManager.markAsIconContainer(findViewById(R.id.icons_container), iconFont);

接下来就能看到正常的图标了

逻辑部分实现

我们先总体分析一下,对于这个计算器,有很多重复的功能,比如有11个数字键的功能都极其相似,因此我们可以专门写一个方法进行封装;而其余的4个运算键也是类似的功能,我们封装一个方法用于实现 + - x ÷ 功能,最后就是归零键与 = 键,只需要单独写两个方法就好了。因此我们首先写这5个方法(其中还包括显示小数点功能的方法),分别是 bindNormalClickEvent , bindSpecialClickEvent , bindCalClickEvent , bindACClickEventbindGetResultEvent

bindNormalClickEvent方法

为了能使绑定这个动作的 btn 完成相应数字(符号)在屏幕上的显示,我们设置 num 来保存这个相应的数字(符号)。

1
2
3
4
5
6
7
8
//input是stringBuilder类型,保存着从键盘上输入的各个数字(符号)
public void bindNormalClickEvent(Button btn, String num) {
btn.setOnClickListener(view -> {
input.append(num);
mTextView.setText(input);
frequency++; //记录数字(符号)的个数
});
}

bindSpecialClickEvent方法

这个方法和 bindNormalClickEvent 方法差不多,只是记录是否有小数点,方便后面操作转换成 double 类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
public void bindSpecialClickEvent(Button btn, String num) {
btn.setOnClickListener(view -> {
//special 限定了一次计算中只能按一次 '.' 键,special初始为true
if (special) {
input.append(num);
mTextView.setText(input);
frequency++;
special = false;
isDecimal = true; //数据中是否包含小数点
}
});
}

bindCalClickEvent

这个方法用来绑定 + - x ÷ 的按钮的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void bindCalClickEvent(Button btn, String num) {
btn.setOnClickListener(view -> {
//isOnlySymbol规定 运算按钮一次运算中只能点击一次
if (isOnlySymbol){
symbol = num;
for (int i = 0; i < frequency; i++) {
//isDecimal来判定运算数中是否有小数点
if (isDecimal && ((int) input.charAt(i) - 46) == 0) {
whereDecimal = frequency - i - 1;
break;
}
}
//将stringBuilder中的字符串提取出来转化为 double 类型的数字
int temp = frequency;
for (int i = 0; i < temp; i++) {
if (isDecimal && ((int) input.charAt(i) - 46) == 0) {
frequency++;
continue;
}
firstNum += ((int) input.charAt(i) - 48) * Math.pow(10, frequency - 1 - i);
}
//whereDecimal 标记了小数点的位置,为转为成double型做准备
if (isDecimal) whereDecimal++;
firstNum /= Math.pow(10, whereDecimal);
hasSymbol = true;
if (input.length() != 0) {
input.delete(0, temp);
input.append(num);
mTextView.setText(input);
//输入完第一个数将这些属性重置,以便第二个数的输入
frequency = 0;
isDecimal = false;
whereDecimal = 0;
special = true;
}
//确保运算按钮一次运算中只能点击一次
isOnlySymbol = false;
}
});

}

bindACClickEvent方法

这是计算器的 AC 归零键功能的实现,原理是将stringBuilder中的字符清空,将所有属性全部初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void bindACClickEvent(Button btn) {
btn.setOnClickListener(view -> {
input.setLength(0);
firstNum = 0;
secondNum = 0;
mTextView.setText(input);
special = true;
frequency = 0;
isDecimal = false;
hasSymbol = false;
hasFirstNum = false;
isOnlySymbol = true;
whereDecimal = 0;
});
}

bindGetResultEvent方法

这个方法又和 bindCalClickEvent 很相似,只是最后多了一段运算符判定的代码,分别对应着 + - x ÷ 不同的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void bindGetResultEvent(Button button) {
button.setOnClickListener(view -> {
if (hasSymbol) {
double result;
input.delete(0, 1);
for (int i = 0; i < frequency; i++) {
if (isDecimal && ((int) input.charAt(i) - 46) == 0) {
whereDecimal = frequency - i - 1;
break;
}
}
int temp2 = frequency;
for (int i = 0; i < temp2; i++) {
if (isDecimal && ((int) input.charAt(i) - 46) == 0) {
frequency++;
continue;
}
secondNum += ((int) input.charAt(i) - 48) * Math.pow(10, frequency - 1 - i);
}
if (isDecimal) whereDecimal++;
secondNum /= Math.pow(10, whereDecimal);

if (Objects.equals(symbol, "+")) {
result = firstNum + secondNum;
} else if (Objects.equals(symbol, "x")) {
result = firstNum * secondNum;
} else if (Objects.equals(symbol, "÷")) {
result = firstNum / secondNum;
} else {
result = firstNum - secondNum;
}
input.setLength(0);
input.append(result);
mTextView.setText(input);
hasSymbol = false;
}
});
}

其余特殊按键

1
2
3
4
5
6
7
8
9
10
11
12

//这是结束当前activity按钮
public void toLastPage(View view) {
TextView textView = (TextView) view;
textView.setOnClickListener(view1 -> finish());
}

//这是对于某些功能还未开放的提示按钮
public void onTipping(View view, Context context) {
view.setOnClickListener(view1 -> Toast.makeText(context, "功能暂未开放", Toast.LENGTH_SHORT).show());
}

结果展示

源代码以及素材在 github 上:https://github.com/cofbro/Calculator-java-and-kotlin-