前言:最近刚开始学习Android,看了一下基本控件和四种布局就开始动手写这个计算器,注明一下啊,它只能算带括号的简单运算,emmm复杂的算不了。这算是自己的第一个APP,如果还有哪些Bug和不完善的地方欢迎大家指出来。让我们来看一下简易计算器的具体实现过程(注意啊,开发环境是Android Studio):
在准备写计算器前应该先在Android Studio里面新建一个项目,之后所有的文件都会被放在这个项目中。建完项目后可能很多人对计算器没什么思路,不知道该从哪下手。其实我们应该从最简单的计算器的界面入手,因为我们都知道我们是通过按钮来输入数字计算结果。
首先,我们要考虑一下布局,Android有四种基本布局:线性布局、相对布局、帧布局和百分比布局,如果要使用线性布局就要注意要嵌套使用LinearLayout,整体在垂直方向上分布,每一行又是水平分布。这里我采用的是百分比布局,它拓展了相对布局的功能。
因为百分比布局属于新增布局,所以我们需要在项目的build.gradle中添加百分比布局库的依赖,打开app/build.gradle文件,在dependencies闭包中添加如下内容(注意:AndroidStudio的新版本里面不是compile,而是implementation:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
//添加的是下面这句代码,其余的保持不变
implementation 'com.android.support:percent:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
修改了gradle文件后,会弹出一个提示,这时只需点击Sync Now就可以了,然后gradle会开始进行同步,把百分比布局库引入到项目中,然后我们就可以在activity_main.xml里写我们的界面代码,这里只需要两种控件:Button和TextView,下面是所有代码:
"http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
这里用到了相对布局,让每个按钮的位置更准确。我在按钮的背景里并不是直接添加颜色,这是因为我给整个按钮布局加了线框,首先找到app/src/main/res/drawable-v24文件,然后在此目录下新建一个xml文件,里面放如下代码:
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<stroke
android:width="0.01dp" //这是边框宽度
android:color="#b1c0c0c0"/> //这是边框颜色
shape>
紧接着在drawable文件夹里新建文件,文件的个数取决于你按钮背景色需要几种,这里我以背景是蓝色举例:
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#87CEEB" /> //这里是背景颜色的设置
<stroke
android:width="0.01dp"
android:color="#b1c0c0c0"/>
shape>
除了加边框,我还百度了下当按下按钮时按钮会变色的代码,和边框一样,此时需要在drawable文件夹里新建一个总的文件,里面包含按下按钮前的文件和按下按钮时的文件:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/gray" android:state_enabled="true"
android:state_pressed="true"/>
<item android:drawable="@drawable/white" android:state_enabled="true"
android:state_pressed="false"/>\
selector>
然后是添加按按钮前的文件drawable/white:
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="#FFFFFF"/>
<stroke
android:width="0.01dp"
android:color="#ccc0c0c0"/>
shape>
再添加按按钮时的文件drawable/gray:
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="#F5F5F5"/>
<stroke
android:width="0.01dp"
android:color="#ccc0c0c0"/>
shape>
此时你按下按钮就可以实现变色了,这里UI界面算是完成了,这也是完成计算器的第一步,然后就是要为Button注册一个监听器,就是你想通过按按钮显示什么。
这时我们需要去修改MainActivity中的代码,这里用Switch来实现选择哪一个按钮就会执行相应的操作。其中我们需要注意的地方有几种:
具体代码如下(判断条件有点冗长,还不够简化):
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private TextView textView;
private StringBuilder pending=new StringBuilder(); //StringBuilder线程不安全,但是效率高
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView=(TextView) findViewById(R.id.text_view);
Button button1=(Button) findViewById(R.id.button1);
Button button2=(Button) findViewById(R.id.button2);
Button button3=(Button) findViewById(R.id.button3);
Button button4=(Button) findViewById(R.id.button4);
Button button5=(Button) findViewById(R.id.button5);
Button button6=(Button) findViewById(R.id.button6);
Button button7=(Button) findViewById(R.id.button7);
Button button8=(Button) findViewById(R.id.button8);
Button button9=(Button) findViewById(R.id.button9);
Button button10=(Button) findViewById(R.id.button10);
Button button11=(Button) findViewById(R.id.button11);
Button button12=(Button) findViewById(R.id.button12);
Button button13=(Button) findViewById(R.id.button13);
Button button14=(Button) findViewById(R.id.button14);
Button button15=(Button) findViewById(R.id.button15);
Button button16=(Button) findViewById(R.id.button16);
Button button17=(Button) findViewById(R.id.button17);
Button button18=(Button) findViewById(R.id.button18);
Button button19=(Button) findViewById(R.id.button19);
Button button20=(Button) findViewById(R.id.button20);
button1.setOnClickListener(this); //调用监听器
button2.setOnClickListener(this);
button3.setOnClickListener(this);
button4.setOnClickListener(this);
button5.setOnClickListener(this);
button6.setOnClickListener(this);
button7.setOnClickListener(this);
button8.setOnClickListener(this);
button9.setOnClickListener(this);
button10.setOnClickListener(this);
button11.setOnClickListener(this);
button12.setOnClickListener(this);
button13.setOnClickListener(this);
button14.setOnClickListener(this);
button15.setOnClickListener(this);
button16.setOnClickListener(this);
button17.setOnClickListener(this);
button18.setOnClickListener(this);
button19.setOnClickListener(this);
button20.setOnClickListener(this);
}
private int flag=0; //用来标志小数点
private int flag2=0; //用来标志是否输出结果
@Override
public void onClick(View v) {
int last=0;
int last2=0;
if(pending.length()!=0){
last=pending.codePointAt(pending.length()-1);//最后一个元素
if(pending.length()>1){
last2=pending.codePointAt(pending.length()-2);//倒数第二个元素
}
}
switch(v.getId()){
case R.id.button1:
if((last>='0'&&last<='9'||last==')')&&decide()!=0){
Toast.makeText(MainActivity.this,"括号不匹配",Toast.LENGTH_SHORT).show();
}else if(last>='0'&&last<='9'||last==')'&&decide()==0) {
Arithmetic arithmetic = new Arithmetic();
String jieguo;
String a = "0";
try {
a= arithmetic.toSuffix(pending);
jieguo = arithmetic.result(a);
} catch (Exception ex) {
jieguo = "Error";
}
textView.setText(jieguo);
pending = pending.delete(0, pending.length());
pending = pending.append(jieguo);
flag2=1;
}
break;
case R.id.button2:
if(pending.length()!=0&&(last>='0'&&last<='9'||last==')')) {
pending = pending.append("+");
textView.setText(pending);
flag=0;
}else if(last=='×'||last=='÷'){
pending=pending.replace(pending.length()-1,pending.length(),"+");
textView.setText(pending);
}else if(last=='-'&&pending.length()!=1&&last2!='×'&&last2!='÷'&&last2!='('){
pending=pending.replace(pending.length()-1,pending.length(),"+");
textView.setText(pending);
}
break;
case R.id.button3:
if(last=='('||(last>='0'&&last<='9')||last==')'||pending.length()==0){
pending = pending.append("-");
textView.setText(pending);
flag=0;
}
if(last=='×'||last=='÷') {
pending = pending.append("(-");
textView.setText(pending);
flag=0;
}else if(last=='+'){
pending=pending.replace(pending.length()-1,pending.length(),"-");
textView.setText(pending);
}
break;
case R.id.button4:
if(pending.length()!=0&&(last>='0'&&last<='9'||last==')')) {
pending = pending.append("×");
textView.setText(pending);
flag=0;
}else if(last=='+'||last=='÷'){
pending=pending.replace(pending.length()-1,pending.length(),"×");
textView.setText(pending);
}else if(last=='-'&&pending.length()!=1&&last2!='×'&&last2!='÷'&&last2!='('){
pending=pending.replace(pending.length()-1,pending.length(),"×");
textView.setText(pending);
}
break;
case R.id.button5:
if(pending.length()!=0&&(last>='0'&&last<='9'||last==')')) {
pending = pending.append("÷");
textView.setText(pending);
flag=0;
}else if(last=='×'||last=='+'){
pending=pending.replace(pending.length()-1,pending.length(),"÷");
textView.setText(pending);
}else if(last=='-'&&pending.length()!=1&&last2!='×'&&last2!='÷'&&last2!='('){
pending=pending.replace(pending.length()-1,pending.length(),"÷");
textView.setText(pending);
}
break;
case R.id.button6:
if(flag==1) break;
if(flag2==1) break;
else if(pending.length()!=0&&(last>='0'&&last<='9')) {
pending = pending.append(".");
textView.setText(pending);
flag=1;
}
break;
case R.id.button7:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("3");
flag2=0;
flag=0;
textView.setText(pending);
}
else if(last!=')') {
pending = pending.append("3");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button8:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("6");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("6");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button9:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("9");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("9");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button10:
if(pending.length()!=0&&((last>='0'&&last<='9')||last==')')&&decide()==1) {
pending = pending.append(")");
textView.setText(pending);
}else if(decide()!=1){
Toast.makeText(MainActivity.this,"括号不匹配",Toast.LENGTH_SHORT).show();
}
break;
case R.id.button11:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("0");
flag2=0;
flag=0;
textView.setText(pending);
}else
if(last!=')') {
pending = pending.append("0");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button12:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("2");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("2");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button13:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("5");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("5");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button14:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("8");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("8");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button15:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("(");
flag2=0;
}
else if(last=='+'||last=='-'||last=='×'||last=='÷'||last=='('||pending.length()==0) {
pending = pending.append("(");
}
textView.setText(pending);
break;
case R.id.button16:
if(pending.indexOf(".")!=-1)
flag=0;
pending=pending.delete(0,pending.length());
textView.setText(pending);
break;
case R.id.button17:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("1");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("1");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button18:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("4");
flag2=0;
flag=0;
textView.setText(pending);
}else
if(last!=')') {
pending = pending.append("4");
flag2=0;
textView.setText(pending);
}
break;
case R.id.button19:
if((flag2==1&&(last>='0'&&last<='9'))||last=='r'){
pending=pending.delete(0,pending.length());
pending = pending.append("7");
flag2=0;
flag=0;
textView.setText(pending);
}else if(last!=')') {
pending = pending.append("7");
flag2=0;
flag=0;
textView.setText(pending);
}
break;
case R.id.button20:
if(last=='r'){
pending=pending.delete(0,pending.length());
textView.setText(pending);
}else if(pending.length()!=0){
if(last=='.') flag=0;
pending=pending.delete(pending.length()-1,pending.length());
textView.setText(pending);
}
break;
}
}
//判断括号是否配对
private int decide(){
int a=0,b=0;
for(int i = 0 ; i < pending.length() ;i++){
if(pending.charAt(i)=='(' ) {
a++;
}
if(pending.charAt(i)==')' ) {
b++;
}
}
if(a == b)
return 0;
if(a > b)
return 1;
return 2; //把所有可能返回的值都要写出来
}
}
注:我在括号不匹配的时候添加了Toast用来提醒用户要正确输入
写到这里,我们算是完成计算器的一半了,然后我们就要开始写我们的算法部分,也是计算器最难的部分
这里我们要知道我们一般算算式都是采用中缀表达式,即运算符在数字中间如9+(3-1)*3+10/2,而计算器并不能识别,一般都是需要我们转成前缀表达式或后缀表达式,这里我转的是后缀表达式,就拿上面的式子来说,后缀表达式就是:9 3 1 - 3 *+ 10 2 / +
这是整个计算器最核心的部分,关于中缀表达式是如何转成后缀表达式以及转成后缀表达式后如何计算,我这有一个很好的链接讲的比较清楚:
中缀表达式和后缀表达式
这里牵扯到运算符优先级问题,比较特殊的是我们需要把”(“的优先级设成0,虽然实际上它的优先级最高。还有就是我们了解栈和队列的数据结构,栈是先进后出用来存储运算符,而队列是先进先出用来存放最终的后缀表达式,以下是具体代码(在写代码之前我建议把以下算法的代码放到一个新的java文件,便于之后调试):
public class Arithmetic {
private static final Map operator=new HashMap<>();
static{
operator.put('+', 1);
operator.put('-', 1);
operator.put('×', 2);
operator.put('÷', 2);
operator.put('(', 0); //为了让括号里面的运算符都能放进去
}
//将中缀表达式转换成后缀表达式
public String toSuffix(StringBuilder pending){
List stack=new ArrayList<>(); //定义栈
List queue=new ArrayList<>(); //定义队列
StringBuilder a=pending; //这里没有用String定义变量是因为String是不可变类
String standard="+-×÷()";
char ch;
int len=0;//数字的长度
for(int i=0;iint last=0;
if(i>0){
last=a.charAt(i-1);//此元素的前一个元素
}
ch=a.charAt(i); //当前元素
if(Character.isDigit(ch)){
len++;
}else if(ch=='.'){
len++;
}else if(ch=='-'&&(last=='×'||last=='÷'||last=='('||i==0)) { //如果是负号的话,把它和数字捆绑在一起
len++;
continue;
}else if(ch=='('&&last=='-'){ //负号还有一种特殊情况-(,此时需要把它变成-1*(
a.insert(i,"1×"); //把1*插入到a中
i--;
continue;
}else if(standard.indexOf(ch)!=-1){ //是运算符其中的一个
if(len>0) {
queue.add(a.substring(i-len,i)); //因为取元素的下标是左闭右开
len = 0;
}
if(ch=='('){
stack.add(ch); //只要是左括号都放入栈中
}
if(stack.isEmpty()&&ch!='('){ //栈为空
stack.add(ch);
continue;
}
if(!stack.isEmpty()){ //栈不为空
int size=stack.size()-1; //栈顶元素下标
boolean flag=false; //是否是()
while(size>=0&&ch==')'&&stack.get(size)!='('){
queue.add(String.valueOf(stack.remove(size)));//删除的元素
size--;
}
if(ch==')'&&stack.get(size)=='('){
flag=true;
}
while (size >= 0 &&!flag&&ch!='('&& operator.get(stack.get(size)) >= operator.get(ch)) { //比较运算符的优先级
queue.add(String.valueOf(stack.remove(size)));
size--;
}
if(ch!= ')'&&ch!='(') { //把当前元素放入栈中
stack.add(ch);
} else if(ch==')'){ //把左括号移出
stack.remove(stack.size()-1);
}
}
}
if(i==a.length()-1){ //最后一个元素
if(len>0){
queue.add(a.substring(i-len+1,i+1));
}
int size=stack.size()-1;
while(size>=0){
queue.add(String.valueOf(stack.remove(size))); //把栈中剩余的运算符添加到队列中
size--;
}
}
}
String s=queue.toString(); //转成字符串
return s.substring(1,s.length()-1);//把中括号去掉
}
这里我用到了两种集合一种是Map,用来存放运算符和其对应的优先级;另一种是ArrayList,用来定义栈和队列。另外我们需要把负号的情况考虑全,最好把它和数字捆绑在一起。
最后部分就是要计算这个后缀表达式,我上面发的链接有讲后缀表达式该怎样计算,在运算时没有使用double,原因是它在计算时会有精度损失,所以这里我们采用BigDecimal,它专门用于大小数操作,也可指定小数的保留位数。具体代码如下:
public String result(String x){
String[] arr=x.split(", "); //把数字和运算符分离出来
List list =new ArrayList<>();
for(int i=0;iint size=list.size();
switch (arr[i]){
case "+":BigDecimal a=new BigDecimal(list.remove(size-2)).add(new BigDecimal(list.remove(size-2)));list.add(String.valueOf(a));break;
case "-":BigDecimal b=new BigDecimal(list.remove(size-2)).subtract(new BigDecimal(list.remove(size-2)));list.add(String.valueOf(b));break;
case "×":BigDecimal c=new BigDecimal(list.remove(size-2)).multiply(new BigDecimal(list.remove(size-2)));list.add(String.valueOf(c));break;
case "÷": BigDecimal d = new BigDecimal(list.remove(size - 2)).divide(new BigDecimal(list.remove(size - 2)),8,ROUND_HALF_UP);
DecimalFormat df = new DecimalFormat("#.#########");
list.add( String.valueOf((df.format(d))));break;//将结果格式化
default:list.add(arr[i]);break;
}
}
if(list.size()==1){
if(list.get(0).length()<11){
BigDecimal bigDecimal = new BigDecimal(list.get(0));
return bigDecimal.toPlainString();
}else{
double d=Double.valueOf(list.get(0));
return String.valueOf(d);//科学计数法
}
}else{
return "运算失败";
}
}
刚开始我发现用BigDecimal虽然可以指定小数的保留位数,但是当计算结果是整数时它也有无意义的小数部分,为了将其去掉,我们采取将其运算结果格式化:
DecimalFormat df = new DecimalFormat("#.#########");
list.add( String.valueOf((df.format(d))));break;//将结果格式化
还有一个优化之处就是,我发现我们的计算器在位数过长时会采用科学计数法输出,而double就可以实现这样的输出,所以我在最后返回字符串前加了个判断,如果位数超过11位,就用double.
此时,你的计算器就算大功告成了,如果你想更完善一下,请看下面。
一般来说,计算器界面最上面都不会有标题栏,带上会有点丑,所以来说下如何隐藏它,打开AndroidManifest文件修改其中的theme:
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
只用修改这一句就好,其余保持不变。
<resources>
<string name="app_name">Egypt_Calculatestring> //这里的Egypt_Calculate是我的app名字
resources>
修改图标:
在app/src/main/java里新建一个Image Asset,然后会出现如下界面:
在path里选择你的图片路径,然后选Next,然后点finish就好了,当你在手机上运行这个app就可以显示你的图标了
总结:计算器的代码就已经全部完成了,大家可以去试一试。在写算法部分时一定要考虑到多种情况,试验时要找比较特殊的例子来检测代码的完善性。