在上一片文章【Android App】Calculator(一)onCreate过程分析中我们简单介绍了Calculator启动过程中都做了些什么事情,比如都历史记录,初始化应用界面等。。。在今天这篇文章我们将重点分析Calculator是如何实现计算用户输入的。
为了能更好的分析这一过程,我输入的表达式选择了经典的海伦公式 S=√[p(p-a)(p-b)(p-c)] 这里的a=3,b=4,c=5,p=6(p=(a+b+c)/2 )。如下图:
结果显而易见为6,那么好,我们现在就来分析Calculator的这一计算过程的实现。
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.app.calculator2;
import com.android.app.calculator2.R;
import android.support.v4.view.ViewPager;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
class EventListener implements View.OnKeyListener,
View.OnClickListener,
View.OnLongClickListener {
Logic mHandler;
ViewPager mPager;
void setHandler(Logic handler, ViewPager pager) {
mHandler = handler;
mPager = pager;
}
@Override
public void onClick(View view) {
int id = view.getId();
switch (id) {
case R.id.del:
mHandler.onDelete();
break;
case R.id.clear:
mHandler.onClear();
break;
case R.id.equal:
mHandler.onEnter();
break;
default:
if (view instanceof Button) {
String text = ((Button) view).getText().toString();
if (text.length() >= 2) {
// add paren after sin, cos, ln, etc. from buttons
text += '(';
}
mHandler.insert(text);
if (mPager != null && mPager.getCurrentItem() == Calculator.ADVANCED_PANEL) {
mPager.setCurrentItem(Calculator.BASIC_PANEL);
}
}
}
}
@Override
public boolean onLongClick(View view) {
int id = view.getId();
if (id == R.id.del) {
mHandler.onClear();
return true;
}
return false;
}
@Override
public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
int action = keyEvent.getAction();
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
boolean eat = mHandler.eatHorizontalMove(keyCode == KeyEvent.KEYCODE_DPAD_LEFT);
return eat;
}
//Work-around for spurious key event from IME, bug #1639445
if (action == KeyEvent.ACTION_MULTIPLE && keyCode == KeyEvent.KEYCODE_UNKNOWN) {
return true; // eat it
}
//Calculator.log("KEY " + keyCode + "; " + action);
if (keyEvent.getUnicodeChar() == '=') {
if (action == KeyEvent.ACTION_UP) {
mHandler.onEnter();
}
return true;
}
if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER &&
keyCode != KeyEvent.KEYCODE_DPAD_UP &&
keyCode != KeyEvent.KEYCODE_DPAD_DOWN &&
keyCode != KeyEvent.KEYCODE_ENTER) {
if (keyEvent.isPrintingKey() && action == KeyEvent.ACTION_UP) {
// Tell the handler that text was updated.
mHandler.onTextChanged();
}
return false;
}
/*
We should act on KeyEvent.ACTION_DOWN, but strangely
sometimes the DOWN event isn't received, only the UP.
So the workaround is to act on UP...
http://b/issue?id=1022478
*/
if (action == KeyEvent.ACTION_UP) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
mHandler.onEnter();
break;
case KeyEvent.KEYCODE_DPAD_UP:
mHandler.onUp();
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
mHandler.onDown();
break;
}
}
return true;
}
}
case R.id.del:
mHandler.onDelete();
break;
case R.id.clear:
mHandler.onClear();
break;
case R.id.equal:
mHandler.onEnter();
break;
因为我们按下的是
√ 所以走下面这段代码:
default:
if (view instanceof Button) {
String text = ((Button) view).getText().toString();
if (text.length() >= 2) {
// add paren after sin, cos, ln, etc. from buttons
text += '(';
}
mHandler.insert(text);
if (mPager != null && mPager.getCurrentItem() == Calculator.ADVANCED_PANEL) {
mPager.setCurrentItem(Calculator.BASIC_PANEL);
}
}
}
首先会判断输入符号的长度,如果输入的是sin之类的,程序就会在sin后面自动加(来保证式子的正确性,我们这里的
√ 不满足,直接走mHandler.insert(text)这段代码,mHandler是什么东西呢?我们在上一篇文章中见到过mLogic这个对象,其实这里的mHandler就是通过mLogic赋值的
void insert(String delta) {
mDisplay.insert(delta);
setDeleteMode(DELETE_MODE_BACKSPACE);
}
这里我们可以看到在Logic的insert方法中是调用的mDisplay的insert方法前面疑问中也讲说到了mDisplay它是用来显示用户输入的,继续进入
mDisplay的insert方法:
void insert(String delta) {
EditText editor = (EditText) getCurrentView();
int cursor = editor.getSelectionStart();
editor.getText().insert(cursor, delta);
}
首先获得EditText类的实例editor,然后获得editor上面光标所在的位置cursor,接着调用editor的getText获得Editable的实例,最后调用insert方法将输入的字符插入到mDisplay上。
case R.id.equal:
mHandler.onEnter();
break;
这里将处理处分转交给mHandler处理
void onEnter() {
if (mDeleteMode == DELETE_MODE_CLEAR) {
clearWithHistory(false); // clear after an Enter on result
} else {
evaluateAndShowResult(getText(), CalculatorDisplay.Scroll.UP);
}
}
这里先判断当前的删除模式,如果为“清除”模式,则调用clearWithHistory,这里我们输入第一个字符
√ 时就已经将删除模式改为DELETE_MODE_BACKSPACE“删除模式(一个字符一个字符删)”直接进入else,调用方法evaluateAndShowResult,看方法就知道这里就是处理式子的关键所在了。该方法获需要两个参数,一个是getText方法的返回值即输入的式子
private String getText() {
return mDisplay.getText().toString();
}
另外一个参数CalculatorDisplay.Scroll.UP的意思是计算完,显示结果的时候有一个上翻的动画效果。
我们进入evaluateAndShowResult这个方法看看
public void evaluateAndShowResult(String text, Scroll scroll) {
try {
String result = evaluate(text);
if (!text.equals(result)) {
mHistory.enter(text);
mResult = result;
mDisplay.setText(mResult, scroll);
setDeleteMode(DELETE_MODE_CLEAR);
}
} catch (SyntaxException e) {
mIsError = true;
mResult = mErrorString;
mDisplay.setText(mResult, scroll);
setDeleteMode(DELETE_MODE_CLEAR);
}
}
看一下这里的方法evaluate(text),即计算出输入式子的结构,赋值给result,如果结果和输入表达式不一样的话将表达式写入历史记录,然后将结果显示在mDisplay上面。最后将删除改为“清除”模式。
我们再来看一下evaluate方法的实现
String evaluate(String input) throws SyntaxException {
if (input.trim().equals("")) {
return "";
}
// drop final infix operators (they can only result in error)
int size = input.length();
while (size > 0 && isOperator(input.charAt(size - 1))) {
input = input.substring(0, size - 1);
--size;
}
// Find and replace any translated mathematical functions.
input = replaceTranslations(input);
double value = mSymbols.eval(input);
String result = "";
for (int precision = mLineLength; precision > 6; precision--) {
result = tryFormattingWithPrecision(value, precision);
if (result.length() <= mLineLength) {
break;
}
}
return result.replace('-', MINUS).replace(INFINITY, INFINITY_UNICODE);
}
在进行了一些错误输入处理和数学符号替换后调用了mSymbols.eval(input)这个方法来得到式子的结果,然后再对结果做一些精确度的处理和字符的替换,最后将结果返回出去。我们看一下这里的mSymbols对象是Symbols类的一个实例,而Symbols类属于一个叫做arity-2.1.2.jar包,这个包专门用来处理一些数学的运算,具体见http://annop-nod-an.googlecode.com/hg/javadoc/index.html?org/javia/arity/package-summary.html 我就不在这里赘述了,因为具体的运算涉及到数据结构方面的知识,已经不再本文章的探讨范围内了,感兴趣的同学可以研究研究,对学习数据结构应该会有很大的帮助。
得到了计算结果后,会调用mHistory.enter(text);来将本次的计算放入历史记录,我们看一下它是怎么操作的
void enter(String text) {
current().clearEdited();
if (mEntries.size() >= MAX_ENTRIES) {
mEntries.remove(0);
}
if (mEntries.size() < 2 ||
!text.equals(mEntries.elementAt(mEntries.size() - 2).getBase())) {
mEntries.insertElementAt(new HistoryEntry(text), mEntries.size() - 1);
}
mPos = mEntries.size() - 1;
notifyChanged();
}
首先会去查看mEntries中的记录是否已经存满,如果存满了,就删除最早的那个记录,然后判断mEntries里面的记录是否是小于2或者当前输入的式子是否和倒数第二例历史记录相同?满足其中任何一个条件就会在mEntries的最后一位插入历史记录,原本的最后的历史记录会往后移动。后面再调用notifyChanged方法来提交这次内容的改动。
那历史记录又是在什么时候被写进本地文件进行保存的呢?
大家应该都知道Activity的生命周期
Calculator在onPause这一阶段就进行了数据的保存,我们来看一下onPause方法
public void onPause() {
super.onPause();
mLogic.updateHistory();
mPersist.setDeleteMode(mLogic.getDeleteMode());
mPersist.save();
}
首先更新历史记录,
void updateHistory() {
String text = getText();
// Don't set the ? marker for empty text or the error string.
// There is no need to evaluate those later.
if (!TextUtils.isEmpty(text) && !TextUtils.equals(text, mErrorString)
&& text.equals(mResult)) {
mHistory.update(MARKER_EVALUATE_ON_RESUME);
} else {
mHistory.update(getText());
}
}
这里做的就是看看当前mDisplay上面是否显示着上一次计算结果,如果是,则在历史记录里面加上?符号,用来在下次启动Calculator的时候重新显示计算结果,这个显示过程在上面有提到
如果mDisplay上面显示的是用户输入的式子的话,直接保存式子。
然后是设置当前的删除状态
最后调用mPersist.save()来存储持久化历史记录数据
public void save() {
try {
OutputStream os = new BufferedOutputStream(mContext.openFileOutput(FILE_NAME, 0), 8192);
DataOutputStream out = new DataOutputStream(os);
out.writeInt(LAST_VERSION);
out.writeInt(mDeleteMode);
history.write(out);
out.close();
} catch (IOException e) {
Calculator.log("" + e);
}
}
上面这段和上一篇中的从本地文件都历史文件是一个逆过程,先往文件写LAST_VERSION以便在下次都历史数据是校验用,然后写mDeleteMode,最后调用history.write(out);将历史记录写入文件
void write(DataOutput out) throws IOException {
out.writeInt(mEntries.size());
for (HistoryEntry entry : mEntries) {
entry.write(out);
}
out.writeInt(mPos);
}
首先是写入历史记录的条数,然后迭代历史记录集合一次写入每个历史记录entry.write(out)
void write(DataOutput out) throws IOException {
out.writeUTF(mBase);
out.writeUTF(mEdited);
//Calculator.log("save " + mEdited);
}
最后写入 out.writeInt(mPos);
这样当我们退出Calculator时,记录将会被写到本地文件。
以上就是整个计算过程的分析,写的有点凌乱,哪里写的不对或不理解的都可以提出来,大家一起讨论。