从零打造Android计算器(安卓开发初体验)
前言:
使用Android Studio开发,SharedPreferences存储数据,java动态生成组件等技术。(可下载源码)
先展示效果图,别放弃!
①初始计算器 | ②带符号、小数点加减乘除 | ③支持旋转操作 |
---|---|---|
没有困难的代码,只有勇敢的🐕🐕!
计算器的实现难度相对于我上一篇博客课程表来说小了很多!!
不浪费时间直接开始:
实现步骤:
- 渲染计算器所需的数字、符号、算式、结果
- 实现计算逻辑(需要用到数据结构相关知识,看不懂可跳过)
- 保存算式,即使关闭程序算式与结果仍然存在(使用SharedPreferences)
实现步骤一:
- 考虑到每个人的安卓设备型号不一样,为了适配按钮以及算式,仍然在java代码(MainActivity)动态生成按钮,而不是在xml写死。
- 观察发现图①按钮有五行四列,所有我们需要在activity_main.xml中设置好,并且给个id:button_grid
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical">
<GridLayout
android:id="@+id/button_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:rowCount="5"
android:columnCount="4">
</GridLayout>
</LinearLayout>
- 接下来要做的就是在MainActivity中动态渲染按钮到上方activity_main.xml中的GridLayout里
MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
GridLayout gridLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//渲染按钮
applyDraw();
}
private void applyDraw() {
//symbol数组下标,表示渲染到第几个符号或数字
int cnt = 0;
//symbol顺序是按照下方for循环的顺序
final String []symbol = {"C", "÷", "×", "⋘", "7", "8", "9", "-", "4", "5", "6", "+", "1", "2", "3", "=", "0", "."};
//通过id找到xml布局
gridLayout = findViewById(R.id.button_grid);
//因为是五行四列,所以需要两个循环来分别对行和列进行渲染
//对行进行渲染
for (int i = 0; i < 5; i++) {
//每一行中有四列,进行每一行的列渲染
for (int j = 0; j < 4; j++) {
//为每一列创建一个button实例
Button btn = new Button(this);
//GridLayout.LayoutParams设置在此gridLayout列中button布局
GridLayout.LayoutParams params1 = new GridLayout.LayoutParams();
//设置在gridLayout中的方位,参数1:在第几行。参数2:占几行。参数3:权值
params1.rowSpec = GridLayout.spec( i , 1,1);
params1.columnSpec = GridLayout.spec(j, 1, 1);
params1.setMargins(15,15,15,15);
params1.width = 0;
params1.height = 0;
//因为有两个按钮比较特殊,会占用两个spec,要进行长度判断
if (cnt < symbol.length) {
//给每一个按钮设置数组对应的值
String s = symbol[cnt++];
btn.setText(s);
//判断当前列是否为 = 或 0,其占用两个spec
if ("=".equals(s)) {
//占用两行一列
params1.rowSpec = GridLayout.spec( i , 2,1);
params1.columnSpec = GridLayout.spec(j, 1, 1);
}
if ("0".equals(s)) {
//占用两列一行
params1.rowSpec = GridLayout.spec( i , 1,1);
params1.columnSpec = GridLayout.spec(j, 2, 1);
//需要跳过占用的列
j++;
}
//按钮样式
btn.setTextSize(30);
btn.setMaxLines(5);
btn.setEllipsize(TextUtils.TruncateAt.END);
btn.setGravity(Gravity.CENTER| Gravity.CENTER_VERTICAL);
//监听按钮点击
btn.setOnClickListener(this);
//把按钮添加到此gridLayout中
gridLayout.addView(btn, params1);
}
}
}
}
@Override
public void onClick(View view) {
}
}
这样的代码不管你跑在任何安卓设备都是适配的,因为没有写死长度宽度,全部使用权重(GridLayout.spec第三个参数代表权重)。样式可按自己的风格改
往xml里添加算式和结果的TextView后步骤一就大功告成了
完整的activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="30dp">
<TextView
android:id="@+id/tex_M"
android:layout_width="20dp"
android:layout_height="20dp"
android:text="M"
android:textSize="20dp"
android:layout_margin="5dp"/>
<TextView
android:id="@+id/tex_count"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="10dp"
android:text=""
android:gravity="right"
android:textSize="35dp"/>
<TextView
android:id="@+id/result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textSize="50dp"
android:textColor="@color/black"
android:textStyle="bold"
android:text=""
android:gravity="right"/>
</LinearLayout>
<GridLayout
android:id="@+id/button_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:rowCount="5"
android:columnCount="4">
</GridLayout>
</LinearLayout>
实现步骤二:
当我们点击按钮发现并没有反应,细心观察发现我们并没有在MainActivity重写的onClick方法中添加内容。这很关键,当我们点击按钮会触发这个方法,下面我们来实现这个方法!
-
我们需要把点击的按钮记录到算式中,如果按下=按钮则生成结果(对应之前activity_main.xml中TextView)
-
之前我们提过需要使用数据结构。为什么呢,因为计算机不能像人一样识别加减乘除符号或者数字,这个时候我们就需要使用到数据结构中的栈【不懂的可以去了解一下,就是简单的先进后出(FILO)原则】结构。
例:1+2*3-4 注意:-4相当于+(-4)
- 处理字符串使他们分为两个数组分别保存:
String s = "1+2*3-4";
String[] Num = {"+","*","+"};
String[] Characters = {"1","2","3","-4"};
- 使用栈来保存第二优先级运算符(+),直接运算第一优先级(*和÷)
第三步之后取出栈顶符号和栈顶数组进行相加。(说难也不难
上onclick方法的代码(包含一些逻辑判断和输入的合法性判断,核心就是上面的内容)
@Override
public void onClick(View view) {
//式子文本
TextView textView = findViewById(R.id.tex_count);
//最终结果
TextView res = findViewById(R.id.result);
//之前的式子
String frontText = textView.getText().toString();
//按下的按钮
Button push = (Button) view;
String pressText = push.getText().toString();
//判断按下的按钮是否是运算符
boolean isCharacter = pressText.equals("+")
|| pressText.equals("-")
|| pressText.equals("×")
|| pressText.equals("÷")
|| pressText.equals(".");
//删除一个按钮值操作
if (pressText.equals("⋘")) {
//如果此次操作文本框为空或者最后一个字符,全部清空
if ("".equals(frontText) || frontText.length() == 1) {
textView.setText("");
res.setText("");
return;
}
//删除最后一个值
textView.setText(frontText.substring(0, frontText.length() - 1));
return;
}
//第一个按下的除了-号都不放行
if ("".equals(frontText)) {
if (isCharacter && !pressText.equals("-")) {
return;
}
}
//第一个是减号,第二个不能是运算符
if ("-".equals(frontText)) {
if (isCharacter) {
return;
}
}
//归零上述式子
if (push.getText().toString().equals("C")) {
textView.setText("");
res.setText("");
return;
}
//核心算法
//计算上述式子
if (push.getText().toString().equals("=")) {
//没有算式
if ("".equals(frontText)) {
return;
}
//如果最后一个是运算符不允许计算
char c = frontText.substring(frontText.length() - 1).charAt(0);
if (c < '0' || c > '9') {
return;
}
//String式子
String formula = textView.getText().toString();
//把数字和加减乘除分开
String[] Num = formula.split("[\\+|\\-|\\×|\\÷]");
String[] Characters = formula.split("[0-9]+\\.?[0-9]*");
//没有输入运算符
if (Num.length > 0 && Characters.length == 0) {
return;
}
//分别定义上述数组下标
int numPoint;
int characterPoint;
//分别创建运算符栈与数字栈
Stack<String> stackCharacter = new Stack<>();
Stack<String> stackNum = new Stack<>();
//当输入为负数为首时
if ("".equals(Num[0])) {
if (Characters.length == 1) {
double num0 = Double.parseDouble(Num[1]);
res.setText(String.valueOf(-num0));
return;
}
//计数器
characterPoint = 1;
numPoint = 1;
//把第一个符号数字分别压入栈,计数器加一
stackNum.push("-" + Num[numPoint]);
numPoint++;
} else {
//把第一个符号数字分别压入栈,计数器加一
characterPoint = 1;
numPoint = 0;
stackNum.push(Num[numPoint]);
numPoint++;//下标加一
}
if (Characters[characterPoint].equals("-")) {
stackCharacter.push("+");
characterPoint++;
//第二个数字入栈
if ("".equals(Num[numPoint])) {
numPoint++;
}
stackNum.push("-" + Num[numPoint]);
numPoint++;//下标加一
} else {
stackCharacter.push(Characters[characterPoint]);
characterPoint++;
if ("".equals(Num[numPoint])) {
numPoint++;
}
stackNum.push(Num[numPoint]);
numPoint++;//下标加一
}
//如果符号栈不为空
while (!stackCharacter.isEmpty()) {
//如果栈顶元素是乘
if (stackCharacter.peek().equals("×")) {
//取出最顶符号
stackCharacter.pop();
//取出最顶两个数字
BigDecimal b1 = new BigDecimal(stackNum.pop());
BigDecimal b2 = new BigDecimal(stackNum.pop());
Double mul = b1.multiply(b2).doubleValue();
//结果压入栈
stackNum.push(mul.toString());
} else if (stackCharacter.peek().equals("×-")) {
//取出最顶符号
stackCharacter.pop();
//取出最顶两个数字
BigDecimal b1 = new BigDecimal(stackNum.pop());
BigDecimal b2 = new BigDecimal(stackNum.pop());
Double mul = -b1.multiply(b2).doubleValue();
//结果压入栈
stackNum.push(mul.toString());
}
//如果栈顶元素是除
else if (stackCharacter.peek().equals("÷")) {
//取出最顶符号
stackCharacter.pop();
//取出最顶两个数字
String pop1 = stackNum.pop();
String pop2 = stackNum.pop();
BigDecimal b1 = new BigDecimal(pop1);
BigDecimal b2 = new BigDecimal(pop2);
if (0 == b1.compareTo(BigDecimal.ZERO)) {
res.setText("不能除以0");
return;
}
Double div = b2.divide(b1, 12, BigDecimal.ROUND_HALF_UP).doubleValue();
//结果压入栈
stackNum.push(div.toString());
} else if (stackCharacter.peek().equals("÷-")) {
//取出最顶符号
stackCharacter.pop();
//取出最顶两个数字
BigDecimal b1 = new BigDecimal(stackNum.pop());
BigDecimal b2 = new BigDecimal(stackNum.pop());
if (0 == b1.compareTo(BigDecimal.ZERO)) {
res.setText("不能除以0");
return;
}
Double div = -b2.divide(b1, 12, BigDecimal.ROUND_HALF_UP).doubleValue();
//结果压入栈
stackNum.push(div.toString());
}
//判断数组是否还有符号
if (characterPoint < Characters.length) {
//把当前下标数组值压入栈
if (Characters[characterPoint].equals("-")) {
stackCharacter.push("+");
characterPoint++;
//把当前下标数字压入栈
if ("".equals(Num[numPoint])) {
numPoint++;
}
stackNum.push("-" + Num[numPoint]);
numPoint++;//下标加一
} else {
stackCharacter.push(Characters[characterPoint]);
characterPoint++;
//把当前下标数字压入栈
if ("".equals(Num[numPoint])) {
numPoint++;
}
stackNum.push(Num[numPoint]);
numPoint++;//下标加一
}
}
//数组无符号
else {
if (!stackCharacter.isEmpty()) {
//取出栈顶符号
String temChar = stackCharacter.pop();
//取出最顶两个数字
String pop1 = stackNum.pop();
String pop2 = stackNum.pop();
Double temNum = Double.parseDouble(pop2) + Double.parseDouble(pop1);
//结果压入栈
stackNum.push(temNum.toString());
}
}
}
//最后的结果
res.setText(stackNum.pop());
return;
}
//没按 C 或 = 式子继续通过按钮增加
if (!"".equals(frontText)) {
//上一个输入的是运算符
if (frontText.substring(frontText.length() - 1).equals("+")
|| frontText.substring(frontText.length() - 1).equals("-")
|| frontText.substring(frontText.length() - 1).equals("×")
|| frontText.substring(frontText.length() - 1).equals("÷")
|| frontText.substring(frontText.length() - 1).equals(".")) {
//这次输入的也是运算符
if (isCharacter) {
if (pressText.equals(".")) {
return;
}
if (frontText.substring(frontText.length() - 1).equals("-")) {
return;
}
if (frontText.substring(frontText.length() - 1).equals("×")
|| frontText.substring(frontText.length() - 1).equals("÷")) {
if (pressText.equals("-")) {
textView.setText(frontText + pressText);
return;
}
}
//把上一个运算符替换成这次输入的
String substring = frontText.substring(0, frontText.length() - 1) + pressText;
textView.setText(substring);
return;
}
}
// 非法操作按钮 . 如2.2.222
//保证最后一个 . 的位置在运算符后面,如2.5+5.8。此时我们判断的是5.8的点而不是2.5的点的位置
int max = Math.max(Math.max(frontText.lastIndexOf("+"), frontText.lastIndexOf("-")),
Math.max(frontText.lastIndexOf("×"), frontText.lastIndexOf("÷")));
if (frontText.lastIndexOf(".") > max && pressText.equals(".")) {
return;
}
}
//将按下的按钮保存到算式
textView.setText(frontText + pressText);
}
至此,核心功能完成!!🍺
实现步骤三:
这部分内容是非核心功能,只是为了让用户拥有良好体验
- 在MainActivity中重写暂停方法,在暂停方法中实现
MainActivity
@Override
protected void onPause() {
//式子文本
TextView textView = findViewById(R.id.tex_count);
//最终结果
TextView res = findViewById(R.id.result);
//存储上次计算式子和结果
SharedPreferences sp = getSharedPreferences("LAST_FORMULA", MODE_PRIVATE);
sp.edit().putString("LAST_FORMULA",textView.getText().toString()).apply();//apply才会写入到xml配置文件里面
SharedPreferences sp1 = getSharedPreferences("LAST_RESULT", MODE_PRIVATE);
sp1.edit().putString("LAST_RESULT",res.getText().toString()).apply();//apply才会写入到xml配置文件里面
super.onPause();
}
- 保存的sp,sp1内容我们该如何获取呢?
我们再在m写一个方法来获取sp,sp1的内容,并把它们渲染到对应的TextView中即可
private void getLastFormula() {
//获取上次运算式子和结果
SharedPreferences sp1 = getSharedPreferences("LAST_FORMULA", MODE_PRIVATE);
SharedPreferences sp2 = getSharedPreferences("LAST_RESULT", MODE_PRIVATE);
//如果SoundCode,获取的值是空的,则会弹出后面的默认值
String obtain = sp1.getString("LAST_FORMULA", "");
String obtain2 = sp2.getString("LAST_RESULT", "");
//式子文本
TextView textView = findViewById(R.id.tex_count);
//最终结果
TextView res = findViewById(R.id.result);
//上次运算结果放入这次文本框
textView.setText(obtain);
res.setText(obtain2);
}
注意在oncreate方法调用此方法
实现步骤四:
创建一个新的activity_main.xml
res->layout-land->activity_main.xml
稍微改一下之前的activity_main.xml布局就可以了:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:orientation="vertical"
android:layout_marginTop="30dp">
<TextView
android:id="@+id/tex_M"
android:layout_width="20dp"
android:layout_height="20dp"
android:text="M"
android:textSize="20dp"
android:layout_margin="5dp"/>
<TextView
android:id="@+id/tex_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:maxLines="2"
android:text=""
android:gravity="right"
android:textSize="35dp"/>
<TextView
android:id="@+id/result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textSize="40dp"
android:textColor="@color/black"
android:textStyle="bold"
android:text=""
android:gravity="right"/>
</LinearLayout>
<GridLayout
android:id="@+id/button_grid"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="7"
android:orientation="vertical"
android:rowCount="5"
android:columnCount="4">
</GridLayout>
</LinearLayout>
运行
完结!!!