【Android App】Calculator(二)计算过程详细分析

    在上一片文章【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 )。如下图:

【Android App】Calculator(二)计算过程详细分析_第1张图片

    结果显而易见为6,那么好,我们现在就来分析Calculator的这一计算过程的实现。


   从输入开始

    还记得我们上一节讲了在onCreate方法中,界面上的每一个Button都会被注册监听事件 mListener,当我们按下第一个字符√时,代表√按钮的监听事件被触发,我们先来看一下对象mListener所属的类

EventListener

/*
 * 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赋值的
。也就是说mHandler就是上一篇中所提到的Logic,而Logic又是整个app的控制中心。进入insert方法:
 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上。
然后回到Logic的insert方法中,最后调用setDeleteMode来更新删除按钮。
至此,我们输入的第一个字符√ 就显示在手机屏幕上了。剩余的字符输入同理我就不一一赘述了。

按下“=”后发生的一切

按下“=”的处理函数同样是在EventListener中进行处理
        case R.id.equal:
            mHandler.onEnter();
            break;
这里将处理处分转交给mHandler处理
onEnter内部
   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时,记录将会被写到本地文件。


以上就是整个计算过程的分析,写的有点凌乱,哪里写的不对或不理解的都可以提出来,大家一起讨论。



















你可能感兴趣的:(Android)