最近基本上把郭霖大神的《第一行代码-第二版》看完了,也跟着敲了几个app的例子,但是总觉得跟着大神写,自己提高有限,还是需要自己亲自实现才带感。在网上找了个简易计算器的效果图,自己就写完了。算是自己亲自写的第一个安卓app吧。不说先看最终的效果图:
首先分析下这个app几个大致功能点吧:
- 界面部分。上面两个地方一个用于展示输入的表达式,一个用于显示计算的表达式结果,用TextView正好就可以了。下面的键盘输入部分,每个字符(或者字符串)正好用一个Button表示,而且可以看到上面4列都是那种1/4屏幕宽,正好用Android里面的layout_weight权重的方式来布局。最后一列也是,只是一个是3/4,一个是1/4。因为要将键盘部分沉底,所以整个界面使用RelativeLayout,然后RelativeLayout里面套两个LinearLayout,一个放上面两个展示用的TextView,一个放下面的键盘。
- 表达式处理。主要分为表达式有效性验证以及表达式求值。表达式有效验证包括当前输入是否合法,比如前面字符已经是一个操作符,这个时候就不能输入操作数或者"."号等。还有就是当输入"="进行求值前需要整个表达式做一次验证(主要是最后一个字符不能是等号,操作符)。表达式求值主要是四则混合运算时候,计算表达式有优先级的问题。由于没有括号运算符可以,相对而言简单很多(没有括号的各种嵌套,优先级只用考虑加减乘除的优先级就可以了)。所以对于表达式遇到一个乘法或者除法,只需要把这个乘除法计算的结果然后替换掉它们原先在表达式中的位置即可,一直不停的迭代直至计算出最终结果为止。 BB了这么多,要show my code了。
布局,直接新建一个calculator_view.xml的布局文件,由于要让键盘沉底,最外层使用RelativeLayout,具体的码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:id="@+id/cal_reg_text"
android:textColor="#FFF"
android:textSize="20sp"
android:gravity="center_vertical|right"
android:layout_marginRight="5dp" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginLeft="20dp"
android:background="#6FFF">
</View>
<TextView
android:id="@+id/cal_result"
android:layout_width="match_parent"
android:layout_height="80dp"
android:textColor="#FFF"
android:textSize="20sp"
android:gravity="center_vertical|right"
android:layout_marginRight="5dp"/>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="#6FFF">
</View>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_alignParentBottom="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<Button
android:id="@+id/cal_reset"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="C"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_del"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="Del"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_dot"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="."
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_add"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="+"
android:textSize="20sp"
android:textColor="#FFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<Button
android:id="@+id/cal_7"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="7"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_8"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="8"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_9"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="9"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_minus"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="-"
android:textSize="20sp"
android:textColor="#FFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<Button
android:id="@+id/cal_4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="4"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_5"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="5"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_6"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="6"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_mul"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="*"
android:textSize="20sp"
android:textColor="#FFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<Button
android:id="@+id/cal_1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="1"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="2"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="3"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_div"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="/"
android:textSize="20sp"
android:textColor="#FFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<Button
android:id="@+id/cal_0"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
android:text="0"
android:textSize="20sp"
android:textColor="#FFF"/>
<Button
android:id="@+id/cal_equal"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:text="="
android:textSize="20sp"
android:textColor="#FFF"/></LinearLayout>
</LinearLayout>
</RelativeLayout>
次层就2个Layout,一个放展示信息的两个TextView,一个用于放键盘,键盘的每列又是一个LinearLayout。
按道理计算器的界面应该和计算逻辑分开了,这里偷懒了下,就把计算器界面和计算逻辑放在一起了(最开始第一版是所有的布局,计算逻辑全写在activity里面:))。然后新建一个计算器视图类CalculatorLayout与布局文件calculator_view.xml对应。
然后再activity_main.xml布局文件里引入这个计算器视图就可以了。码如下:
<com.example.frank.calculator.CalculatorLayout
android:id="@+id/calculator_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.frank.calculator.CalculatorLayout>
这样就完成了在activity里加载我们的计算器视图。下面具体看看表达式的处理。
CalculatorLayout的完整代码如下:
public class CalculatorLayout extends FrameLayout {
private TextView expression;
private TextView result;
public CalculatorLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.calculator_view, this);
List<Button> buttons = getAllButtons(this);
expression = (TextView)findViewById(R.id.cal_reg_text);
result = (TextView)findViewById(R.id.cal_result);
for (Button button :buttons) {
button.setOnClickListener((v) -> {
String text = (String)((Button)v).getText();
if (text.equals("Del")) {
String tempText = (String) expression.getText();
if (tempText.length() > 0) {
expression.setText(tempText.substring(0, tempText.length() - 1));
}
} else if (text.equals("C")) {
expression.setText("");
result.setText("");
} else if (text.equals("=")) {
String expression = (String) this.expression.getText();
if (isExpressionValidBeforeCal(expression)) {
result.setText("" + calculateExpression(expression));
}
} else {
String tempText = (String) expression.getText();
if (isValidForInput(text.charAt(0))) {
tempText = tempText + text;
expression.setText(tempText);
}
}
});
}
}
/*
获取所有的Button,主要是为了统一给他们添加响应事件
* */
private List<Button> getAllButtons(View view) {
List<Button> buttons = new ArrayList<>();
if (view instanceof ViewGroup) {
ViewGroup vp= (ViewGroup)view;
for (int i = 0; i < vp.getChildCount(); i++) {
View temp = vp.getChildAt(i);
if (temp instanceof Button) {
buttons.add((Button)temp);
}
buttons.addAll(getAllButtons(temp));
}
}
return buttons;
}
/*
在计算表达式值前,检查表达式是否合法
* */
private boolean isExpressionValidBeforeCal(String expression) {
if (expression.length() == 0) {
return false;
}
Character character = expression.charAt(expression.length() - 1);
if (character.equals('+') || character.equals('-') || character.equals('*') || character.equals('/') || character.equals('.')) {
return false;
}
return true;
}
/*
检查当前输入是否合法
* */
private boolean isValidForInput(Character input) {
String tempText = (String) expression.getText();
if (tempText.length() > 0) {
Character lastChar = tempText.charAt(tempText.length() - 1);
if (Character.isDigit(lastChar)) {//如果最后一位数字
if (input.equals('.')) {//如果输入的是'.',那么最后一个操作数中不能包含'.'
String lastOperatorNumber = lastOperatorNumber(tempText);
return !lastOperatorNumber.contains(".");
}
return true;
} else {
if (lastChar.equals('.')) {//如果最后一位不是'.'
return Character.isDigit(input);
} else if (lastChar.equals('+') || lastChar.equals('-') || lastChar.equals('*') || lastChar.equals('/')) {
return Character.isDigit(input);
}
return false;
}
} else {
return Character.isDigit(input);
}
}
/*
获取表达式的最后一个操作数
* */
private String lastOperatorNumber(String expression) {
if (expression.length() == 0)
return "";
int start = 0;
for (int i = expression.length() - 1; i >= 0; i--) {
Character character = expression.charAt(i);
if (character.equals('+') || character.equals('-') || character.equals('*') || character.equals('/')) {
start = i + 1;
break;
}
}
return expression.substring(start);
}
/*
获取表达式第一个操作数,返回值是操作数的索引
* */
private int firstOperatorNumber(String expression) {
if (expression.length() == 0) {
return -1;
}
for (int i = 1; i < expression.length(); i++) {
Character character = expression.charAt(i);
if (character.equals('+') || character.equals('-') || character.equals('*') || character.equals('/')) {
return i;
}
}
return expression.length();
}
/*
计算表达式的值
* */
private double calculateExpression(String expression) {
if (expression.length() == 0) {
return 0;
}
double result = 0;
String temp = expression;
while (temp.length() > 0) {
String temp2 = temp;
int firstIndex = firstOperatorNumber(temp2);
double firstNum = Double.valueOf(temp2.substring(0, firstIndex));
if (firstIndex == temp.length()) {
return firstNum;
}
Character operator1 = temp.charAt(firstIndex);
temp2 = temp2.substring(firstIndex + 1);
int secondIndex = firstOperatorNumber(temp2);
double secondNum = Double.valueOf(temp2.substring(0, secondIndex));
if (operator1.equals('*')) {
result = firstNum * secondNum;
if (secondIndex >= temp2.length())
break;
temp = String.valueOf(result) + temp2.substring(secondIndex);
} else if (operator1.equals('/')) {
result = firstNum / secondNum;
if (secondIndex >= temp2.length())
break;
temp = String.valueOf(result) + temp2.substring(secondIndex);
} else if (operator1.equals('+') || operator1.equals('-')) {
if (secondIndex == temp2.length()) {
return operator1.equals('+') ? (firstNum + secondNum) : (firstNum - secondNum);
} else {
Character operator2 = temp2.charAt(secondIndex);
if (operator2.equals('+') || operator2.equals('-')) {
double result2 = operator1.equals('+') ? (firstNum + secondNum) : (firstNum - secondNum);
result += result2;
temp = result2 + temp2.substring(secondIndex);
} else {
int max = maxMultiDivExpForExpression(temp2);//拿到最长的乘除法表达式
temp = firstNum + ( "" + operator1 + String.valueOf(calMultiOrDivForExpression(temp2.substring(0, max)))) + temp2.substring(max);
}
}
}
}
return result;
}
/*
获取最长乘除法表达式的索引值
* */
private int maxMultiDivExpForExpression(String expression) {
for (int i = 0; i < expression.length(); i++) {
Character character = expression.charAt(i);
if (character.equals('-') || character.equals('+')) {
return i;
}
}
return expression.length();
}
/*
计算乘除法表达式的值,递归计算
* */
private double calMultiOrDivForExpression(String expression) {
if (isExpressionDigit(expression)) {
return Double.valueOf(expression);
}
int firstIndex = firstOperatorNumber(expression);
double firstNum = Double.valueOf(expression.substring(0, firstIndex));
Character operator = expression.charAt(firstIndex);
return operator.equals('*') ? firstNum * calculateExpression(expression.substring(firstIndex + 1)) : firstNum / calculateExpression(expression.substring(firstIndex + 1));
}
/*
判断表达式是否是数字
* */
private boolean isExpressionDigit(String expression) {
try {
Double.valueOf(expression);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}
整个CalculatorLayout还是比较简单的。在CalculatorLayout的构造函数里首先加载布局,然后拿到各个控件,比如两个TextView(因为很多地方都要对展示信息进行处理,所以把他们做成了私有属性)。然后是为各个Button设置点击回调,用Lamada表达式(lamada表达式需要JDK1.8级以上才支持,需要在模块的gralde脚本里加上compileOptions { sourceCompatibility org.gradle.api.JavaVersion.VERSION18 targetCompatibility org.gradle.api.JavaVersion.VERSION18}, 以及 jackOptions { enabled true } 这样的配置)统一处理他们的回调方法,里面根据各个Button的text判断怎么处理。具体计算表达式上面代码里都有相应的注释,大家有兴趣的可以copy去测试下,如果有问题欢迎随时反馈给我。最后我想说的是,大神们有没有好的Android项目可以练手啊,循序渐进的对新手友好的,跪谢(不得不说还是专门的Markdown写作软件排版更美观:))。