最近基本上把郭霖大神的《第一行代碼-第二版》看完了谆焊,也跟著敲了幾個(gè)app的例子,但是總覺(jué)得跟著大神寫浦夷,自己提高有限辖试,還是需要自己親自實(shí)現(xiàn)才帶感。在網(wǎng)上找了個(gè)簡(jiǎn)易計(jì)算器的效果圖劈狐,自己就寫完了罐孝。算是自己親自寫的第一個(gè)安卓app吧。不說(shuō)先看最終的效果圖:
首先分析下這個(gè)app幾個(gè)大致功能點(diǎn)吧:
- 界面部分肥缔。上面兩個(gè)地方一個(gè)用于展示輸入的表達(dá)式莲兢,一個(gè)用于顯示計(jì)算的表達(dá)式結(jié)果,用TextView正好就可以了续膳。下面的鍵盤輸入部分改艇,每個(gè)字符(或者字符串)正好用一個(gè)Button表示,而且可以看到上面4列都是那種1/4屏幕寬坟岔,正好用Android里面的layout_weight權(quán)重的方式來(lái)布局谒兄。最后一列也是,只是一個(gè)是3/4社付,一個(gè)是1/4承疲。因?yàn)橐獙㈡I盤部分沉底,所以整個(gè)界面使用RelativeLayout鸥咖,然后RelativeLayout里面套兩個(gè)LinearLayout纪隙,一個(gè)放上面兩個(gè)展示用的TextView,一個(gè)放下面的鍵盤扛或。
- 表達(dá)式處理绵咱。主要分為表達(dá)式有效性驗(yàn)證以及表達(dá)式求值。表達(dá)式有效驗(yàn)證包括當(dāng)前輸入是否合法,比如前面字符已經(jīng)是一個(gè)操作符悲伶,這個(gè)時(shí)候就不能輸入操作數(shù)或者"."號(hào)等艾恼。還有就是當(dāng)輸入"="進(jìn)行求值前需要整個(gè)表達(dá)式做一次驗(yàn)證(主要是最后一個(gè)字符不能是等號(hào),操作符)麸锉。表達(dá)式求值主要是四則混合運(yùn)算時(shí)候钠绍,計(jì)算表達(dá)式有優(yōu)先級(jí)的問(wèn)題。由于沒(méi)有括號(hào)運(yùn)算符可以花沉,相對(duì)而言簡(jiǎn)單很多(沒(méi)有括號(hào)的各種嵌套柳爽,優(yōu)先級(jí)只用考慮加減乘除的優(yōu)先級(jí)就可以了)。所以對(duì)于表達(dá)式遇到一個(gè)乘法或者除法碱屁,只需要把這個(gè)乘除法計(jì)算的結(jié)果然后替換掉它們?cè)仍诒磉_(dá)式中的位置即可磷脯,一直不停的迭代直至計(jì)算出最終結(jié)果為止。 BB了這么多娩脾,要show my code了赵誓。
布局,直接新建一個(gè)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個(gè)Layout碰声,一個(gè)放展示信息的兩個(gè)TextView诡蜓,一個(gè)用于放鍵盤,鍵盤的每列又是一個(gè)LinearLayout胰挑。
按道理計(jì)算器的界面應(yīng)該和計(jì)算邏輯分開了蔓罚,這里偷懶了下,就把計(jì)算器界面和計(jì)算邏輯放在一起了(最開始第一版是所有的布局洽腺,計(jì)算邏輯全寫在activity里面:))。然后新建一個(gè)計(jì)算器視圖類CalculatorLayout與布局文件calculator_view.xml對(duì)應(yīng)覆旱。
然后再activity_main.xml布局文件里引入這個(gè)計(jì)算器視圖就可以了蘸朋。碼如下:
<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里加載我們的計(jì)算器視圖。下面具體看看表達(dá)式的處理扣唱。
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藕坯,主要是為了統(tǒng)一給他們添加響應(yīng)事件
* */
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;
}
/*
在計(jì)算表達(dá)式值前,檢查表達(dá)式是否合法
* */
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;
}
/*
檢查當(dāng)前輸入是否合法
* */
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)) {//如果最后一位數(shù)字
if (input.equals('.')) {//如果輸入的是'.'噪沙,那么最后一個(gè)操作數(shù)中不能包含'.'
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);
}
}
/*
獲取表達(dá)式的最后一個(gè)操作數(shù)
* */
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);
}
/*
獲取表達(dá)式第一個(gè)操作數(shù)炼彪,返回值是操作數(shù)的索引
* */
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();
}
/*
計(jì)算表達(dá)式的值
* */
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);//拿到最長(zhǎng)的乘除法表達(dá)式
temp = firstNum + ( "" + operator1 + String.valueOf(calMultiOrDivForExpression(temp2.substring(0, max)))) + temp2.substring(max);
}
}
}
}
return result;
}
/*
獲取最長(zhǎng)乘除法表達(dá)式的索引值
* */
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();
}
/*
計(jì)算乘除法表達(dá)式的值,遞歸計(jì)算
* */
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));
}
/*
判斷表達(dá)式是否是數(shù)字
* */
private boolean isExpressionDigit(String expression) {
try {
Double.valueOf(expression);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}
整個(gè)CalculatorLayout還是比較簡(jiǎn)單的正歼。在CalculatorLayout的構(gòu)造函數(shù)里首先加載布局辐马,然后拿到各個(gè)控件彤路,比如兩個(gè)TextView(因?yàn)楹芏嗟胤蕉家獙?duì)展示信息進(jìn)行處理扬卷,所以把他們做成了私有屬性)。然后是為各個(gè)Button設(shè)置點(diǎn)擊回調(diào),用Lamada表達(dá)式(lamada表達(dá)式需要JDK1.8級(jí)以上才支持棺滞,需要在模塊的gralde腳本里加上compileOptions { sourceCompatibility org.gradle.api.JavaVersion.VERSION18 targetCompatibility org.gradle.api.JavaVersion.VERSION18}, 以及 jackOptions { enabled true } 這樣的配置)統(tǒng)一處理他們的回調(diào)方法患久,里面根據(jù)各個(gè)Button的text判斷怎么處理元咙。具體計(jì)算表達(dá)式上面代碼里都有相應(yīng)的注釋,大家有興趣的可以copy去測(cè)試下湃密,如果有問(wèn)題歡迎隨時(shí)反饋給我诅挑。最后我想說(shuō)的是,大神們有沒(méi)有好的Android項(xiàng)目可以練手啊泛源,循序漸進(jìn)的對(duì)新手友好的拔妥,跪謝(不得不說(shuō)還是專門的Markdown寫作軟件排版更美觀:))。