blob: f5b92471ca7bdeca446707a0adb06b594560ae5b [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070017// FIXME: Menu handling, particularly for cut/paste, is very ugly
18// and not the way it was intended.
19// Other menus are not handled brilliantly either.
20// TODO: Revisit handling of "Help" menu, so that it's more consistent
21// with our conventions.
22// TODO: See if we can make scrolling look better, especially on small
23// displays. Fix evaluation interface so the evaluator returns entire
24// result, and formatting of exponent etc. is done separately.
25// TODO: Better indication of when the result is known to be exact.
Hans Boehm84614952014-11-25 18:46:17 -080026// TODO: Fix placement of inverse trig buttons.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070027// TODO: Fix internationalization, particularly for result.
28// TODO: Check and possibly fix accessability issues.
29// TODO: Copy & more general paste in formula?
Hans Boehm84614952014-11-25 18:46:17 -080030
Justin Klaassen4b3af052014-05-27 17:53:10 -070031package com.android.calculator2;
32
33import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070034import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070035import android.animation.AnimatorListenerAdapter;
36import android.animation.AnimatorSet;
37import android.animation.ArgbEvaluator;
38import android.animation.ObjectAnimator;
39import android.animation.ValueAnimator;
40import android.animation.ValueAnimator.AnimatorUpdateListener;
41import android.app.Activity;
Hans Boehm84614952014-11-25 18:46:17 -080042import android.app.AlertDialog;
43import android.content.Context;
44import android.content.DialogInterface;
Hans Boehmbfe8c222015-04-02 16:26:07 -070045import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070046import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070047import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070048import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070049import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070050import android.support.annotation.NonNull;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010051import android.support.v4.view.ViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -070052import android.text.Editable;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070053import android.text.SpannableString;
54import android.text.Spanned;
Justin Klaassen4b3af052014-05-27 17:53:10 -070055import android.text.TextUtils;
56import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070057import android.text.style.ForegroundColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080058import android.util.Log;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070059import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070060import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080061import android.view.Menu;
62import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070063import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070064import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070065import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070066import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070067import android.view.ViewGroupOverlay;
Justin Klaassen4b3af052014-05-27 17:53:10 -070068import android.view.animation.AccelerateDecelerateInterpolator;
Hans Boehm84614952014-11-25 18:46:17 -080069import android.webkit.WebView;
Justin Klaassen4b3af052014-05-27 17:53:10 -070070import android.widget.Button;
Hans Boehm84614952014-11-25 18:46:17 -080071import android.widget.PopupMenu;
72import android.widget.PopupMenu.OnMenuItemClickListener;
Justin Klaassenfed941a2014-06-09 18:42:40 +010073import android.widget.TextView;
74
75import com.android.calculator2.CalculatorEditText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080076
77import java.io.ByteArrayInputStream;
78import java.io.ObjectInputStream;
79import java.io.ByteArrayOutputStream;
80import java.io.ObjectOutputStream;
81import java.io.ObjectInput;
82import java.io.ObjectOutput;
83import java.io.IOException;
84import java.text.DecimalFormatSymbols; // TODO: May eventually not need this here.
Justin Klaassen4b3af052014-05-27 17:53:10 -070085
Justin Klaassen04f79c72014-06-27 17:25:35 -070086public class Calculator extends Activity
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070087 implements OnTextSizeChangeListener, OnLongClickListener, OnMenuItemClickListener, CalculatorEditText.PasteListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070088
89 /**
90 * Constant for an invalid resource id.
91 */
92 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070093
94 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080095 INPUT, // Result and formula both visible, no evaluation requested,
96 // Though result may be visible on bottom line.
97 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
98 INIT, // Very temporary state used as alternative to EVALUATE
99 // during reinitialization. Do not animate on completion.
100 ANIMATE, // Result computed, animation to enlarge result window in progress.
101 RESULT, // Result displayed, formula invisible.
102 // If we are in RESULT state, the formula was evaluated without
103 // error to initial precision.
104 ERROR // Error displayed: Formula visible, result shows error message.
105 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700106 }
Hans Boehm84614952014-11-25 18:46:17 -0800107 // Normal transition sequence is
108 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
109 // A RESULT -> ERROR transition is possible in rare corner cases, in which
110 // a higher precision evaluation exposes an error. This is possible, since we
111 // initially evaluate assuming we were given a well-defined problem. If we
112 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
113 // unless we are asked for enough precision that we can distinguish the argument from zero.
114 // TODO: Consider further heuristics to reduce the chance of observing this?
115 // It already seems to be observable only in contrived cases.
116 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
117 // is restarted in that state. This leads us to recompute and redisplay the result
118 // ASAP.
119 // TODO: Possibly save a bit more information, e.g. its initial display string
120 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700121
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700122 // We currently assume that the formula does not change out from under us in
123 // any way. We explicitly handle all input to the formula here.
124 // TODO: Perhaps the formula should not be editable at all?
Justin Klaassen4b3af052014-05-27 17:53:10 -0700125
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700126 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
127 @Override
128 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700129 if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700130 switch (keyCode) {
131 case KeyEvent.KEYCODE_NUMPAD_ENTER:
132 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700133 case KeyEvent.KEYCODE_DPAD_CENTER:
134 mCurrentButton = mEqualButton;
135 onEquals();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700136 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700137 case KeyEvent.KEYCODE_DEL:
138 mCurrentButton = mDeleteButton;
139 onDelete();
140 return true;
141 default:
142 final int raw = keyEvent.getKeyCharacterMap()
143 .get(keyCode, keyEvent.getMetaState());
144 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
145 return true; // discard
146 }
147 // Try to discard non-printing characters and the like.
148 // The user will have to explicitly delete other junk that gets past us.
149 if (Character.isIdentifierIgnorable(raw)
150 || Character.isWhitespace(raw)) {
151 return true;
152 }
153 char c = (char)raw;
154 if (c == '=') {
155 onEquals();
156 } else {
157 addChars(String.valueOf(c));
158 redisplayAfterFormulaChange();
159 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700160 }
161 return false;
162 }
163 };
164
Hans Boehm84614952014-11-25 18:46:17 -0800165 private static final String NAME = Calculator.class.getName();
166 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
167 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
168 // Associated value is a byte array holding both mCalculatorState
169 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700170
Justin Klaassen4b3af052014-05-27 17:53:10 -0700171 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800172 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700173
Justin Klaassen06360f92014-08-28 11:08:44 -0700174 private View mDisplayView;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700175 private CalculatorEditText mFormulaEditText;
Hans Boehm84614952014-11-25 18:46:17 -0800176 private CalculatorResult mResult;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700177 private TextView mDegRadDisplay;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100178 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700179 private View mDeleteButton;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700180 private View mEqualButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700181 private View mClearButton;
Hans Boehm84614952014-11-25 18:46:17 -0800182 private View mOverflowMenuButton;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700183 private Button mDegRadButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700184
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700185 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700186 private Animator mCurrentAnimator;
187
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700188 private String mUnprocessedChars = null; // Characters that were recently entered
189 // at the end of the display that have not yet
190 // been added to the underlying expression.
191
Justin Klaassen4b3af052014-05-27 17:53:10 -0700192 @Override
193 protected void onCreate(Bundle savedInstanceState) {
194 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700195 setContentView(R.layout.activity_calculator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700196
Justin Klaassen06360f92014-08-28 11:08:44 -0700197 mDisplayView = findViewById(R.id.display);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700198 mFormulaEditText = (CalculatorEditText) findViewById(R.id.formula);
Hans Boehm84614952014-11-25 18:46:17 -0800199 mResult = (CalculatorResult) findViewById(R.id.result);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700200 mDegRadDisplay = (TextView) findViewById(R.id.deg_rad);
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100201 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700202 mDeleteButton = findViewById(R.id.del);
203 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700204 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
205 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
206 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
207 }
Hans Boehm84614952014-11-25 18:46:17 -0800208 mOverflowMenuButton = findViewById(R.id.overflow_menu);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700209 mDegRadButton = (Button)findViewById(R.id.mode_deg_rad);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700210
Hans Boehm84614952014-11-25 18:46:17 -0800211 mEvaluator = new Evaluator(this, mResult);
212 mResult.setEvaluator(mEvaluator);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700213
Hans Boehm84614952014-11-25 18:46:17 -0800214 if (savedInstanceState != null) {
215 setState(CalculatorState.values()[
216 savedInstanceState.getInt(KEY_DISPLAY_STATE,
217 CalculatorState.INPUT.ordinal())]);
218 byte[] state =
219 savedInstanceState.getByteArray(KEY_EVAL_STATE);
220 if (state != null) {
221 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
222 mEvaluator.restoreInstanceState(in);
223 } catch (Throwable ignored) {
224 // When in doubt, revert to clean state
225 mCurrentState = CalculatorState.INPUT;
226 mEvaluator.clear();
227 }
228 }
229 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700230 mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener);
Justin Klaassenfed941a2014-06-09 18:42:40 +0100231 mFormulaEditText.setOnTextSizeChangeListener(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700232 mFormulaEditText.setPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700233 mDeleteButton.setOnLongClickListener(this);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700234 updateDegreeMode(mEvaluator.getDegreeMode());
Hans Boehm84614952014-11-25 18:46:17 -0800235 if (mCurrentState == CalculatorState.EVALUATE) {
236 // Odd case. Evaluation probably took a long time. Let user ask for it again
237 mCurrentState = CalculatorState.INPUT;
238 // TODO: This can happen if the user rotates the screen.
239 // Is this rotate-to-abort behavior correct? Revisit after experimentation.
240 }
241 if (mCurrentState != CalculatorState.INPUT) {
242 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800243 mEvaluator.requireResult();
244 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700245 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800246 }
247 // TODO: We're currently not saving and restoring scroll position.
248 // We probably should. Details may require care to deal with:
249 // - new display size
250 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700251 }
252
253 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700254 protected void onSaveInstanceState(@NonNull Bundle outState) {
255 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
256 if (mCurrentAnimator != null) {
257 mCurrentAnimator.cancel();
258 }
259
Justin Klaassen4b3af052014-05-27 17:53:10 -0700260 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800261 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
262 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
263 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
264 mEvaluator.saveInstanceState(out);
265 } catch (IOException e) {
266 // Impossible; No IO involved.
267 throw new AssertionError("Impossible IO exception", e);
268 }
269 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700270 }
271
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700272 // Set the state, updating delete label and display colors.
273 // This restores display positions on moving to INPUT.
274 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700275 private void setState(CalculatorState state) {
276 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800277 if (state == CalculatorState.INPUT) {
278 restoreDisplayPositions();
279 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700280 mCurrentState = state;
281
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700282 if (mCurrentState == CalculatorState.RESULT) {
283 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700284 mDeleteButton.setVisibility(View.GONE);
285 mClearButton.setVisibility(View.VISIBLE);
286 } else {
287 mDeleteButton.setVisibility(View.VISIBLE);
288 mClearButton.setVisibility(View.GONE);
289 }
290
Hans Boehm84614952014-11-25 18:46:17 -0800291 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700292 final int errorColor = getResources().getColor(R.color.calculator_error_color);
293 mFormulaEditText.setTextColor(errorColor);
Hans Boehm84614952014-11-25 18:46:17 -0800294 mResult.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700295 getWindow().setStatusBarColor(errorColor);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700296 } else {
297 mFormulaEditText.setTextColor(
298 getResources().getColor(R.color.display_formula_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800299 mResult.setTextColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700300 getResources().getColor(R.color.display_result_text_color));
Justin Klaassen8fff1442014-06-19 10:43:29 -0700301 getWindow().setStatusBarColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700302 getResources().getColor(R.color.calculator_accent_color));
303 }
304 }
305 }
306
307 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100308 public void onBackPressed() {
309 if (mPadViewPager == null || mPadViewPager.getCurrentItem() == 0) {
310 // If the user is currently looking at the first pad (or the pad is not paged),
311 // allow the system to handle the Back button.
312 super.onBackPressed();
313 } else {
314 // Otherwise, select the previous pad.
315 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
316 }
317 }
318
319 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700320 public void onUserInteraction() {
321 super.onUserInteraction();
322
323 // If there's an animation in progress, cancel it so the user interaction can be handled
324 // immediately.
325 if (mCurrentAnimator != null) {
326 mCurrentAnimator.cancel();
327 }
328 }
329
Hans Boehmbfe8c222015-04-02 16:26:07 -0700330 // Update the top corner degree/radian display and mode button
331 // to reflect the indicated current degree mode (true = degrees)
332 // TODO: Hide the top corner display until the advanced panel is exposed.
333 private void updateDegreeMode(boolean dm) {
334 Resources res = getResources();
335 String descr;
336 if (dm) {
337 mDegRadDisplay.setText(R.string.mode_deg);
338 mDegRadButton.setText(R.string.mode_rad);
339 mDegRadButton.setContentDescription(res.getString(R.string.desc_mode_rad));
340 } else {
341 mDegRadDisplay.setText(R.string.mode_rad);
342 mDegRadButton.setText(R.string.mode_deg);
343 mDegRadButton.setContentDescription(res.getString(R.string.desc_mode_deg));
344 }
345 }
Hans Boehm84614952014-11-25 18:46:17 -0800346
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700347 // Add the given button id to input expression.
348 // If appropriate, clear the expression before doing so.
349 private void addKeyToExpr(int id) {
350 if (mCurrentState == CalculatorState.ERROR) {
351 setState(CalculatorState.INPUT);
352 } else if (mCurrentState == CalculatorState.RESULT) {
353 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
354 mEvaluator.collapse();
355 } else {
356 mEvaluator.clear();
357 }
358 setState(CalculatorState.INPUT);
359 }
360 if (!mEvaluator.append(id)) {
361 // TODO: Some user visible feedback?
362 }
363 }
364
365 private void redisplayAfterFormulaChange() {
366 // TODO: Could do this more incrementally.
367 redisplayFormula();
368 setState(CalculatorState.INPUT);
369 mResult.clear();
370 mEvaluator.evaluateAndShowResult();
371 }
372
Justin Klaassen4b3af052014-05-27 17:53:10 -0700373 public void onButtonClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700374 mCurrentButton = view;
Hans Boehm84614952014-11-25 18:46:17 -0800375 int id = view.getId();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700376
Hans Boehm84614952014-11-25 18:46:17 -0800377 // Always cancel in-progress evaluation.
378 // If we were waiting for the result, do nothing else.
379 mEvaluator.cancelAll();
380 if (mCurrentState == CalculatorState.EVALUATE
381 || mCurrentState == CalculatorState.ANIMATE) {
382 onCancelled();
383 return;
384 }
385 switch (id) {
386 case R.id.overflow_menu:
387 PopupMenu menu = constructPopupMenu();
388 if (menu != null) {
389 menu.show();
390 }
391 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700392 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700393 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700394 break;
395 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700396 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700397 break;
398 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700399 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700400 break;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700401 case R.id.mode_deg_rad:
402 boolean mode = !mEvaluator.getDegreeMode();
403 updateDegreeMode(mode);
404 if (mCurrentState == CalculatorState.RESULT) {
405 mEvaluator.collapse(); // Capture result evaluated in old mode
406 redisplayFormula();
407 }
408 // In input mode, we reinterpret already entered trig functions.
409 mEvaluator.setDegreeMode(mode);
410 setState(CalculatorState.INPUT);
411 mResult.clear();
412 mEvaluator.evaluateAndShowResult();
413 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700414 default:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700415 addKeyToExpr(id);
416 redisplayAfterFormulaChange();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700417 break;
418 }
419 }
420
Hans Boehm84614952014-11-25 18:46:17 -0800421 void redisplayFormula() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700422 String formula = mEvaluator.getExpr().toString(this);
423 if (mUnprocessedChars != null) {
424 // Add and highlight characters we couldn't process.
425 SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
426 // TODO: should probably match this to the error color.
427 formatted.setSpan(new ForegroundColorSpan(Color.RED),
428 formula.length(), formatted.length(),
429 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
430 mFormulaEditText.setText(formatted);
431 } else {
432 mFormulaEditText.setText(formula);
433 }
Hans Boehm84614952014-11-25 18:46:17 -0800434 }
435
Justin Klaassen4b3af052014-05-27 17:53:10 -0700436 @Override
437 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700438 mCurrentButton = view;
439
Justin Klaassen4b3af052014-05-27 17:53:10 -0700440 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700441 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700442 return true;
443 }
444 return false;
445 }
446
Hans Boehm84614952014-11-25 18:46:17 -0800447 // Initial evaluation completed successfully. Initiate display.
448 public void onEvaluate(int initDisplayPrec, String truncatedWholeNumber) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700449 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehm84614952014-11-25 18:46:17 -0800450 // Just update small result display.
451 mResult.displayResult(initDisplayPrec, truncatedWholeNumber);
452 } else { // in EVALUATE or INIT state
453 mResult.displayResult(initDisplayPrec, truncatedWholeNumber);
454 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700455 }
Hans Boehm84614952014-11-25 18:46:17 -0800456 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700457
Hans Boehm84614952014-11-25 18:46:17 -0800458 public void onCancelled() {
459 // We should be in EVALUATE state.
460 // Display is still in input state.
461 setState(CalculatorState.INPUT);
462 }
463
464 // Reevaluation completed; ask result to redisplay current value.
465 public void onReevaluate()
466 {
467 mResult.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700468 }
469
Justin Klaassenfed941a2014-06-09 18:42:40 +0100470 @Override
471 public void onTextSizeChanged(final TextView textView, float oldSize) {
472 if (mCurrentState != CalculatorState.INPUT) {
473 // Only animate text changes that occur from user input.
474 return;
475 }
476
477 // Calculate the values needed to perform the scale and translation animations,
478 // maintaining the same apparent baseline for the displayed text.
479 final float textScale = oldSize / textView.getTextSize();
480 final float translationX = (1.0f - textScale) *
481 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
482 final float translationY = (1.0f - textScale) *
483 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
484
485 final AnimatorSet animatorSet = new AnimatorSet();
486 animatorSet.playTogether(
487 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
488 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
489 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
490 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700491 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100492 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
493 animatorSet.start();
494 }
495
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700496 private void onEquals() {
497 if (mCurrentState == CalculatorState.INPUT) {
498 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800499 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700500 }
501 }
502
503 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700504 // Delete works like backspace; remove the last character or operator from the expression.
505 // Note that we handle keyboard delete exactly like the delete button. For
506 // example the delete button can be used to delete a character from an incomplete
507 // function name typed on a physical keyboard.
Hans Boehm84614952014-11-25 18:46:17 -0800508 mEvaluator.cancelAll();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700509 // This should be impossible in RESULT state.
510 setState(CalculatorState.INPUT);
511 if (mUnprocessedChars != null) {
512 int len = mUnprocessedChars.length();
513 if (len > 0) {
514 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
515 } else {
516 mEvaluator.getExpr().delete();
517 }
518 } else {
519 mEvaluator.getExpr().delete();
520 }
521 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700522 }
523
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700524 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700525 final ViewGroupOverlay groupOverlay =
526 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700527
528 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700529 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700530
531 // Make reveal cover the display and status bar.
532 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700533 revealView.setBottom(displayRect.bottom);
534 revealView.setLeft(displayRect.left);
535 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700536 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700537 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700538
Justin Klaassen4b3af052014-05-27 17:53:10 -0700539 final int[] clearLocation = new int[2];
540 sourceView.getLocationInWindow(clearLocation);
541 clearLocation[0] += sourceView.getWidth() / 2;
542 clearLocation[1] += sourceView.getHeight() / 2;
543
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700544 final int revealCenterX = clearLocation[0] - revealView.getLeft();
545 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700546
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700547 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
548 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
549 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700550 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
551
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700552 final Animator revealAnimator =
553 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700554 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700555 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700556 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700557 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700558
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700559 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700560 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700561 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700562
563 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700564 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700565 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
566 animatorSet.addListener(new AnimatorListenerAdapter() {
567 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700568 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700569 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700570 mCurrentAnimator = null;
571 }
572 });
573
574 mCurrentAnimator = animatorSet;
575 animatorSet.start();
576 }
577
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700578 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800579 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700580 return;
581 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700582 mUnprocessedChars = null;
Hans Boehm84614952014-11-25 18:46:17 -0800583 mResult.clear();
584 mEvaluator.clear();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700585 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
586 @Override
587 public void onAnimationEnd(Animator animation) {
Hans Boehm84614952014-11-25 18:46:17 -0800588 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700589 }
590 });
591 }
592
Hans Boehm84614952014-11-25 18:46:17 -0800593 // Evaluation encountered en error. Display the error.
594 void onError(final int errorResourceId) {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700595 if (mCurrentState != CalculatorState.EVALUATE) {
596 // Only animate error on evaluate.
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700597 return;
598 }
599
Hans Boehm84614952014-11-25 18:46:17 -0800600 setState(CalculatorState.ANIMATE);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700601 reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() {
602 @Override
603 public void onAnimationEnd(Animator animation) {
604 setState(CalculatorState.ERROR);
Hans Boehm84614952014-11-25 18:46:17 -0800605 mResult.displayError(errorResourceId);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700606 }
607 });
608 }
609
Hans Boehm84614952014-11-25 18:46:17 -0800610
611 // Animate movement of result into the top formula slot.
612 // Result window now remains translated in the top slot while the result is displayed.
613 // (We convert it back to formula use only when the user provides new input.)
614 // Historical note: In the Lollipop version, this invisibly and instantaeously moved
615 // formula and result displays back at the end of the animation. We no longer do that,
616 // so that we can continue to properly support scrolling of the result.
617 // We assume the result already contains the text to be expanded.
618 private void onResult(boolean animate) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700619 // Calculate the values needed to perform the scale and translation animations.
620 // We now fix the character size in the display to avoid weird effects
Hans Boehm84614952014-11-25 18:46:17 -0800621 // when we scroll.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700622 // Display.xml is designed to ensure exactly a 3/2 ratio between the formula
623 // slot and small result slot.
624 final float resultScale = 1.5f;
625 final float resultTranslationX = -mResult.getWidth() * (resultScale - 1)/2;
626 // mFormulaEditText is aligned with mResult on the right.
627 // When we enlarge it around its center, the right side
628 // moves to the right. This compensates.
629 float resultTranslationY = -mResult.getHeight();
630 // This is how much we want to move the bottom.
631 // Now compensate for the fact that we're
632 // simultaenously expanding it around its center by half its height
633 resultTranslationY += mResult.getHeight() * (resultScale-1)/2;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700634 final float formulaTranslationY = -mFormulaEditText.getBottom();
635
Hans Boehm84614952014-11-25 18:46:17 -0800636 // TODO: Reintroduce textColorAnimator?
637 // The initial and final colors seemed to be the same in L.
638 // With the new model, the result logically changes back to a formula
639 // only when we switch back to INPUT state, so it's unclear that animating
640 // a color change here makes sense.
641 if (animate) {
642 final AnimatorSet animatorSet = new AnimatorSet();
643 animatorSet.playTogether(
644 ObjectAnimator.ofFloat(mResult, View.SCALE_X, resultScale),
645 ObjectAnimator.ofFloat(mResult, View.SCALE_Y, resultScale),
646 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_X, resultTranslationX),
647 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_Y, resultTranslationY),
648 ObjectAnimator.ofFloat(mFormulaEditText, View.TRANSLATION_Y,
649 formulaTranslationY));
650 animatorSet.setDuration(
651 getResources().getInteger(android.R.integer.config_longAnimTime));
652 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
653 animatorSet.addListener(new AnimatorListenerAdapter() {
654 @Override
655 public void onAnimationStart(Animator animation) {
656 // Result should already be displayed; no need to do anything.
657 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700658
Hans Boehm84614952014-11-25 18:46:17 -0800659 @Override
660 public void onAnimationEnd(Animator animation) {
661 setState(CalculatorState.RESULT);
662 mCurrentAnimator = null;
663 }
664 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700665
Hans Boehm84614952014-11-25 18:46:17 -0800666 mCurrentAnimator = animatorSet;
667 animatorSet.start();
668 } else /* No animation desired; get there fast, e.g. when restarting */ {
669 mResult.setScaleX(resultScale);
670 mResult.setScaleY(resultScale);
671 mResult.setTranslationX(resultTranslationX);
672 mResult.setTranslationY(resultTranslationY);
673 mFormulaEditText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700674 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800675 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700676 }
Hans Boehm84614952014-11-25 18:46:17 -0800677
678 // Restore positions of the formula and result displays back to their original,
679 // pre-animation state.
680 private void restoreDisplayPositions() {
681 // Clear result.
682 mResult.setText("");
683 // Reset all of the values modified during the animation.
684 mResult.setScaleX(1.0f);
685 mResult.setScaleY(1.0f);
686 mResult.setTranslationX(0.0f);
687 mResult.setTranslationY(0.0f);
688 mFormulaEditText.setTranslationY(0.0f);
689
690 mFormulaEditText.requestFocus();
691 }
692
693 // Overflow menu handling.
694 private PopupMenu constructPopupMenu() {
695 final PopupMenu popupMenu = new PopupMenu(this, mOverflowMenuButton);
696 mOverflowMenuButton.setOnTouchListener(popupMenu.getDragToOpenListener());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700697 popupMenu.inflate(R.menu.overflow);
Hans Boehm84614952014-11-25 18:46:17 -0800698 final Menu menu = popupMenu.getMenu();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700699 if (mCurrentState != CalculatorState.RESULT) {
700 menu.findItem(R.id.menu_fraction).setEnabled(false);
701 menu.findItem(R.id.menu_leading).setEnabled(false);
702 } else if (mEvaluator.getRational() == null) {
703 menu.findItem(R.id.menu_fraction).setEnabled(false);
704 }
Hans Boehm84614952014-11-25 18:46:17 -0800705 popupMenu.setOnMenuItemClickListener(this);
706 onPrepareOptionsMenu(menu);
707 return popupMenu;
708 }
709
710 @Override
711 public boolean onMenuItemClick(MenuItem item) {
712 switch (item.getItemId()) {
713 case R.id.menu_help:
714 displayHelpMessage();
715 return true;
716 case R.id.menu_about:
717 displayAboutPage();
718 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700719 case R.id.menu_fraction:
720 displayFraction();
721 return true;
722 case R.id.menu_leading:
723 displayFull();
724 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800725 default:
726 return super.onOptionsItemSelected(item);
727 }
728 }
729
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700730 private void displayMessage(String s) {
Hans Boehm84614952014-11-25 18:46:17 -0800731 AlertDialog.Builder builder = new AlertDialog.Builder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700732 builder.setMessage(s)
733 .setNegativeButton(R.string.dismiss,
Hans Boehm84614952014-11-25 18:46:17 -0800734 new DialogInterface.OnClickListener() {
735 public void onClick(DialogInterface d, int which) { }
736 })
737 .show();
738 }
739
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700740 private void displayHelpMessage() {
741 Resources res = getResources();
742 String msg = res.getString(R.string.help_message);
743 if (mPadViewPager != null) {
744 msg += res.getString(R.string.help_pager);
745 }
746 displayMessage(msg);
747 }
748
749 private void displayFraction() {
750 BoundedRational result = mEvaluator.getRational();
751 displayMessage(result.toNiceString());
752 }
753
754 // Display full result to currently evaluated precision
755 private void displayFull() {
756 Resources res = getResources();
757 String msg = mResult.getFullText() + " ";
758 if (mResult.fullTextIsExact()) {
759 msg += res.getString(R.string.exact);
760 } else {
761 msg += res.getString(R.string.approximate);
762 }
763 displayMessage(msg);
764 }
765
Hans Boehm84614952014-11-25 18:46:17 -0800766 private void displayAboutPage() {
767 WebView wv = new WebView(this);
768 wv.loadUrl("file:///android_asset/about.txt");
769 new AlertDialog.Builder(this)
770 .setView(wv)
771 .setNegativeButton(R.string.dismiss,
772 new DialogInterface.OnClickListener() {
773 public void onClick(DialogInterface d, int which) { }
774 })
775 .show();
776 }
777
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700778 // Add input characters to the end of the expression by mapping them to
779 // the appropriate button pushes when possible. Leftover characters
780 // are added to mUnprocessedChars, which is presumed to immediately
781 // precede the newly added characters.
782 private void addChars(String moreChars) {
783 if (mUnprocessedChars != null) {
784 moreChars = mUnprocessedChars + moreChars;
785 }
786 int current = 0;
787 int len = moreChars.length();
788 while (current < len) {
789 char c = moreChars.charAt(current);
790 int k = KeyMaps.keyForChar(c, this);
791 if (k != View.NO_ID) {
792 mCurrentButton = findViewById(k);
793 addKeyToExpr(k);
794 if (Character.isSurrogate(c)) {
795 current += 2;
796 } else {
797 ++current;
798 }
799 continue;
800 }
801 int f = KeyMaps.funForString(moreChars, current, this);
802 if (f != View.NO_ID) {
803 mCurrentButton = findViewById(f);
804 addKeyToExpr(f);
805 if (f == R.id.op_sqrt) {
806 // Square root entered as function; don't lose the parenthesis.
807 addKeyToExpr(R.id.lparen);
808 }
809 current = moreChars.indexOf('(', current) + 1;
810 continue;
811 }
812 // There are characters left, but we can't convert them to button presses.
813 mUnprocessedChars = moreChars.substring(current);
814 redisplayAfterFormulaChange();
815 return;
816 }
817 mUnprocessedChars = null;
818 redisplayAfterFormulaChange();
819 return;
Hans Boehm84614952014-11-25 18:46:17 -0800820 }
821
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700822 @Override
823 public boolean paste(Uri uri) {
824 if (mEvaluator.isLastSaved(uri)) {
825 if (mCurrentState == CalculatorState.ERROR
826 || mCurrentState == CalculatorState.RESULT) {
827 setState(CalculatorState.INPUT);
828 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800829 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700830 mEvaluator.addSaved();
831 redisplayAfterFormulaChange();
832 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800833 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700834 return false;
Hans Boehm84614952014-11-25 18:46:17 -0800835 }
836
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700837 @Override
838 public void paste(String s) {
839 addChars(s);
Hans Boehm84614952014-11-25 18:46:17 -0800840 }
841
Justin Klaassen4b3af052014-05-27 17:53:10 -0700842}