blob: f1c9835a47edd4441ab27aadd51fad37f11fea47 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
Justin Klaassen44595162015-05-28 17:55:20 -07002 * Copyright (C) 2015 The Android Open Source Project
Justin Klaassen4b3af052014-05-27 17:53:10 -07003 *
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// TODO: Better indication of when the result is known to be exact.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070018// TODO: Check and possibly fix accessability issues.
Hans Boehm013969e2015-04-13 20:29:47 -070019// TODO: Copy & more general paste in formula? Note that this requires
20// great care: Currently the text version of a displayed formula
21// is not directly useful for re-evaluating the formula later, since
22// it contains ellipses representing subexpressions evaluated with
23// a different degree mode. Rather than supporting copy from the
24// formula window, we may eventually want to support generation of a
25// more useful text version in a separate window. It's not clear
26// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080027
Justin Klaassen4b3af052014-05-27 17:53:10 -070028package com.android.calculator2;
29
30import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070031import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070032import android.animation.AnimatorListenerAdapter;
33import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070034import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070035import android.animation.PropertyValuesHolder;
Justin Klaassen4b3af052014-05-27 17:53:10 -070036import android.app.Activity;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070037import android.content.ClipData;
Justin Klaassend36d63e2015-05-05 12:59:36 -070038import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070039import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070040import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070041import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070042import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070043import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070044import android.support.annotation.NonNull;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010045import android.support.v4.view.ViewPager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070046import android.text.SpannableString;
47import android.text.Spanned;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070048import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070049import android.util.Property;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070050import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070051import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080052import android.view.Menu;
53import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070054import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070055import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070056import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070057import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070058import android.view.ViewGroupOverlay;
Justin Klaassen4b3af052014-05-27 17:53:10 -070059import android.view.animation.AccelerateDecelerateInterpolator;
Justin Klaassenfed941a2014-06-09 18:42:40 +010060import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070061import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010062
Hans Boehm08e8f322015-04-21 13:18:38 -070063import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080064
65import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080066import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080067import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070068import java.io.ObjectInput;
69import java.io.ObjectInputStream;
70import java.io.ObjectOutput;
71import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070072
Justin Klaassen04f79c72014-06-27 17:25:35 -070073public class Calculator extends Activity
Justin Klaassenfc5ac822015-06-18 13:15:17 -070074 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070075
76 /**
77 * Constant for an invalid resource id.
78 */
79 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070080
81 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080082 INPUT, // Result and formula both visible, no evaluation requested,
83 // Though result may be visible on bottom line.
84 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070085 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080086 INIT, // Very temporary state used as alternative to EVALUATE
87 // during reinitialization. Do not animate on completion.
88 ANIMATE, // Result computed, animation to enlarge result window in progress.
89 RESULT, // Result displayed, formula invisible.
90 // If we are in RESULT state, the formula was evaluated without
91 // error to initial precision.
92 ERROR // Error displayed: Formula visible, result shows error message.
93 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -070094 }
Hans Boehm84614952014-11-25 18:46:17 -080095 // Normal transition sequence is
96 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
97 // A RESULT -> ERROR transition is possible in rare corner cases, in which
98 // a higher precision evaluation exposes an error. This is possible, since we
99 // initially evaluate assuming we were given a well-defined problem. If we
100 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
101 // unless we are asked for enough precision that we can distinguish the argument from zero.
102 // TODO: Consider further heuristics to reduce the chance of observing this?
103 // It already seems to be observable only in contrived cases.
104 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
105 // is restarted in that state. This leads us to recompute and redisplay the result
106 // ASAP.
107 // TODO: Possibly save a bit more information, e.g. its initial display string
108 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700109
Justin Klaassen44595162015-05-28 17:55:20 -0700110 private final Property<TextView, Integer> TEXT_COLOR =
111 new Property<TextView, Integer>(Integer.class, "textColor") {
112 @Override
113 public Integer get(TextView textView) {
114 return textView.getCurrentTextColor();
115 }
116
117 @Override
118 public void set(TextView textView, Integer textColor) {
119 textView.setTextColor(textColor);
120 }
121 };
122
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700123 // We currently assume that the formula does not change out from under us in
124 // any way. We explicitly handle all input to the formula here.
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700125 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
126 @Override
127 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm1176f232015-05-11 16:26:03 -0700128 stopActionMode();
Justin Klaassen06c49442015-06-04 14:39:27 -0700129 // Never consume DPAD key events.
130 switch (keyCode) {
131 case KeyEvent.KEYCODE_DPAD_UP:
132 case KeyEvent.KEYCODE_DPAD_DOWN:
133 case KeyEvent.KEYCODE_DPAD_LEFT:
134 case KeyEvent.KEYCODE_DPAD_RIGHT:
135 return false;
136 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700137 // Always cancel unrequested in-progress evaluation, so that we don't have
138 // to worry about subsequent asynchronous completion.
139 // Requested in-progress evaluations are handled below.
140 if (mCurrentState != CalculatorState.EVALUATE) {
141 mEvaluator.cancelAll(true);
142 }
143 // In other cases we go ahead and process the input normally after cancelling:
Justin Klaassen06c49442015-06-04 14:39:27 -0700144 if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
145 return true;
146 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700147 switch (keyCode) {
148 case KeyEvent.KEYCODE_NUMPAD_ENTER:
149 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700150 case KeyEvent.KEYCODE_DPAD_CENTER:
151 mCurrentButton = mEqualButton;
152 onEquals();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700153 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700154 case KeyEvent.KEYCODE_DEL:
155 mCurrentButton = mDeleteButton;
156 onDelete();
157 return true;
158 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700159 cancelIfEvaluating(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700160 final int raw = keyEvent.getKeyCharacterMap()
Justin Klaassen44595162015-05-28 17:55:20 -0700161 .get(keyCode, keyEvent.getMetaState());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700162 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
163 return true; // discard
164 }
165 // Try to discard non-printing characters and the like.
166 // The user will have to explicitly delete other junk that gets past us.
167 if (Character.isIdentifierIgnorable(raw)
Justin Klaassen44595162015-05-28 17:55:20 -0700168 || Character.isWhitespace(raw)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700169 return true;
170 }
Justin Klaassen44595162015-05-28 17:55:20 -0700171 char c = (char) raw;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700172 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700173 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700174 onEquals();
175 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700176 addChars(String.valueOf(c), true);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700177 redisplayAfterFormulaChange();
178 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700179 }
180 return false;
181 }
182 };
183
Hans Boehm84614952014-11-25 18:46:17 -0800184 private static final String NAME = Calculator.class.getName();
185 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700186 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Hans Boehm84614952014-11-25 18:46:17 -0800187 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
188 // Associated value is a byte array holding both mCalculatorState
189 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700190
Justin Klaassen4b3af052014-05-27 17:53:10 -0700191 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800192 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700193
Justin Klaassen06360f92014-08-28 11:08:44 -0700194 private View mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700195 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700196 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700197 private CalculatorResult mResultText;
Justin Klaassend48b7562015-04-16 16:51:38 -0700198
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100199 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700200 private View mDeleteButton;
201 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700202 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700203
204 private TextView mInverseToggle;
205 private TextView mModeToggle;
206
Justin Klaassen721ec842015-05-28 14:30:08 -0700207 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700208 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700209
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700210 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700211 private Animator mCurrentAnimator;
212
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700213 private String mUnprocessedChars = null; // Characters that were recently entered
214 // at the end of the display that have not yet
215 // been added to the underlying expression.
216
Justin Klaassen4b3af052014-05-27 17:53:10 -0700217 @Override
218 protected void onCreate(Bundle savedInstanceState) {
219 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700220 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700221 setActionBar((Toolbar) findViewById(R.id.toolbar));
222
223 // Hide all default options in the ActionBar.
224 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700225
Justin Klaassen06360f92014-08-28 11:08:44 -0700226 mDisplayView = findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700227 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700228 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700229 mResultText = (CalculatorResult) findViewById(R.id.result);
Justin Klaassend48b7562015-04-16 16:51:38 -0700230
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100231 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700232 mDeleteButton = findViewById(R.id.del);
233 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700234 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
235 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
236 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
237 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700238
239 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
240 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
241
Justin Klaassen721ec842015-05-28 14:30:08 -0700242 mInvertibleButtons = new View[] {
243 findViewById(R.id.fun_sin),
244 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700245 findViewById(R.id.fun_tan),
246 findViewById(R.id.fun_ln),
247 findViewById(R.id.fun_log),
248 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700249 };
250 mInverseButtons = new View[] {
251 findViewById(R.id.fun_arcsin),
252 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700253 findViewById(R.id.fun_arctan),
254 findViewById(R.id.fun_exp),
255 findViewById(R.id.fun_10pow),
256 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700257 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700258
Justin Klaassen44595162015-05-28 17:55:20 -0700259 mEvaluator = new Evaluator(this, mResultText);
260 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700261 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700262
Hans Boehm84614952014-11-25 18:46:17 -0800263 if (savedInstanceState != null) {
264 setState(CalculatorState.values()[
265 savedInstanceState.getInt(KEY_DISPLAY_STATE,
266 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700267 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
268 if (unprocessed != null) {
269 mUnprocessedChars = unprocessed.toString();
270 }
271 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800272 if (state != null) {
273 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
274 mEvaluator.restoreInstanceState(in);
275 } catch (Throwable ignored) {
276 // When in doubt, revert to clean state
277 mCurrentState = CalculatorState.INPUT;
278 mEvaluator.clear();
279 }
280 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700281 } else {
282 mCurrentState = CalculatorState.INPUT;
283 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800284 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700285
Hans Boehm08e8f322015-04-21 13:18:38 -0700286 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
287 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700288 mFormulaText.setOnPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700289 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700290
291 onInverseToggled(mInverseToggle.isSelected());
292 onModeChanged(mEvaluator.getDegreeMode());
293
Hans Boehm84614952014-11-25 18:46:17 -0800294 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700295 // Just reevaluate.
296 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800297 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800298 mEvaluator.requireResult();
299 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700300 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800301 }
302 // TODO: We're currently not saving and restoring scroll position.
303 // We probably should. Details may require care to deal with:
304 // - new display size
305 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700306 }
307
308 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700309 protected void onSaveInstanceState(@NonNull Bundle outState) {
310 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
311 if (mCurrentAnimator != null) {
312 mCurrentAnimator.cancel();
313 }
314
Justin Klaassen4b3af052014-05-27 17:53:10 -0700315 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800316 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700317 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800318 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
319 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
320 mEvaluator.saveInstanceState(out);
321 } catch (IOException e) {
322 // Impossible; No IO involved.
323 throw new AssertionError("Impossible IO exception", e);
324 }
325 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700326 }
327
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700328 // Set the state, updating delete label and display colors.
329 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700330 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700331 private void setState(CalculatorState state) {
332 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800333 if (state == CalculatorState.INPUT) {
334 restoreDisplayPositions();
335 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700336 mCurrentState = state;
337
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700338 if (mCurrentState == CalculatorState.RESULT) {
339 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700340 mDeleteButton.setVisibility(View.GONE);
341 mClearButton.setVisibility(View.VISIBLE);
342 } else {
343 mDeleteButton.setVisibility(View.VISIBLE);
344 mClearButton.setVisibility(View.GONE);
345 }
346
Hans Boehm84614952014-11-25 18:46:17 -0800347 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700348 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700349 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700350 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700351 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700352 } else if (mCurrentState != CalculatorState.RESULT) {
353 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
354 mResultText.setTextColor(getColor(R.color.display_result_text_color));
355 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700356 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700357
358 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700359 }
360 }
361
Hans Boehm1176f232015-05-11 16:26:03 -0700362 // Stop any active ActionMode. Return true if there was one.
363 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700364 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700365 return true;
366 }
367 if (mFormulaText.stopActionMode()) {
368 return true;
369 }
370 return false;
371 }
372
Justin Klaassen4b3af052014-05-27 17:53:10 -0700373 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100374 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700375 if (!stopActionMode()) {
376 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
377 // Select the previous pad.
378 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
379 } else {
380 // If the user is currently looking at the first pad (or the pad is not paged),
381 // allow the system to handle the Back button.
382 super.onBackPressed();
383 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100384 }
385 }
386
387 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700388 public void onUserInteraction() {
389 super.onUserInteraction();
390
Hans Boehmc1ea0912015-06-19 15:05:07 -0700391 // If there's an animation in progress, end it immediately, so the user interaction can
392 // be handled.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700393 if (mCurrentAnimator != null) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700394 mCurrentAnimator.end();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700395 }
396 }
397
Justin Klaassene2711cb2015-05-28 11:13:17 -0700398 /**
399 * Invoked whenever the inverse button is toggled to update the UI.
400 *
401 * @param showInverse {@code true} if inverse functions should be shown
402 */
403 private void onInverseToggled(boolean showInverse) {
404 if (showInverse) {
405 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700406 for (View invertibleButton : mInvertibleButtons) {
407 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700408 }
409 for (View inverseButton : mInverseButtons) {
410 inverseButton.setVisibility(View.VISIBLE);
411 }
412 } else {
413 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700414 for (View invertibleButton : mInvertibleButtons) {
415 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700416 }
417 for (View inverseButton : mInverseButtons) {
418 inverseButton.setVisibility(View.GONE);
419 }
420 }
421 }
422
423 /**
424 * Invoked whenever the deg/rad mode may have changed to update the UI.
425 *
426 * @param degreeMode {@code true} if in degree mode
427 */
428 private void onModeChanged(boolean degreeMode) {
429 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700430 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700431 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
432
433 mModeToggle.setText(R.string.mode_rad);
434 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700435 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700436 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700437 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
438
439 mModeToggle.setText(R.string.mode_deg);
440 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700441 }
442 }
Hans Boehm84614952014-11-25 18:46:17 -0800443
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700444 // Add the given button id to input expression.
445 // If appropriate, clear the expression before doing so.
446 private void addKeyToExpr(int id) {
447 if (mCurrentState == CalculatorState.ERROR) {
448 setState(CalculatorState.INPUT);
449 } else if (mCurrentState == CalculatorState.RESULT) {
450 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
451 mEvaluator.collapse();
452 } else {
Hans Boehmccc55662015-07-07 14:16:59 -0700453 announceClearForAccessibility();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700454 mEvaluator.clear();
455 }
456 setState(CalculatorState.INPUT);
457 }
458 if (!mEvaluator.append(id)) {
459 // TODO: Some user visible feedback?
460 }
461 }
462
Hans Boehm017de982015-06-10 17:46:03 -0700463 /**
464 * Add the given button id to input expression, assuming it was explicitly
465 * typed/touched.
466 * We perform slightly more aggressive correction than in pasted expressions.
467 */
468 private void addExplicitKeyToExpr(int id) {
469 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
470 mEvaluator.getExpr().removeTrailingAdditiveOperators();
471 }
472 addKeyToExpr(id);
473 }
474
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700475 private void redisplayAfterFormulaChange() {
476 // TODO: Could do this more incrementally.
477 redisplayFormula();
478 setState(CalculatorState.INPUT);
Hans Boehmc023b732015-04-29 11:30:47 -0700479 if (mEvaluator.getExpr().hasInterestingOps()) {
480 mEvaluator.evaluateAndShowResult();
481 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700482 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700483 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700484 }
485
Justin Klaassen4b3af052014-05-27 17:53:10 -0700486 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700487 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700488 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700489 stopActionMode();
Hans Boehmc1ea0912015-06-19 15:05:07 -0700490 // See onKey above for the rationale behind some of the behavior below:
491 if (mCurrentState != CalculatorState.EVALUATE) {
492 // Cancel evaluations that were not specifically requested.
493 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800494 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700495 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800496 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700497 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700498 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700499 break;
500 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700501 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700502 break;
503 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700504 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700505 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700506 case R.id.toggle_inv:
507 final boolean selected = !mInverseToggle.isSelected();
508 mInverseToggle.setSelected(selected);
509 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700510 if (mCurrentState == CalculatorState.RESULT) {
511 mResultText.redisplay(); // In case we cancelled reevaluation.
512 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700513 break;
514 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700515 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700516 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700517 if (mCurrentState == CalculatorState.RESULT) {
518 mEvaluator.collapse(); // Capture result evaluated in old mode
519 redisplayFormula();
520 }
521 // In input mode, we reinterpret already entered trig functions.
522 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700523 onModeChanged(mode);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700524 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700525 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700526 if (mEvaluator.getExpr().hasInterestingOps()) {
527 mEvaluator.evaluateAndShowResult();
528 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700529 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700530 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700531 cancelIfEvaluating(false);
Hans Boehm017de982015-06-10 17:46:03 -0700532 addExplicitKeyToExpr(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700533 redisplayAfterFormulaChange();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700534 break;
535 }
536 }
537
Hans Boehm84614952014-11-25 18:46:17 -0800538 void redisplayFormula() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700539 String formula = mEvaluator.getExpr().toString(this);
540 if (mUnprocessedChars != null) {
541 // Add and highlight characters we couldn't process.
542 SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
543 // TODO: should probably match this to the error color.
544 formatted.setSpan(new ForegroundColorSpan(Color.RED),
545 formula.length(), formatted.length(),
546 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehmccc55662015-07-07 14:16:59 -0700547 mFormulaText.changeTextTo(formatted);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700548 } else {
Hans Boehmccc55662015-07-07 14:16:59 -0700549 mFormulaText.changeTextTo(formula);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700550 }
Hans Boehm84614952014-11-25 18:46:17 -0800551 }
552
Justin Klaassen4b3af052014-05-27 17:53:10 -0700553 @Override
554 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700555 mCurrentButton = view;
556
Justin Klaassen4b3af052014-05-27 17:53:10 -0700557 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700558 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700559 return true;
560 }
561 return false;
562 }
563
Hans Boehm84614952014-11-25 18:46:17 -0800564 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700565 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
566 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700567 // Invalidate any options that may depend on the current result.
568 invalidateOptionsMenu();
569
Hans Boehma0e45f32015-05-30 13:20:35 -0700570 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700571 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800572 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700573 }
Hans Boehm84614952014-11-25 18:46:17 -0800574 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700575
Hans Boehmc1ea0912015-06-19 15:05:07 -0700576 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800577 public void onCancelled() {
578 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800579 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700580 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800581 }
582
583 // Reevaluation completed; ask result to redisplay current value.
584 public void onReevaluate()
585 {
Justin Klaassen44595162015-05-28 17:55:20 -0700586 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700587 }
588
Justin Klaassenfed941a2014-06-09 18:42:40 +0100589 @Override
590 public void onTextSizeChanged(final TextView textView, float oldSize) {
591 if (mCurrentState != CalculatorState.INPUT) {
592 // Only animate text changes that occur from user input.
593 return;
594 }
595
596 // Calculate the values needed to perform the scale and translation animations,
597 // maintaining the same apparent baseline for the displayed text.
598 final float textScale = oldSize / textView.getTextSize();
599 final float translationX = (1.0f - textScale) *
600 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
601 final float translationY = (1.0f - textScale) *
602 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
603
604 final AnimatorSet animatorSet = new AnimatorSet();
605 animatorSet.playTogether(
606 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
607 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
608 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
609 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700610 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100611 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
612 animatorSet.start();
613 }
614
Hans Boehmc1ea0912015-06-19 15:05:07 -0700615 /**
616 * Cancel any in-progress explicitly requested evaluations.
617 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
618 value, and certainly changes the display, so it seems reasonable to warn.
619 * @return true if there was such an evaluation
620 */
621 private boolean cancelIfEvaluating(boolean quiet) {
622 if (mCurrentState == CalculatorState.EVALUATE) {
623 mEvaluator.cancelAll(quiet);
624 return true;
625 } else {
626 return false;
627 }
628 }
629
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700630 private void onEquals() {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700631 // In non-INPUT state assume this was redundant and ignore it.
Hans Boehmc023b732015-04-29 11:30:47 -0700632 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700633 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800634 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700635 }
636 }
637
638 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700639 // Delete works like backspace; remove the last character or operator from the expression.
640 // Note that we handle keyboard delete exactly like the delete button. For
641 // example the delete button can be used to delete a character from an incomplete
642 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700643 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700644 // If there is an in-progress explicit evaluation, just cancel it and return.
645 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700646 setState(CalculatorState.INPUT);
647 if (mUnprocessedChars != null) {
648 int len = mUnprocessedChars.length();
649 if (len > 0) {
650 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
651 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700652 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700653 }
654 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700655 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700656 }
657 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700658 }
659
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700660 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700661 final ViewGroupOverlay groupOverlay =
662 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700663
664 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700665 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700666
667 // Make reveal cover the display and status bar.
668 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700669 revealView.setBottom(displayRect.bottom);
670 revealView.setLeft(displayRect.left);
671 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700672 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700673 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700674
Justin Klaassen4b3af052014-05-27 17:53:10 -0700675 final int[] clearLocation = new int[2];
676 sourceView.getLocationInWindow(clearLocation);
677 clearLocation[0] += sourceView.getWidth() / 2;
678 clearLocation[1] += sourceView.getHeight() / 2;
679
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700680 final int revealCenterX = clearLocation[0] - revealView.getLeft();
681 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700682
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700683 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
684 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
685 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700686 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
687
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700688 final Animator revealAnimator =
689 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700690 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700691 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700692 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700693 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700694
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700695 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700696 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700697 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700698
699 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700700 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700701 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
702 animatorSet.addListener(new AnimatorListenerAdapter() {
703 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700704 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700705 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700706 mCurrentAnimator = null;
707 }
708 });
709
710 mCurrentAnimator = animatorSet;
711 animatorSet.start();
712 }
713
Hans Boehmccc55662015-07-07 14:16:59 -0700714 private void announceClearForAccessibility() {
715 mResultText.announceForAccessibility(getResources().getString(R.string.desc_clr));
716 }
717
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700718 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800719 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700720 return;
721 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700722 cancelIfEvaluating(true);
Hans Boehmccc55662015-07-07 14:16:59 -0700723 announceClearForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700724 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
725 @Override
726 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700727 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700728 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700729 mEvaluator.clear();
730 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800731 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700732 }
733 });
734 }
735
Hans Boehm84614952014-11-25 18:46:17 -0800736 // Evaluation encountered en error. Display the error.
737 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700738 if (mCurrentState == CalculatorState.EVALUATE) {
739 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -0700740 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -0700741 reveal(mCurrentButton, R.color.calculator_error_color,
742 new AnimatorListenerAdapter() {
743 @Override
744 public void onAnimationEnd(Animator animation) {
745 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700746 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700747 }
748 });
749 } else if (mCurrentState == CalculatorState.INIT) {
750 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700751 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700752 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700753 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700754 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700755 }
756
Hans Boehm84614952014-11-25 18:46:17 -0800757
758 // Animate movement of result into the top formula slot.
759 // Result window now remains translated in the top slot while the result is displayed.
760 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700761 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800762 // formula and result displays back at the end of the animation. We no longer do that,
763 // so that we can continue to properly support scrolling of the result.
764 // We assume the result already contains the text to be expanded.
765 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700766 // Calculate the textSize that would be used to display the result in the formula.
767 // For scrollable results just use the minimum textSize to maximize the number of digits
768 // that are visible on screen.
769 float textSize = mFormulaText.getMinimumTextSize();
770 if (!mResultText.isScrollable()) {
771 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
772 }
773
774 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
775 // when a result is reused in a subsequent expression.
776 final float resultScale = textSize / mResultText.getTextSize();
777
778 // Set the result's pivot to match its gravity.
779 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
780 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
781
782 // Calculate the necessary translations so the result takes the place of the formula and
783 // the formula moves off the top of the screen.
784 final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
785 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
Hans Boehm08e8f322015-04-21 13:18:38 -0700786 final float formulaTranslationY = -mFormulaText.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700787
Justin Klaassen44595162015-05-28 17:55:20 -0700788 // Change the result's textColor to match the formula.
789 final int formulaTextColor = mFormulaText.getCurrentTextColor();
790
Hans Boehm84614952014-11-25 18:46:17 -0800791 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -0700792 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
793 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -0700794 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800795 final AnimatorSet animatorSet = new AnimatorSet();
796 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700797 ObjectAnimator.ofPropertyValuesHolder(mResultText,
798 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
799 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
800 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
801 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
802 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
803 animatorSet.setDuration(getResources().getInteger(
804 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800805 animatorSet.addListener(new AnimatorListenerAdapter() {
806 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800807 public void onAnimationEnd(Animator animation) {
808 setState(CalculatorState.RESULT);
809 mCurrentAnimator = null;
810 }
811 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700812
Hans Boehm84614952014-11-25 18:46:17 -0800813 mCurrentAnimator = animatorSet;
814 animatorSet.start();
815 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700816 mResultText.setScaleX(resultScale);
817 mResultText.setScaleY(resultScale);
818 mResultText.setTranslationY(resultTranslationY);
819 mResultText.setTextColor(formulaTextColor);
Hans Boehm08e8f322015-04-21 13:18:38 -0700820 mFormulaText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700821 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800822 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700823 }
Hans Boehm84614952014-11-25 18:46:17 -0800824
825 // Restore positions of the formula and result displays back to their original,
826 // pre-animation state.
827 private void restoreDisplayPositions() {
828 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700829 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800830 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700831 mResultText.setScaleX(1.0f);
832 mResultText.setScaleY(1.0f);
833 mResultText.setTranslationX(0.0f);
834 mResultText.setTranslationY(0.0f);
Hans Boehm08e8f322015-04-21 13:18:38 -0700835 mFormulaText.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800836
Hans Boehm08e8f322015-04-21 13:18:38 -0700837 mFormulaText.requestFocus();
Hans Boehm84614952014-11-25 18:46:17 -0800838 }
839
Justin Klaassend48b7562015-04-16 16:51:38 -0700840 @Override
841 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700842 super.onCreateOptionsMenu(menu);
843
844 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700845 return true;
846 }
847
848 @Override
849 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700850 super.onPrepareOptionsMenu(menu);
851
852 // Show the leading option when displaying a result.
853 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
854
855 // Show the fraction option when displaying a rational result.
856 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
857 && mEvaluator.getRational() != null);
858
Justin Klaassend48b7562015-04-16 16:51:38 -0700859 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800860 }
861
862 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700863 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800864 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700865 case R.id.menu_leading:
866 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800867 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700868 case R.id.menu_fraction:
869 displayFraction();
870 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700871 case R.id.menu_licenses:
872 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700873 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800874 default:
875 return super.onOptionsItemSelected(item);
876 }
877 }
878
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700879 private void displayMessage(String s) {
Hans Boehma3723842015-06-24 17:54:13 -0700880 AlertDialogFragment.showMessageDialog(this, s);
Hans Boehm84614952014-11-25 18:46:17 -0800881 }
882
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700883 private void displayFraction() {
884 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700885 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700886 }
887
888 // Display full result to currently evaluated precision
889 private void displayFull() {
890 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -0700891 String msg = mResultText.getFullText() + " ";
892 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700893 msg += res.getString(R.string.exact);
894 } else {
895 msg += res.getString(R.string.approximate);
896 }
897 displayMessage(msg);
898 }
899
Hans Boehm017de982015-06-10 17:46:03 -0700900 /**
901 * Add input characters to the end of the expression.
902 * Map them to the appropriate button pushes when possible. Leftover characters
903 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
904 * added characters.
905 * @param moreChars Characters to be added.
Hans Boehm0b9806f2015-06-29 16:07:15 -0700906 * @param explicit These characters were explicitly typed by the user, not pasted.
Hans Boehm017de982015-06-10 17:46:03 -0700907 */
908 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700909 if (mUnprocessedChars != null) {
910 moreChars = mUnprocessedChars + moreChars;
911 }
912 int current = 0;
913 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -0700914 boolean lastWasDigit = false;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700915 while (current < len) {
916 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -0700917 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -0700918 if (!explicit) {
919 int expEnd;
920 if (lastWasDigit && current !=
921 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
922 // Process scientific notation with 'E' when pasting, in spite of ambiguity
923 // with base of natural log.
924 // Otherwise the 10^x key is the user's friend.
925 mEvaluator.addExponent(moreChars, current, expEnd);
926 current = expEnd;
927 lastWasDigit = false;
928 continue;
929 } else {
930 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
931 if (current == 0 && (isDigit || k == R.id.dec_point)
932 && mEvaluator.getExpr().hasTrailingConstant()) {
933 // Refuse to concatenate pasted content to trailing constant.
934 // This makes pasting of calculator results more consistent, whether or
935 // not the old calculator instance is still around.
936 addKeyToExpr(R.id.op_mul);
937 }
938 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
939 }
940 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700941 if (k != View.NO_ID) {
942 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -0700943 if (explicit) {
944 addExplicitKeyToExpr(k);
945 } else {
946 addKeyToExpr(k);
947 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700948 if (Character.isSurrogate(c)) {
949 current += 2;
950 } else {
951 ++current;
952 }
953 continue;
954 }
Hans Boehm013969e2015-04-13 20:29:47 -0700955 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700956 if (f != View.NO_ID) {
957 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -0700958 if (explicit) {
959 addExplicitKeyToExpr(f);
960 } else {
961 addKeyToExpr(f);
962 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700963 if (f == R.id.op_sqrt) {
964 // Square root entered as function; don't lose the parenthesis.
965 addKeyToExpr(R.id.lparen);
966 }
967 current = moreChars.indexOf('(', current) + 1;
968 continue;
969 }
970 // There are characters left, but we can't convert them to button presses.
971 mUnprocessedChars = moreChars.substring(current);
972 redisplayAfterFormulaChange();
973 return;
974 }
975 mUnprocessedChars = null;
976 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800977 }
978
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700979 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700980 public boolean onPaste(ClipData clip) {
981 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
982 if (item == null) {
983 // nothing to paste, bail early...
984 return false;
985 }
986
987 // Check if the item is a previously copied result, otherwise paste as raw text.
988 final Uri uri = item.getUri();
989 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700990 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700991 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700992 setState(CalculatorState.INPUT);
993 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800994 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700995 mEvaluator.addSaved();
996 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700997 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700998 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -0800999 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001000 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001001 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001002}