diff options
Diffstat (limited to 'src/com/android/calculator2')
-rw-r--r-- | src/com/android/calculator2/Calculator.java | 161 | ||||
-rw-r--r-- | src/com/android/calculator2/CalculatorDisplay.java | 157 | ||||
-rw-r--r-- | src/com/android/calculator2/CalculatorEditable.java | 110 | ||||
-rw-r--r-- | src/com/android/calculator2/ColorButton.java | 147 | ||||
-rw-r--r-- | src/com/android/calculator2/EventListener.java | 129 | ||||
-rw-r--r-- | src/com/android/calculator2/History.java | 118 | ||||
-rw-r--r-- | src/com/android/calculator2/HistoryAdapter.java | 87 | ||||
-rw-r--r-- | src/com/android/calculator2/HistoryEntry.java | 69 | ||||
-rw-r--r-- | src/com/android/calculator2/Logic.java | 208 | ||||
-rw-r--r-- | src/com/android/calculator2/PanelSwitcher.java | 140 | ||||
-rw-r--r-- | src/com/android/calculator2/Persist.java | 70 |
11 files changed, 1396 insertions, 0 deletions
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java new file mode 100644 index 0000000..eb7453d --- /dev/null +++ b/src/com/android/calculator2/Calculator.java @@ -0,0 +1,161 @@ +/* + * 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.calculator2; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.util.Config; +import android.view.Menu; +import android.view.MenuItem; +import android.view.Window; +import android.view.View; +import android.widget.Button; +import android.widget.ListView; +import android.content.res.Configuration; + +public class Calculator extends Activity { + EventListener mListener = new EventListener(); + private CalculatorDisplay mDisplay; + private Persist mPersist; + private History mHistory; + private Logic mLogic; + private PanelSwitcher mPanelSwitcher; + + private static final int CMD_CLEAR_HISTORY = 1; + private static final int CMD_BASIC_PANEL = 2; + private static final int CMD_ADVANCED_PANEL = 3; + + static final int BASIC_PANEL = 0; + static final int ADVANCED_PANEL = 1; + + private static final String LOG_TAG = "Calculator"; + private static final boolean DEBUG = false; + private static final boolean LOG_ENABLED = DEBUG ? Config.LOGD : Config.LOGV; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.main); + + mPersist = new Persist(this); + mHistory = mPersist.history; + + mDisplay = (CalculatorDisplay) findViewById(R.id.display); + + mLogic = new Logic(this, mHistory, mDisplay, (Button) findViewById(R.id.equal)); + HistoryAdapter historyAdapter = new HistoryAdapter(this, mHistory, mLogic); + mHistory.setObserver(historyAdapter); + View view; + mPanelSwitcher = (PanelSwitcher) findViewById(R.id.panelswitch); + + mListener.setHandler(mLogic, mPanelSwitcher); + + mDisplay.setOnKeyListener(mListener); + + + if ((view = findViewById(R.id.del)) != null) { + view.setOnClickListener(mListener); + view.setOnLongClickListener(mListener); + } + /* + if ((view = findViewById(R.id.clear)) != null) { + view.setOnClickListener(mListener); + } + */ + + /* + ListView historyPad = (ListView) findViewById(R.id.historyPad); + if (historyPad != null) { + historyPad.setAdapter(historyAdapter); + } + */ + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuItem item; + + item = menu.add(0, CMD_CLEAR_HISTORY, 0, R.string.clear_history); + item.setIcon(R.drawable.clear_history); + + item = menu.add(0, CMD_ADVANCED_PANEL, 0, R.string.advanced); + item.setIcon(R.drawable.advanced); + + item = menu.add(0, CMD_BASIC_PANEL, 0, R.string.basic); + item.setIcon(R.drawable.simple); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.findItem(CMD_BASIC_PANEL).setVisible(mPanelSwitcher != null && + mPanelSwitcher.getCurrentIndex() == ADVANCED_PANEL); + + menu.findItem(CMD_ADVANCED_PANEL).setVisible(mPanelSwitcher != null && + mPanelSwitcher.getCurrentIndex() == BASIC_PANEL); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case CMD_CLEAR_HISTORY: + mHistory.clear(); + break; + + case CMD_BASIC_PANEL: + if (mPanelSwitcher != null && + mPanelSwitcher.getCurrentIndex() == ADVANCED_PANEL) { + mPanelSwitcher.moveRight(); + } + break; + + case CMD_ADVANCED_PANEL: + if (mPanelSwitcher != null && + mPanelSwitcher.getCurrentIndex() == BASIC_PANEL) { + mPanelSwitcher.moveLeft(); + } + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onSaveInstanceState(Bundle icicle) { + // as work-around for ClassCastException in TextView on restart + // avoid calling superclass, to keep icicle empty + } + + @Override + public void onPause() { + super.onPause(); + mLogic.updateHistory(); + mPersist.save(); + } + + static void log(String message) { + if (LOG_ENABLED) { + Log.v(LOG_TAG, message); + } + } +} diff --git a/src/com/android/calculator2/CalculatorDisplay.java b/src/com/android/calculator2/CalculatorDisplay.java new file mode 100644 index 0000000..5e0d76f --- /dev/null +++ b/src/com/android/calculator2/CalculatorDisplay.java @@ -0,0 +1,157 @@ +/* + * 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.calculator2; + +import android.content.Context; +import android.text.Editable; +import android.text.Spanned; +import android.text.method.NumberKeyListener; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.animation.TranslateAnimation; +import android.text.InputType; +import android.widget.EditText; +import android.widget.ViewSwitcher; +import android.graphics.Rect; + +import java.util.Map; + +/** + * Provides vertical scrolling for the input/result EditText. + */ +class CalculatorDisplay extends ViewSwitcher { + // only these chars are accepted from keyboard + private static final char[] ACCEPTED_CHARS = + "0123456789.+-*/\u2212\u00d7\u00f7()!%^".toCharArray(); + + private static final int ANIM_DURATION = 500; + enum Scroll { UP, DOWN, NONE } + + TranslateAnimation inAnimUp; + TranslateAnimation outAnimUp; + TranslateAnimation inAnimDown; + TranslateAnimation outAnimDown; + + public CalculatorDisplay(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void setLogic(Logic logic) { + NumberKeyListener calculatorKeyListener = + new NumberKeyListener() { + public int getInputType() { + // Don't display soft keyboard. + return InputType.TYPE_NULL; + } + + protected char[] getAcceptedChars() { + return ACCEPTED_CHARS; + } + + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + /* the EditText should still accept letters (eg. 'sin') + coming from the on-screen touch buttons, so don't filter anything. + */ + return null; + } + }; + + Editable.Factory factory = new CalculatorEditable.Factory(logic); + for (int i = 0; i < 2; ++i) { + EditText text = (EditText) getChildAt(i); + text.setBackgroundDrawable(null); + text.setEditableFactory(factory); + text.setKeyListener(calculatorKeyListener); + } + } + + @Override + public void setOnKeyListener(OnKeyListener l) { + getChildAt(0).setOnKeyListener(l); + getChildAt(1).setOnKeyListener(l); + } + + @Override + protected void onSizeChanged(int w, int h, int oldW, int oldH) { + inAnimUp = new TranslateAnimation(0, 0, h, 0); + inAnimUp.setDuration(ANIM_DURATION); + outAnimUp = new TranslateAnimation(0, 0, 0, -h); + outAnimUp.setDuration(ANIM_DURATION); + + inAnimDown = new TranslateAnimation(0, 0, -h, 0); + inAnimDown.setDuration(ANIM_DURATION); + outAnimDown = new TranslateAnimation(0, 0, 0, h); + outAnimDown.setDuration(ANIM_DURATION); + } + + void insert(String delta) { + EditText editor = (EditText) getCurrentView(); + int cursor = editor.getSelectionStart(); + editor.getText().insert(cursor, delta); + } + + EditText getEditText() { + return (EditText) getCurrentView(); + } + + Editable getText() { + EditText text = (EditText) getCurrentView(); + return text.getText(); + } + + void setText(CharSequence text, Scroll dir) { + if (getText().length() == 0) { + dir = Scroll.NONE; + } + + if (dir == Scroll.UP) { + setInAnimation(inAnimUp); + setOutAnimation(outAnimUp); + } else if (dir == Scroll.DOWN) { + setInAnimation(inAnimDown); + setOutAnimation(outAnimDown); + } else { // Scroll.NONE + setInAnimation(null); + setOutAnimation(null); + } + + EditText editText = (EditText) getNextView(); + editText.setText(text); + //Calculator.log("selection to " + text.length() + "; " + text); + editText.setSelection(text.length()); + showNext(); + } + + void setSelection(int i) { + EditText text = (EditText) getCurrentView(); + text.setSelection(i); + } + + int getSelectionStart() { + EditText text = (EditText) getCurrentView(); + return text.getSelectionStart(); + } + + @Override + protected void onFocusChanged(boolean gain, int direction, Rect prev) { + //Calculator.log("focus " + gain + "; " + direction + "; " + prev); + if (!gain) { + requestFocus(); + } + } +} diff --git a/src/com/android/calculator2/CalculatorEditable.java b/src/com/android/calculator2/CalculatorEditable.java new file mode 100644 index 0000000..60ba817 --- /dev/null +++ b/src/com/android/calculator2/CalculatorEditable.java @@ -0,0 +1,110 @@ +/* + * 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.calculator2; + +import android.text.SpannableStringBuilder; +import android.text.Editable; + +class CalculatorEditable extends SpannableStringBuilder { + private static final char[] ORIGINALS = {'-', '*', '/'}; + private static final char[] REPLACEMENTS = {'\u2212', '\u00d7', '\u00f7'}; + private boolean isInsideReplace = false; + private Logic mLogic; + + private CalculatorEditable(CharSequence source, Logic logic) { + super(source); + mLogic = logic; + } + + @Override + public SpannableStringBuilder + replace(int start, int end, CharSequence tb, int tbstart, int tbend) { + if (isInsideReplace) { + return super.replace(start, end, tb, tbstart, tbend); + } else { + isInsideReplace = true; + try { + String delta = tb.subSequence(tbstart, tbend).toString(); + return internalReplace(start, end, delta); + } finally { + isInsideReplace = false; + } + } + } + + private SpannableStringBuilder internalReplace(int start, int end, String delta) { + if (!mLogic.acceptInsert(delta)) { + mLogic.cleared(); + start = 0; + end = length(); + } + + for (int i = ORIGINALS.length - 1; i >= 0; --i) { + delta = delta.replace(ORIGINALS[i], REPLACEMENTS[i]); + } + + int length = delta.length(); + if (length == 1) { + char text = delta.charAt(0); + + //don't allow leading operator + * / + if (start == 0 && Logic.isOperator(text) && text != Logic.MINUS) { + return super.replace(start, end, ""); + } + + //don't allow two dots in the same number + if (text == '.') { + int p = start - 1; + while (p >= 0 && Character.isDigit(charAt(p))) { + --p; + } + if (p >= 0 && charAt(p) == '.') { + return super.replace(start, end, ""); + } + } + + char prevChar = start > 0 ? charAt(start-1) : '\0'; + + //don't allow 2 successive minuses + if (text == Logic.MINUS && prevChar == Logic.MINUS) { + return super.replace(start, end, ""); + } + + //don't allow multiple successive operators + if (Logic.isOperator(text)) { + while (Logic.isOperator(prevChar) && + (text != Logic.MINUS || prevChar == '+')) { + --start; + prevChar = start > 0 ? charAt(start-1) : '\0'; + } + } + } + return super.replace(start, end, delta); + } + + public static class Factory extends Editable.Factory { + private Logic mLogic; + + public Factory(Logic logic) { + mLogic = logic; + } + + public Editable newEditable(CharSequence source) { + return new CalculatorEditable(source, mLogic); + } + } +} diff --git a/src/com/android/calculator2/ColorButton.java b/src/com/android/calculator2/ColorButton.java new file mode 100644 index 0000000..5d78446 --- /dev/null +++ b/src/com/android/calculator2/ColorButton.java @@ -0,0 +1,147 @@ +/* + * 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.calculator2; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.Button; +import android.view.View.OnClickListener; +import android.view.View; +import android.view.MotionEvent; +import android.content.res.Resources; + +import java.util.Map; + +/** + * Button with click-animation effect. + */ +class ColorButton extends Button implements OnClickListener { + int CLICK_FEEDBACK_COLOR; + static final int CLICK_FEEDBACK_INTERVAL = 10; + static final int CLICK_FEEDBACK_DURATION = 350; + + Drawable mButtonBackground; + Drawable mButton; + float mTextX; + float mTextY; + long mAnimStart; + OnClickListener mListener; + + public ColorButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + mListener = ((Calculator) context).mListener; + setOnClickListener(this); + } + + public void onClick(View view) { + animateClickFeedback(); + mListener.onClick(this); + } + + private void init() { + setBackgroundDrawable(null); + + Resources res = getResources(); + + mButtonBackground = res.getDrawable(R.drawable.button_bg); + mButton = res.getDrawable(R.drawable.button); + CLICK_FEEDBACK_COLOR = res.getColor(R.color.magic_flame); + getPaint().setColor(res.getColor(R.color.button_text)); + + mAnimStart = -1; + } + + + @Override + public void onSizeChanged(int w, int h, int oldW, int oldH) { + int selfW = mButton.getIntrinsicWidth(); + int selfH = mButton.getIntrinsicHeight(); + int marginX = (w - selfW) / 2; + int marginY = (h - selfH) / 2; + mButtonBackground.setBounds(marginX, marginY, marginX + selfW, marginY + selfH); + mButton.setBounds(marginX, marginY, marginX + selfW, marginY + selfH); + measureText(); + } + + private void measureText() { + Paint paint = getPaint(); + mTextX = (getWidth() - paint.measureText(getText().toString())) / 2; + mTextY = (getHeight() - paint.ascent() - paint.descent()) / 2; + } + + @Override + protected void onTextChanged(CharSequence text, int start, int before, int after) { + measureText(); + } + + private void drawMagicFlame(int duration, Canvas canvas) { + int alpha = 255 - 255 * duration / CLICK_FEEDBACK_DURATION; + int color = CLICK_FEEDBACK_COLOR | (alpha << 24); + mButtonBackground.setColorFilter(color, PorterDuff.Mode.SRC_IN); + + int cx = getWidth() / 2; + int cy = getHeight() / 2; + float angle = 250.0f * duration / CLICK_FEEDBACK_DURATION; + canvas.rotate(angle, cx, cy); + mButtonBackground.draw(canvas); + canvas.rotate(-angle, cx, cy); + } + + @Override + public void onDraw(Canvas canvas) { + if (mAnimStart != -1) { + int animDuration = (int) (System.currentTimeMillis() - mAnimStart); + + if (animDuration >= CLICK_FEEDBACK_DURATION) { + mButtonBackground.clearColorFilter(); + mAnimStart = -1; + } else { + drawMagicFlame(animDuration, canvas); + postInvalidateDelayed(CLICK_FEEDBACK_INTERVAL); + } + } else if (isPressed()) { + drawMagicFlame(0, canvas); + } + + mButton.draw(canvas); + + CharSequence text = getText(); + canvas.drawText(text, 0, text.length(), mTextX, mTextY, getPaint()); + } + + public void animateClickFeedback() { + mAnimStart = System.currentTimeMillis(); + invalidate(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int a = event.getAction(); + if (a == MotionEvent.ACTION_DOWN + || a == MotionEvent.ACTION_CANCEL + || a == MotionEvent.ACTION_UP) { + invalidate(); + } + return super.onTouchEvent(event); + } +} diff --git a/src/com/android/calculator2/EventListener.java b/src/com/android/calculator2/EventListener.java new file mode 100644 index 0000000..0274a8b --- /dev/null +++ b/src/com/android/calculator2/EventListener.java @@ -0,0 +1,129 @@ +/* + * 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.calculator2; + +import android.view.View; +import android.view.KeyEvent; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.View.OnKeyListener; +import android.widget.Button; + +class EventListener implements View.OnKeyListener, + View.OnClickListener, + View.OnLongClickListener { + Logic mHandler; + PanelSwitcher mPanelSwitcher; + + void setHandler(Logic handler, PanelSwitcher panelSwitcher) { + mHandler = handler; + mPanelSwitcher = panelSwitcher; + } + + //@Override + public void onClick(View view) { + int id = view.getId(); + switch (id) { + case R.id.del: + mHandler.onDelete(); + break; + + case R.id.equal: + mHandler.onEnter(); + break; + + /* + case R.id.clear: + mHandler.onClear(); + 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 (mPanelSwitcher != null && + mPanelSwitcher.getCurrentIndex() == Calculator.ADVANCED_PANEL) { + mPanelSwitcher.moveRight(); + } + } + } + } + + //@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 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER && + keyCode != KeyEvent.KEYCODE_DPAD_UP && + keyCode != KeyEvent.KEYCODE_DPAD_DOWN && + keyCode != KeyEvent.KEYCODE_ENTER) { + 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; + } +} diff --git a/src/com/android/calculator2/History.java b/src/com/android/calculator2/History.java new file mode 100644 index 0000000..ff2cc65 --- /dev/null +++ b/src/com/android/calculator2/History.java @@ -0,0 +1,118 @@ +/* + * 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.calculator2; + +import android.widget.BaseAdapter; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Vector; + +class History { + private static final int VERSION_1 = 1; + private static final int MAX_ENTRIES = 100; + Vector<HistoryEntry> mEntries = new Vector<HistoryEntry>(); + int mPos; + BaseAdapter mObserver; + + History() { + clear(); + } + + History(int version, DataInput in) throws IOException { + if (version >= VERSION_1) { + int size = in.readInt(); + for (int i = 0; i < size; ++i) { + mEntries.add(new HistoryEntry(version, in)); + } + mPos = in.readInt(); + } else { + throw new IOException("invalid version " + version); + } + } + + void setObserver(BaseAdapter observer) { + mObserver = observer; + } + + private void notifyChanged() { + if (mObserver != null) { + mObserver.notifyDataSetChanged(); + } + } + + void clear() { + mEntries.clear(); + mEntries.add(new HistoryEntry("")); + mPos = 0; + notifyChanged(); + } + + void write(DataOutput out) throws IOException { + out.writeInt(mEntries.size()); + for (HistoryEntry entry : mEntries) { + entry.write(out); + } + out.writeInt(mPos); + } + + void update(String text) { + current().setEdited(text); + } + + boolean moveToPrevious() { + if (mPos > 0) { + --mPos; + return true; + } + return false; + } + + boolean moveToNext() { + if (mPos < mEntries.size() - 1) { + ++mPos; + return true; + } + return false; + } + + 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(); + } + + HistoryEntry current() { + return mEntries.elementAt(mPos); + } + + String getText() { + return current().getEdited(); + } + + String getBase() { + return current().getBase(); + } +} diff --git a/src/com/android/calculator2/HistoryAdapter.java b/src/com/android/calculator2/HistoryAdapter.java new file mode 100644 index 0000000..02ceeee --- /dev/null +++ b/src/com/android/calculator2/HistoryAdapter.java @@ -0,0 +1,87 @@ +/* + * 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.calculator2; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.view.View; +import android.content.Context; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.Vector; + +import org.javia.arity.SyntaxException; + +class HistoryAdapter extends BaseAdapter { + private Vector<HistoryEntry> mEntries; + private LayoutInflater mInflater; + private Logic mEval; + + HistoryAdapter(Context context, History history, Logic evaluator) { + mEntries = history.mEntries; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mEval = evaluator; + } + + // @Override + public int getCount() { + return mEntries.size() - 1; + } + + // @Override + public Object getItem(int position) { + return mEntries.elementAt(position); + } + + // @Override + public long getItemId(int position) { + return position; + } + + @Override + public boolean hasStableIds() { + return true; + } + + // @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + if (convertView == null) { + view = mInflater.inflate(R.layout.history_item, parent, false); + } else { + view = convertView; + } + + TextView expr = (TextView) view.findViewById(R.id.historyExpr); + TextView result = (TextView) view.findViewById(R.id.historyResult); + + HistoryEntry entry = mEntries.elementAt(position); + String base = entry.getBase(); + expr.setText(entry.getBase()); + + try { + String res = mEval.evaluate(base); + result.setText("= " + res); + } catch (SyntaxException e) { + result.setText(""); + } + + return view; + } +} + diff --git a/src/com/android/calculator2/HistoryEntry.java b/src/com/android/calculator2/HistoryEntry.java new file mode 100644 index 0000000..80319d8 --- /dev/null +++ b/src/com/android/calculator2/HistoryEntry.java @@ -0,0 +1,69 @@ +/* + * 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.calculator2; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +class HistoryEntry { + private static final int VERSION_1 = 1; + private String mBase; + private String mEdited; + + HistoryEntry(String str) { + mBase = str; + clearEdited(); + } + + HistoryEntry(int version, DataInput in) throws IOException { + if (version >= VERSION_1) { + mBase = in.readUTF(); + mEdited = in.readUTF(); + //Calculator.log("load " + mEdited); + } else { + throw new IOException("invalid version " + version); + } + } + + void write(DataOutput out) throws IOException { + out.writeUTF(mBase); + out.writeUTF(mEdited); + //Calculator.log("save " + mEdited); + } + + @Override + public String toString() { + return mBase; + } + + void clearEdited() { + mEdited = mBase; + } + + String getEdited() { + return mEdited; + } + + void setEdited(String edited) { + mEdited = edited; + } + + String getBase() { + return mBase; + } +} diff --git a/src/com/android/calculator2/Logic.java b/src/com/android/calculator2/Logic.java new file mode 100644 index 0000000..244f438 --- /dev/null +++ b/src/com/android/calculator2/Logic.java @@ -0,0 +1,208 @@ +/* + * 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.calculator2; + +import android.view.View; +import android.view.KeyEvent; +import android.widget.Button; +import android.widget.EditText; +import android.content.Context; +import android.content.res.Configuration; + +import org.javia.arity.Symbols; +import org.javia.arity.SyntaxException; +import org.javia.arity.Util; + +class Logic { + private CalculatorDisplay mDisplay; + private Symbols mSymbols = new Symbols(); + private History mHistory; + private String mResult = ""; + private Button mEqualButton; + private final String mEnterString; + private boolean mIsError = false; + private final boolean mOrientationPortrait; + private final int mLineLength; + + private static final int LINE_LENGTH_PORTRAIT = 14; + private static final int LINE_LENGTH_LANDSCAPE = 21; + private static final String INFINITY_UNICODE = "\u221e"; + + // the two strings below are the result of Double.toString() for Infinity & NaN + // they are not output to the user and don't require internationalization + private static final String INFINITY = "Infinity"; + private static final String NAN = "NaN"; + + static final char MINUS = '\u2212'; + + private final String mErrorString; + + Logic(Context context, History history, CalculatorDisplay display, Button equalButton) { + mErrorString = context.getResources().getString(R.string.error); + mOrientationPortrait = context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT; + mLineLength = mOrientationPortrait ? LINE_LENGTH_PORTRAIT : LINE_LENGTH_LANDSCAPE; + + try { + // in calculator we use log() for base-10, + // unlike in arity-lib where log() is natural logarithm + mSymbols.define(mSymbols.compileWithName("log(x)=log10(x)")); + } catch (SyntaxException e) { + throw new Error("" + e); //never + } + mHistory = history; + mDisplay = display; + mDisplay.setLogic(this); + mEqualButton = equalButton; + mEnterString = context.getText(R.string.enter).toString(); + + clearWithHistory(false); + } + + boolean eatHorizontalMove(boolean toLeft) { + EditText editText = mDisplay.getEditText(); + int cursorPos = editText.getSelectionStart(); + return toLeft ? cursorPos == 0 : cursorPos >= editText.length(); + } + + private String getText() { + return mDisplay.getText().toString(); + } + + void insert(String delta) { + mDisplay.insert(delta); + } + + private void setText(CharSequence text) { + mDisplay.setText(text, CalculatorDisplay.Scroll.UP); + } + + private void clearWithHistory(boolean scroll) { + mDisplay.setText(mHistory.getText(), + scroll ? CalculatorDisplay.Scroll.UP : CalculatorDisplay.Scroll.NONE); + mResult = ""; + mIsError = false; + } + + private void clear(boolean scroll) { + mDisplay.setText("", scroll ? CalculatorDisplay.Scroll.UP : CalculatorDisplay.Scroll.NONE); + cleared(); + } + + void cleared() { + mResult = ""; + mIsError = false; + updateHistory(); + } + + boolean acceptInsert(String delta) { + String text = getText(); + return !mIsError && + (!mResult.equals(text) || + isOperator(delta) || + mDisplay.getSelectionStart() != text.length()); + } + + void onDelete() { + if (getText().equals(mResult) || mIsError) { + clear(false); + } else { + mDisplay.dispatchKeyEvent(new KeyEvent(0, KeyEvent.KEYCODE_DEL)); + mResult = ""; + } + } + + void onClear() { + clear(false); + } + + void onEnter() { + String text = getText(); + if (text.equals(mResult)) { + clearWithHistory(false); //clear after an Enter on result + } else { + mHistory.enter(text); + try { + mResult = evaluate(text); + } catch (SyntaxException e) { + mIsError = true; + mResult = mErrorString; + } + if (text.equals(mResult)) { + //no need to show result, it is exactly what the user entered + clearWithHistory(true); + } else { + setText(mResult); + //mEqualButton.setText(mEnterString); + } + } + } + + void onUp() { + String text = getText(); + if (!text.equals(mResult)) { + mHistory.update(text); + } + if (mHistory.moveToPrevious()) { + mDisplay.setText(mHistory.getText(), CalculatorDisplay.Scroll.DOWN); + } + } + + void onDown() { + String text = getText(); + if (!text.equals(mResult)) { + mHistory.update(text); + } + if (mHistory.moveToNext()) { + mDisplay.setText(mHistory.getText(), CalculatorDisplay.Scroll.UP); + } + } + + void updateHistory() { + mHistory.update(getText()); + } + + private static final int ROUND_DIGITS = 1; + 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; + } + + String result = Util.doubleToString(mSymbols.eval(input), mLineLength, ROUND_DIGITS); + if (result.equals(NAN)) { // treat NaN as Error + mIsError = true; + return mErrorString; + } + return result.replace('-', MINUS).replace(INFINITY, INFINITY_UNICODE); + } + + static boolean isOperator(String text) { + return text.length() == 1 && isOperator(text.charAt(0)); + } + + static boolean isOperator(char c) { + //plus minus times div + return "+\u2212\u00d7\u00f7/*".indexOf(c) != -1; + } +} diff --git a/src/com/android/calculator2/PanelSwitcher.java b/src/com/android/calculator2/PanelSwitcher.java new file mode 100644 index 0000000..c64022c --- /dev/null +++ b/src/com/android/calculator2/PanelSwitcher.java @@ -0,0 +1,140 @@ +/* + * 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.calculator2; + +import android.view.animation.TranslateAnimation; +import android.view.MotionEvent; +import android.view.View; +import android.view.GestureDetector; +import android.widget.FrameLayout; +import android.content.Context; +import android.util.AttributeSet; +import android.os.Handler; + +import java.util.Map; + +class PanelSwitcher extends FrameLayout { + private static final int MAJOR_MOVE = 60; + private static final int ANIM_DURATION = 400; + + private GestureDetector mGestureDetector; + private int mCurrentView; + private View mChild, mHistoryView; + private View children[]; + + private int mWidth; + private TranslateAnimation inLeft; + private TranslateAnimation outLeft; + + private TranslateAnimation inRight; + private TranslateAnimation outRight; + + private static final int NONE = 1; + private static final int LEFT = 2; + private static final int RIGHT = 3; + private int mPreviousMove; + + public PanelSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + mCurrentView = 0; + mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + int dx = (int) (e2.getX() - e1.getX()); + + // don't accept the fling if it's too short + // as it may conflict with a button push + if (Math.abs(dx) > MAJOR_MOVE && Math.abs(velocityX) > Math.abs(velocityY)) { + if (velocityX > 0) { + moveRight(); + } else { + moveLeft(); + } + return true; + } else { + return false; + } + } + }); + } + + @Override + public void onSizeChanged(int w, int h, int oldW, int oldH) { + mWidth = w; + inLeft = new TranslateAnimation(mWidth, 0, 0, 0); + outLeft = new TranslateAnimation(0, -mWidth, 0, 0); + inRight = new TranslateAnimation(-mWidth, 0, 0, 0); + outRight = new TranslateAnimation(0, mWidth, 0, 0); + + inLeft.setDuration(ANIM_DURATION); + outLeft.setDuration(ANIM_DURATION); + inRight.setDuration(ANIM_DURATION); + outRight.setDuration(ANIM_DURATION); + } + + protected void onFinishInflate() { + int count = getChildCount(); + children = new View[count]; + for (int i = 0; i < count; ++i) { + children[i] = getChildAt(i); + if (i != mCurrentView) { + children[i].setVisibility(View.GONE); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + return true; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + + void moveLeft() { + // <-- + if (mCurrentView < children.length - 1 && mPreviousMove != LEFT) { + children[mCurrentView+1].setVisibility(View.VISIBLE); + children[mCurrentView+1].startAnimation(inLeft); + children[mCurrentView].startAnimation(outLeft); + children[mCurrentView].setVisibility(View.GONE); + + mCurrentView++; + mPreviousMove = LEFT; + } + } + + void moveRight() { + // --> + if (mCurrentView > 0 && mPreviousMove != RIGHT) { + children[mCurrentView-1].setVisibility(View.VISIBLE); + children[mCurrentView-1].startAnimation(inRight); + children[mCurrentView].startAnimation(outRight); + children[mCurrentView].setVisibility(View.GONE); + + mCurrentView--; + mPreviousMove = RIGHT; + } + } + + int getCurrentIndex() { + return mCurrentView; + } +} diff --git a/src/com/android/calculator2/Persist.java b/src/com/android/calculator2/Persist.java new file mode 100644 index 0000000..454f10b --- /dev/null +++ b/src/com/android/calculator2/Persist.java @@ -0,0 +1,70 @@ +/* + * 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.calculator2; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import android.content.Context; + +class Persist { + private static final int LAST_VERSION = 1; + private static final String FILE_NAME = "calculator.data"; + private Context mContext; + + History history = new History(); + + Persist(Context context) { + this.mContext = context; + load(); + } + + private void load() { + try { + InputStream is = new BufferedInputStream(mContext.openFileInput(FILE_NAME), 8192); + DataInputStream in = new DataInputStream(is); + int version = in.readInt(); + if (version > LAST_VERSION) { + throw new IOException("data version " + version + "; expected " + LAST_VERSION); + } + history = new History(version, in); + in.close(); + } catch (FileNotFoundException e) { + Calculator.log("" + e); + } catch (IOException e) { + Calculator.log("" + e); + } + } + + void save() { + try { + OutputStream os = new BufferedOutputStream(mContext.openFileOutput(FILE_NAME, 0), 8192); + DataOutputStream out = new DataOutputStream(os); + out.writeInt(LAST_VERSION); + history.write(out); + out.close(); + } catch (IOException e) { + Calculator.log("" + e); + } + } +} |