目录html
码云连接java
实现带括号的四则运算
android
能够输入小数(".")、正负数(按"±"键)、对结果开根("√")、输入上次计算的结果("Mr")、对算式和结果清零("CE")、退格("←")
git
处理异常,包括算式格式错误,缺失左右括号,除0错误等
web
切换有理数模式和分数模式
算法
登陆、注册(还没有完成后端链接数据库)
数据库
页面布局参考iPhone自带的计算器,可是要实现括号按钮,发现排不成好看的矩形。。因而多加了MR和开根的功能。
考虑到要知足有理数计算和分数计算,因此设计一个菜单来切换模式。同时分数的计算没法处理浮点数,正好将小数点键改成/
。
顺便作个登陆功能,计划只有用户成功登陆之后才能使用分数模式,目前还没有完成。express
综上,须要三个Activity,MainActivity实现计算器,LoginActivity实现登陆,RegisterActivity实现注册。重点是MainActivity编程
清单文件以下小程序
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.edu.besti.is.onlinecalculator"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".LoginActivity" android:label="@string/login_screen_title" android:parentActivityName=".MainActivity">//ActionBar出现返回键,设置上一级界面 </activity> <activity android:name=".RegisterActivity" android:label="注册" android:parentActivityName=".LoginActivity"> </activity> </application> <uses-permission android:name="android.permission.INTERNET" />//容许该应用程序连接网络 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> </manifest>
布局文件activity_main.xml
以下
<?xml version="1.0" encoding="utf-8"?> <GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:useDefaultMargins="false" android:alignmentMode="alignBounds" android:columnOrderPreserved="false" android:layout_gravity="center_horizontal" android:background="#111" android:columnCount="4" android:rowCount="7" > <FrameLayout android:layout_width="match_parent" android:layout_height="200sp" android:layout_row="0" android:layout_column="3"> <TextView android:id="@+id/textView1" android:layout_width="match_parent" android:ellipsize="start" android:singleLine="true" android:gravity="center|start" android:layout_height="90sp" android:layout_gravity="center_horizontal" android:background="#111" android:text="" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="#fff" android:textSize="45sp" /> <TextView android:id="@+id/textView2" android:layout_width="match_parent" android:layout_height="110sp" android:layout_gravity="bottom" android:gravity="end|center" android:background="#000" android:text="" android:singleLine="true" android:textColor="#fff" android:textSize="60sp" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_row="1" android:layout_column="3" android:layout_gravity="top" android:orientation="horizontal" > <Button android:id="@+id/button1_1" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:text="CE" android:background="@drawable/button_style1" /> <Button android:id="@+id/button1_2" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:background="@drawable/button_style1" android:text="±" /> <Button android:id="@+id/button1_3" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:background="@drawable/button_style1" android:text="←" /> <Button android:id="@+id/button1_4" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:layout_margin="3sp" android:textSize="45sp" android:textColor="#fff" android:background="@drawable/button_style2" android:text="√" /> </LinearLayout> ··· </GridLayout>
使用GridLayout配合LinearLayout和FrameLayout,FrameLayout包含两个TextView,分别是用户输入的表达式和计算的结果。
每一个LinearLayout表明一行按钮,不一样的按钮设置不同的样式,以button_style1.xml
为例
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_pressed="true">//按下时的样式 <shape android:shape="rectangle">//圆角按钮 <solid android:color="#eee"/>//颜色 <corners android:radius="8dip"/>//圆角程度 </shape> </item> <item android:state_pressed="false">//松开时的样式 <shape android:shape="rectangle"> <solid android:color="#bbb"/> <corners android:radius="8dip"/> </shape> </item> </selector>
主界面效果以下
其余页面的布局见码云连接
package cn.edu.besti.is.onlinecalculator; import android.content.Intent; import android.os.StrictMode; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import java.util.LinkedList; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private Button[] buttons = new Button[23]; private int[] ids = new int[]{ R.id.button1_1, R.id.button1_2, R.id.button1_3, R.id.button1_4, R.id.button2_1, R.id.button2_2, R.id.button2_3, R.id.button2_4, R.id.button3_1, R.id.button3_2, R.id.button3_3, R.id.button3_4, R.id.button4_1, R.id.button4_2, R.id.button4_3, R.id.button4_4, R.id.button5_1, R.id.button5_2, R.id.button5_3, R.id.button5_4, R.id.button6_1, R.id.button6_2, R.id.button6_3 }; private TextView textView1, textView2; private String result = "0"; private LinkedList<String> expr = new LinkedList<>(); private String Mod = "Rational"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build()); for (int i = 0; i < ids.length; i++) { buttons[i] = findViewById(ids[i]); buttons[i].setOnClickListener(this); } this.textView1 = findViewById(R.id.textView1); this.textView2 = findViewById(R.id.textView2); } //onClick方法处理各类点击事件 @Override public void onClick(View view) { int id = view.getId(); Button button = view.findViewById(id); String current = button.getText().toString(); String token; StringBuilder expression = new StringBuilder(); if (current.equals("CE")) { expr.clear(); result = "0"; } else if (current.equals("±")) { if (!expr.isEmpty()) { token = expr.pollLast(); if (!calcArithmatic.isOperator(token)) { if (token.contains("-")) { token = token.replaceAll("-", ""); } else { token = "-" + token; } } expr.offerLast(token); } } else if (current.equals("←")) { expr.pollLast(); } else if (current.equals(".") || current.equals("/")) { if (!expr.isEmpty()) { token = expr.pollLast(); if (!calcArithmatic.isOperator(token)) { if (!token.contains(current)) { token += current; } } expr.offerLast(token); } } else if (current.equals("=")) {//按下等号时,在本地将中缀表达式转为后缀表达式,传输给服务端,接收服务器的计算结果 if (!expr.isEmpty()) { for (String s : expr) { expression.append(" ").append(s); } try { MyBC myBC = new MyBC(); final String formula = myBC.getEquation(expression.toString().trim()); try { result = Client.Connect(formula, Mod); } catch (Exception e) { Toast.makeText(this, "请检查网络链接", Toast.LENGTH_SHORT).show(); } } catch (ExprFormatException e) { result = e.getMessage(); } catch (ArithmeticException e0) { result = "Divide Zero Error"; } finally { expr.clear(); } } } else if (current.equals("√")) { if (Mod.equals("Rational")) { result = String.valueOf(Math.sqrt(Double.parseDouble(result))); } } else if (current.equals("Mr")) { if (result.matches("[0-9.\\-/]+")) { current = result; expr.offerLast(current); } } else if (calcArithmatic.isOperator(current)) { expr.offerLast(current); } else { if (!expr.isEmpty()) { token = expr.pollLast(); if (calcArithmatic.isOperator(token)) { expr.offerLast(token); expr.offer(current); } else { token += current; expr.offerLast(token); } } else { expr.offerLast(current); } } for (String s : expr) { expression.append(" ").append(s); } textView1.setText(expression.toString().trim()); textView2.setText(result); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.option1: Intent intent = new Intent(this, LoginActivity.class); startActivity(intent); return true; case R.id.option2: expr.clear(); result = ""; if (item.getTitle().equals("分数模式")) { buttons[21].setText("/"); item.setTitle("有理数模式"); Mod = "Fraction"; } else { buttons[21].setText("."); item.setTitle("分数模式"); Mod = "Rational"; } return true; default: return super.onOptionsItemSelected(item); } } }
使用private LinkedList<String> expr = new LinkedList<>();
来处理用户的每次点击形成的输入,方便将数字和操做符分开,若是队尾元素是数字或小数点,而当前值是数字或小数点,则对队尾元素进行字符串拼接;若是队尾元素是操做符,就直接入队。最后计算结果的时候,依次拼接队中元素,造成中缀表达式。
MyBC实现中缀转后缀,大体流程以下,异常处理未体现
具体代码以下
package cn.edu.besti.is.onlinecalculator; import java.util.EmptyStackException; import java.util.Stack; import java.util.StringTokenizer; class MyBC extends calcArithmatic{ private Stack<String> OpStack; private String output=""; MyBC(){ OpStack = new Stack<>(); } private void Shunt(String expr)throws ExprFormatException{ String token; StringTokenizer tokenizer = new StringTokenizer(expr); while (tokenizer.hasMoreTokens()){ token=tokenizer.nextToken(); if (isOperator(token)){ if (token.equals(")")){ try{ while (!OpStack.peek().equals("(")) { output = output.concat(OpStack.pop() + " "); } OpStack.pop(); }catch (EmptyStackException e){ throw new ExprFormatException("Missing '('"); } } else if (!OpStack.empty()){ if(judgeValue(token)>judgeValue(OpStack.peek()) || token.equals("(")) { OpStack.push(token); } else { while (!OpStack.empty() && judgeValue(token)<=judgeValue(OpStack.peek())){ output=output.concat(OpStack.pop()+" "); } OpStack.push(token); } } else { OpStack.push(token); } } else { output=output.concat(token+" "); } } while (!OpStack.empty()){ if (OpStack.peek().equals("(")){ throw new ExprFormatException("Missing ')'"); } output=output.concat(OpStack.pop()+" "); } } private int judgeValue(String str){ int value; switch(str){ case "(": value=1; break; case "+": case "-": value=2; break; case "×": case "÷": value=3; break; case ")": value=4; break; default: value=0; } return value; } String getEquation(String str) throws ExprFormatException{ Shunt(str); return output; } }
try { MyBC myBC = new MyBC(); final String formula = myBC.getEquation(expression.toString().trim()); try { result = Client.Connect(formula, Mod); } catch (Exception e) { Toast.makeText(this, "请检查网络链接",Toast.LENGTH_SHORT).show(); } } catch (ExprFormatException e) { result = e.getMessage(); } catch (ArithmeticException e0) { result = "Divide Zero Error"; } finally { expr.clear(); } }
正常来讲进行网络请求必须在线程中进行,可是由于咱们只是一个小程序,阻塞一下没有什么问题,因此我就直接在主进程里面发送请求,需在MainActivity里面加上以下代码
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build());
同时Android应用默认不开启网络链接,要在清单文件里声明
<uses-permission android:name="android.permission.INTERNET" />
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.option1: Intent intent = new Intent(this, LoginActivity.class); startActivity(intent); return true; case R.id.option2: expr.clear(); result = ""; if (item.getTitle().equals("分数模式")) { buttons[21].setText("/"); item.setTitle("有理数模式"); Mod = "Fraction"; } else { buttons[21].setText("."); item.setTitle("分数模式"); Mod = "Rational"; } return true; default: return super.onOptionsItemSelected(item); } }
点击切换模式之后,仅仅是将"."按钮的值改为"/",由于在处理点击事件的时候也是根据被点击按钮的值来决定行为的。
同时切换模式后相应的改变菜单里模式按钮的文字。
menu.xml以下,app:showAsAction="never"
决定该菜单按钮的位置,never表明永远折叠在菜单中
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/option1" android:title="登陆" app:showAsAction="never" /> <item android:id="@+id/option2" android:title="分数模式" app:showAsAction="never"/> </menu>
我定义了Client类来完成发送请求和收发数据,在这个过程当中进行加密传输,代码以下
package cn.edu.besti.is.onlinecalculator; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.security.*; import java.security.spec.X509EncodedKeySpec; import java.net.*; public class Client { public static String Connect(String formula, String mod) throws Exception { String mode = "AES"; Socket mysocket; DataInputStream in; DataOutputStream out; mysocket = new Socket(); mysocket.connect(new InetSocketAddress("172.30.7.19", 2010),5000); mysocket.setSoTimeout(5000); in = new DataInputStream(mysocket.getInputStream()); out = new DataOutputStream(mysocket.getOutputStream()); //使用AES进行后缀表达式的加密 KeyGenerator kg = KeyGenerator.getInstance(mode); kg.init(128); SecretKey k = kg.generateKey();//生成密钥 byte[] mkey = k.getEncoded(); Cipher cp = Cipher.getInstance(mode); cp.init(Cipher.ENCRYPT_MODE, k); byte[] ptext = formula.getBytes("UTF8"); byte[] ctext = cp.doFinal(ptext); //将加密后的后缀表达式传送给服务器 String out1 = B_H.parseByte2HexStr(ctext); out.writeUTF(out1); //建立客户端DH算法公、私钥 KeyPair keyPair = Key_DH5_6.createPubAndPriKey(); PublicKey pbk = keyPair.getPublic();//Client公钥 PrivateKey prk = keyPair.getPrivate();//Client私钥 //将公钥传给服务器 byte[] cpbk = pbk.getEncoded(); String CpubKey = B_H.parseByte2HexStr(cpbk); out.writeUTF(CpubKey); Thread.sleep(1000); //接收服务器公钥 String SpubKey = in.readUTF(); byte[] spbk = H_B.parseHexStr2Byte(SpubKey); KeyFactory kf = KeyFactory.getInstance("DH"); PublicKey serverPub = kf.generatePublic(new X509EncodedKeySpec(spbk)); //生成共享信息,并生成AES密钥 SecretKeySpec key = KeyAgree5_6.createKey(serverPub, prk); //对加密后缀表达式的密钥进行加密,并传给服务器 cp.init(Cipher.ENCRYPT_MODE, key); byte[] ckey = cp.doFinal(mkey); String Key = B_H.parseByte2HexStr(ckey); out.writeUTF(Key); out.writeUTF(mod); //接收服务器回答 return in.readUTF(); } }
以下设置链接请求超时时间为5秒
mysocket.connect(new InetSocketAddress("172.30.7.19", 2010),5000);
以下设置收发数据超时时间为5秒
mysocket.setSoTimeout(5000);
密码学部分参考我搭档的博客
服务器端简单的用Java实现,代码以下
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.security.*; import java.security.spec.X509EncodedKeySpec; public class Server extends Thread { Socket socketOnServer; public Server(Socket socketOnServer) { super(); this.socketOnServer = socketOnServer; } public static void main(String[] args) { ServerSocket serverForClient; try { serverForClient = new ServerSocket(2010); while (true) { System.out.println(currentThread()+"等待客户呼叫:"); Socket socketOnServer = serverForClient.accept(); new Server(socketOnServer).start(); } } catch (IOException e1) { System.out.println(e1.getMessage()); } } @Override public void run() { String mode = "AES"; DataOutputStream out = null; DataInputStream in = null; String result; try { out = new DataOutputStream(socketOnServer.getOutputStream()); in = new DataInputStream(socketOnServer.getInputStream()); //接收加密后的后缀表达式 String cformula = in.readUTF(); byte cipher[] = H_B.parseHexStr2Byte(cformula); //接收Client端公钥 String push = in.readUTF(); byte np[] = H_B.parseHexStr2Byte(push); KeyFactory kf = KeyFactory.getInstance("DH"); PublicKey ClientPub = kf.generatePublic(new X509EncodedKeySpec(np)); //建立服务器DH算法公、私钥 KeyPair keyPair = Key_DH5_6.createPubAndPriKey(); PublicKey pbk = keyPair.getPublic();//Server公钥 PrivateKey prk = keyPair.getPrivate();//Server私钥 //将服务器公钥传给Client端 byte cpbk[] = pbk.getEncoded(); String CpubKey = B_H.parseByte2HexStr(cpbk); out.writeUTF(CpubKey); Thread.sleep(1000); //生成共享信息,并生成AES密钥 SecretKeySpec key = KeyAgree5_6.createKey(ClientPub, prk); String k = in.readUTF();//读取加密后密钥 byte[] encryptKey = H_B.parseHexStr2Byte(k); String mod = in.readUTF(); //对加密后密钥进行解密 Cipher cp = Cipher.getInstance(mode); cp.init(Cipher.DECRYPT_MODE, key); byte decryptKey[] = cp.doFinal(encryptKey); //对密文进行解密 SecretKeySpec plainkey = new SecretKeySpec(decryptKey, mode); cp.init(Cipher.DECRYPT_MODE, plainkey); byte[] plain = cp.doFinal(cipher); //计算后缀表达式结果 String formula = new String(plain); MyDC myDC = new MyDC(mod); try { result = myDC.calculate(formula); //后缀表达式formula调用MyDC进行求值 } catch (ExprFormatException e) { result = e.getMessage(); } catch (ArithmeticException e0) { result = "Divide Zero Error"; } //将计算结果传给Client端 out.writeUTF(result); } catch (Exception e) { System.out.println("客户已断开" + e); } } }
MyDC流程大体以下,真正在计算时会根据是有理数模式仍是分数模式使用不一样的计算规则
密码学部分一样参考我搭档的博客
要实现ActionBar出现返回键,在清单文件中相应的Activity下设置parentActivityName
<activity android:name=".LoginActivity" android:label="@string/login_screen_title" android:parentActivityName=".MainActivity">//ActionBar出现返回键,设置上一级界面 </activity>
未完待续,随缘更新(这已经超出实验的范围了,我只是随便玩玩)
问题1解决:说明根本就没有向我主机发送请求,原来Android程序中尝试链接localhost,程序会将Android手机做为主机,固然连不到我服务端所在的电脑。应该将地址改成内网地址
问题2解决(并无):没有找到改权限的方法,因此只能直接传输密钥,不通过文件。
问题3:测试除0时返回意料以外的结果
问题3解决:Java浮点数除0会出现三种状况,NaN、Infinity、-Infinity,参考连接Java浮点数运算两个特殊的状况:NaN,Infinity。为了统一格式,我判断Infinity的状况而后主动抛出除零异常。
虽然时间不是很充裕,但仍是想熬夜敲代码,由于我不肯定我到底有没有这个能力完成它,天然要挑战一下。由于没有系统地学过Android,不少地方都是现查现学,参考别人的代码改,总的来讲我以为最后作出来的东西还算比较满意。在这个过程当中我更加深刻的了解了Android的开发机制,学会了一些小技巧,一些组件的用法等等,同时对Java web编程也有了必定了解,我以为Android其实和Web编程仍是有类似之处的,但愿以后能将数据库部分完成,“活学活用”一下。