2.5 实战项目:简单计算器
到目前为止,虽然只学了一些Android的初级控件,但是也可以学以致用,即便只有这些简单的布局和控件,也能够做出实用的App。接下来我们设计并实现一个简单计算器。
2.5.1 设计思路
计算器是人们日常生活中最常用的工具之一,无论在电脑上还是手机上,都少不了计算器的身影。以Windwos上的计算器为例,界面简洁且十分实用,程序界面如图2-31所示。
图2-31 Windows的计算器
这个计算器界面主要分为两部分,一部分是上面的文本框,用于显示计算结果;另一部分是下面的几排按钮,用于输入数字与各种运算符。为了减少复杂度,我们可以精简一些功能,只保留数字与加、减、乘、除四则运算,另外补充一个开根号(求平方根)的运算。至于App的显示界面,基本与习惯的计算器界面保持一致,经过对操作按钮的适当排列,调整后的设计效果如图2-32所示。
图2-32 简单计算器的设计效果图
这个计算器虽然小巧,但是基本囊括了本章的知识点,先来看看用了哪些控件。
● 线性布局LinearLayout:计算器界面整体上是从上往下布局的,所以需要垂直方向的LinearLayout;下面部分每行都有4个按钮,又需要水平方向的LinearLayout。
● 滚动视图ScrollView:虽然计算器界面不宽也不高,但是以防万一,最好还是加个垂直方向的ScrollView。
● 文本视图TextView:很明显上方标题“简单计算器”就是TextView,下面的计算结果也需要使用TextView,而且是能够自动从下往上滚动的TextView,即聊天室效果的文本视图。
● 按钮Button:绝大多数数字与运算符按钮都采用Button控件。
● 图像视图ImageView:暂时未用到。
● 图像按钮ImageButton:开根号的运算符“√”虽然能够打出来,但是右上角少了数学课本上的一横,所以该按钮要用一张标准的开根号图片显示,这就用到了ImageButton。
● 状态列表图形:每个按钮都有按下和弹起两种状态,这里定制了按钮控件的自定义样式,因此用到了状态列表图形。
● 形状图形:运算结果用到的文本视图边框是圆角矩形,所以得给它定义一个shape文件,把shape定义的圆角矩形作为文本视图的背景。
● 九宫格图片:注意计算器界面左下角的“0”,该按钮是其他按钮的两倍宽,如果使用普通图片当背景,势必造成边缘线被拉宽、拉模糊的问题,故而要采用点九图片避免这种情况。
经过对计算器效果图的详细分析,我们初步了解了所运用的控件技术,接下来就可以对界面进行布局和排列了。
2.5.2 小知识:日志Log/提示Toast
在正式编码之前,读者有必要了解一下Android中的运行信息调试手段。例如,开发C程序时,我们常常用printf函数输出程序日志;开发Java程序时,我们常常用System.out.println函数输出程序日志。同样,App开发也有相应的函数输出提示信息。提示信息可分为两类,一类是给开发者看的,另一类是给用户看的。
1. Log
给开发者看的提示信息要调用Log类的相应方法,日志打印结果可在Android Studio界面下方的logcat小窗口查看。Log类各种方法的区别在于日志的等级,具体说明如下。
● Log.e:表示错误信息,比如可能导致程序崩溃的异常。
● Log.w:表示警告信息。
● Log.i:表示一般消息。
● Log.d:表示调试信息,可把程序运行时的变量值打印出来,方便跟踪调试。
● Log.v:表示冗余信息。
2. Toast
给用户看的提示信息要调用Toast类的相应方法,提示文字会在屏幕下方以一个小窗口临时展现。对于计算器来说,有好几种情况需要提示用户,如“被除数不能为0”“开根号的数值不能小于0”等。
Toast的简单用法只需一行代码就可以了,示例代码如下:
Toast.makeText(MainActivity.this, "提示文字", Toast.LENGTH_SHORT).show();
另外,计算器每个按钮的展示风格基本相同,为了减少冗余代码,可将相同的样式定义写在values目录下的styles.xml文件中,然后在布局文件节点下增加style="@style/btn_cal"这样的属性定义。下面是styles.xml中计算器按钮风格定义的例子:
<style name="btn_cal"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">match_parent</item> <item name="android:layout_weight">1</item> <item name="android:gravity">center</item> <item name="android:textColor">@color/black</item> <item name="android:textSize">30sp</item> <item name="android:background">@drawable/btn_nine_selector</item> </style>
2.5.3 代码示例
看到这里,估计读者对计算器App的布局和代码框架都了然于胸了,接下来介绍一些业务逻辑判断与基本的数学四则运算。只要设计充分并且合理,编码就会很快。计算器App运行后的计算效果如图2-33所示。
图2-33 简单计算器的运行效果图
编码过程主要分为3个步骤:
步骤01 先想好代码文件与布局文件的名称,比如代码文件取名CalculatorActivity.java、布局文件取名activity_calculator.xml。记得在AndroidManifest.xml中注册acitivity节点,不然App运行时会报ActivityNotFoundException异常,具体是在application节点下补充一行声明:
<activity android:name=".CalculatorActivity" />
步骤02 在res/layout目录下创建布局文件activity_calculator.xml,按照简单计算器的效果图在里面填入各控件的布局结构,并指定相关的属性定义。
步骤03 在项目的包名目录下创建CalculatorActivity类,仿照MainActivity代码在onCreate内部的setContentView方法中填入参数R.layout.activity_calculator,表示该页面使用activity_calculator.xml中定义的界面布局。接着编写具体的控件操作与业务代码。
下面是计算器App的主要业务代码片段:
public void onClick(View v) { int resid = v.getId(); String inputText; if (resid == R.id.ib_sqrt) { inputText = "√"; } else { inputText = ((TextView) v).getText().toString(); } Log.d(TAG, "resid="+resid+", inputText="+inputText); if (resid == R.id.btn_clear) { clear(""); } else if (resid == R.id.btn_cancel) { if (operator.equals("") == true) { if (firstNum.length() == 1) { firstNum = "0"; } else if (firstNum.length() > 0) { firstNum = firstNum.substring(0, firstNum.length() -1); } else { Toast.makeText(this, "没有可取消的数字了", Toast.LENGTH_SHORT).show(); return; } showText=firstNum; tv_result.setText(showText); }else{ if(nextNum.length()==1){ nextNum=""; }else if(nextNum.length()>0){ nextNum=nextNum.substring(0, nextNum.length()-1); }else{ Toast.makeText(this, "没有可取消的数字了", Toast.LENGTH_SHORT).show(); return; } showText=showText.substring(0, showText.length()-1); tv_result.setText(showText); } }else if(resid==R.id.btn_equal){ if(operator.length()==0||operator.equals("=")==true){ Toast.makeText(this, "请输入运算符", Toast.LENGTH_SHORT).show(); return; }else if(nextNum.length()<=0){ Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show(); return; } if(caculate()==true){ operator=inputText; showText=showText+"="+result; tv_result.setText(showText); }else{ return; } }else if(resid==R.id.btn_plus||resid==R.id.btn_minus ||resid==R.id.btn_multiply||resid==R.id.btn_divide){ if(firstNum.length()<=0){ Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show(); return; } if(operator.length()==0||operator.equals("=")==true||operator.equals("√")==true){ operator=inputText; // 操作符 showText=showText+operator; tv_result.setText(showText); }else{ Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show(); return; } }else if(resid==R.id.ib_sqrt){ if(firstNum.length()<=0){ Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show(); return; }else if(Double.parseDouble(firstNum)<0){ Toast.makeText(this, "开根号的数值不能小于0", Toast.LENGTH_SHORT).show(); return; } result=String.valueOf(Math.sqrt(Double.parseDouble(firstNum))); firstNum=result; nextNum=""; operator=inputText; showText=showText+"√="+result; tv_result.setText(showText); Log.d(TAG, "result="+result+", firstNum="+firstNum+", operator="+operator); }else{ if(operator.equals("=")==true){ operator=""; firstNum=""; showText=""; } if(resid==R.id.btn_dot){ inputText="."; } if(operator.equals("")==true){ firstNum=firstNum+inputText; }else{ nextNum=nextNum+inputText; } showText=showText+inputText; tv_result.setText(showText); } } private String operator=""; // 操作符 private String firstNum=""; // 前一个操作数 private String nextNum=""; // 后一个操作数 private String result=""; // 当前计算结果 private String showText=""; // 显示的文本内容 private boolean caculate(){ // 开始加减乘除四则运算 if(operator.equals("+")==true){ result=String.valueOf(Arith.add(firstNum, nextNum)); } else if (operator.equals("-") == true) { result = String.valueOf(Arith.sub(firstNum, nextNum)); } else if (operator.equals("×") == true) { result = String.valueOf(Arith.mul(firstNum, nextNum)); } else if (operator.equals("÷") == true) { if ("0".equals(nextNum)) { Toast.makeText(this, "被除数不能为零", Toast.LENGTH_SHORT).show(); return false; } else { result = String.valueOf(Arith.div(firstNum, nextNum)); } } firstNum = result; nextNum = ""; return true; } private void clear(String text){ // 清空并初始化 showText = text; tv_result.setText(showText); operator = ""; firstNum = ""; nextNum = ""; result = ""; }