blob: 84f92c84c2d2fa64a88e750a8212183b8618489f [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 Boehm013969e2015-04-13 20:29:47 -070017// TODO: Copy & more general paste in formula? Note that this requires
18// great care: Currently the text version of a displayed formula
19// is not directly useful for re-evaluating the formula later, since
20// it contains ellipses representing subexpressions evaluated with
21// a different degree mode. Rather than supporting copy from the
22// formula window, we may eventually want to support generation of a
23// more useful text version in a separate window. It's not clear
24// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080025
Justin Klaassen4b3af052014-05-27 17:53:10 -070026package com.android.calculator2;
27
28import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070029import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070030import android.animation.AnimatorListenerAdapter;
31import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070032import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070033import android.animation.PropertyValuesHolder;
Justin Klaassen4b3af052014-05-27 17:53:10 -070034import android.app.Activity;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070035import android.app.AlertDialog;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070036import android.content.ClipData;
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070037import android.content.DialogInterface;
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;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070047import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070048import android.text.Spanned;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070049import android.text.style.ForegroundColorSpan;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070050import android.text.TextUtils;
Justin Klaassen44595162015-05-28 17:55:20 -070051import android.util.Property;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070052import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070053import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080054import android.view.Menu;
55import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070056import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070057import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070058import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070059import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070060import android.view.ViewGroupOverlay;
Justin Klaassen4b3af052014-05-27 17:53:10 -070061import android.view.animation.AccelerateDecelerateInterpolator;
Justin Klaassenfed941a2014-06-09 18:42:40 +010062import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070063import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010064
Hans Boehm08e8f322015-04-21 13:18:38 -070065import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080066
67import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080068import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080069import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070070import java.io.ObjectInput;
71import java.io.ObjectInputStream;
72import java.io.ObjectOutput;
73import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070074
Justin Klaassen04f79c72014-06-27 17:25:35 -070075public class Calculator extends Activity
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070076 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
77 AlertDialogFragment.OnClickListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070078
79 /**
80 * Constant for an invalid resource id.
81 */
82 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070083
84 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080085 INPUT, // Result and formula both visible, no evaluation requested,
86 // Though result may be visible on bottom line.
87 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070088 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080089 INIT, // Very temporary state used as alternative to EVALUATE
90 // during reinitialization. Do not animate on completion.
91 ANIMATE, // Result computed, animation to enlarge result window in progress.
92 RESULT, // Result displayed, formula invisible.
93 // If we are in RESULT state, the formula was evaluated without
94 // error to initial precision.
95 ERROR // Error displayed: Formula visible, result shows error message.
96 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -070097 }
Hans Boehm84614952014-11-25 18:46:17 -080098 // Normal transition sequence is
99 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
100 // A RESULT -> ERROR transition is possible in rare corner cases, in which
101 // a higher precision evaluation exposes an error. This is possible, since we
102 // initially evaluate assuming we were given a well-defined problem. If we
103 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
104 // unless we are asked for enough precision that we can distinguish the argument from zero.
105 // TODO: Consider further heuristics to reduce the chance of observing this?
106 // It already seems to be observable only in contrived cases.
107 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
108 // is restarted in that state. This leads us to recompute and redisplay the result
109 // ASAP.
110 // TODO: Possibly save a bit more information, e.g. its initial display string
111 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700112
Justin Klaassen44595162015-05-28 17:55:20 -0700113 private final Property<TextView, Integer> TEXT_COLOR =
114 new Property<TextView, Integer>(Integer.class, "textColor") {
115 @Override
116 public Integer get(TextView textView) {
117 return textView.getCurrentTextColor();
118 }
119
120 @Override
121 public void set(TextView textView, Integer textColor) {
122 textView.setTextColor(textColor);
123 }
124 };
125
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700126 // We currently assume that the formula does not change out from under us in
127 // any way. We explicitly handle all input to the formula here.
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700128 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
129 @Override
130 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm1176f232015-05-11 16:26:03 -0700131 stopActionMode();
Justin Klaassen06c49442015-06-04 14:39:27 -0700132 // Never consume DPAD key events.
133 switch (keyCode) {
134 case KeyEvent.KEYCODE_DPAD_UP:
135 case KeyEvent.KEYCODE_DPAD_DOWN:
136 case KeyEvent.KEYCODE_DPAD_LEFT:
137 case KeyEvent.KEYCODE_DPAD_RIGHT:
138 return false;
139 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700140 // Always cancel unrequested in-progress evaluation, so that we don't have
141 // to worry about subsequent asynchronous completion.
142 // Requested in-progress evaluations are handled below.
143 if (mCurrentState != CalculatorState.EVALUATE) {
144 mEvaluator.cancelAll(true);
145 }
146 // In other cases we go ahead and process the input normally after cancelling:
Justin Klaassen06c49442015-06-04 14:39:27 -0700147 if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
148 return true;
149 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700150 switch (keyCode) {
151 case KeyEvent.KEYCODE_NUMPAD_ENTER:
152 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700153 case KeyEvent.KEYCODE_DPAD_CENTER:
154 mCurrentButton = mEqualButton;
155 onEquals();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700156 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700157 case KeyEvent.KEYCODE_DEL:
158 mCurrentButton = mDeleteButton;
159 onDelete();
160 return true;
161 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700162 cancelIfEvaluating(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700163 final int raw = keyEvent.getKeyCharacterMap()
Justin Klaassen44595162015-05-28 17:55:20 -0700164 .get(keyCode, keyEvent.getMetaState());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700165 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
166 return true; // discard
167 }
168 // Try to discard non-printing characters and the like.
169 // The user will have to explicitly delete other junk that gets past us.
170 if (Character.isIdentifierIgnorable(raw)
Justin Klaassen44595162015-05-28 17:55:20 -0700171 || Character.isWhitespace(raw)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700172 return true;
173 }
Justin Klaassen44595162015-05-28 17:55:20 -0700174 char c = (char) raw;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700175 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700176 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700177 onEquals();
178 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700179 addChars(String.valueOf(c), true);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700180 redisplayAfterFormulaChange();
181 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700182 }
183 return false;
184 }
185 };
186
Hans Boehm84614952014-11-25 18:46:17 -0800187 private static final String NAME = Calculator.class.getName();
188 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700189 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Hans Boehm84614952014-11-25 18:46:17 -0800190 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
191 // Associated value is a byte array holding both mCalculatorState
192 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700193
Justin Klaassen4b3af052014-05-27 17:53:10 -0700194 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800195 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700196
Justin Klaassen06360f92014-08-28 11:08:44 -0700197 private View mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700198 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700199 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700200 private CalculatorResult mResultText;
Justin Klaassend48b7562015-04-16 16:51:38 -0700201
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100202 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700203 private View mDeleteButton;
204 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700205 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700206
207 private TextView mInverseToggle;
208 private TextView mModeToggle;
209
Justin Klaassen721ec842015-05-28 14:30:08 -0700210 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700211 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700212
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700213 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700214 private Animator mCurrentAnimator;
215
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700216 // Characters that were recently entered at the end of the display that have not yet
217 // been added to the underlying expression.
218 private String mUnprocessedChars = null;
219
220 // Color to highlight unprocessed characters from physical keyboard.
221 // TODO: should probably match this to the error color?
222 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700223
Justin Klaassen4b3af052014-05-27 17:53:10 -0700224 @Override
225 protected void onCreate(Bundle savedInstanceState) {
226 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700227 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700228 setActionBar((Toolbar) findViewById(R.id.toolbar));
229
230 // Hide all default options in the ActionBar.
231 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700232
Justin Klaassen06360f92014-08-28 11:08:44 -0700233 mDisplayView = findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700234 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700235 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700236 mResultText = (CalculatorResult) findViewById(R.id.result);
Justin Klaassend48b7562015-04-16 16:51:38 -0700237
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100238 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700239 mDeleteButton = findViewById(R.id.del);
240 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700241 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
242 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
243 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
244 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700245
246 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
247 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
248
Justin Klaassen721ec842015-05-28 14:30:08 -0700249 mInvertibleButtons = new View[] {
250 findViewById(R.id.fun_sin),
251 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700252 findViewById(R.id.fun_tan),
253 findViewById(R.id.fun_ln),
254 findViewById(R.id.fun_log),
255 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700256 };
257 mInverseButtons = new View[] {
258 findViewById(R.id.fun_arcsin),
259 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700260 findViewById(R.id.fun_arctan),
261 findViewById(R.id.fun_exp),
262 findViewById(R.id.fun_10pow),
263 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700264 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700265
Justin Klaassen44595162015-05-28 17:55:20 -0700266 mEvaluator = new Evaluator(this, mResultText);
267 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700268 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700269
Hans Boehm84614952014-11-25 18:46:17 -0800270 if (savedInstanceState != null) {
271 setState(CalculatorState.values()[
272 savedInstanceState.getInt(KEY_DISPLAY_STATE,
273 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700274 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
275 if (unprocessed != null) {
276 mUnprocessedChars = unprocessed.toString();
277 }
278 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800279 if (state != null) {
280 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
281 mEvaluator.restoreInstanceState(in);
282 } catch (Throwable ignored) {
283 // When in doubt, revert to clean state
284 mCurrentState = CalculatorState.INPUT;
285 mEvaluator.clear();
286 }
287 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700288 } else {
289 mCurrentState = CalculatorState.INPUT;
290 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800291 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700292
Hans Boehm08e8f322015-04-21 13:18:38 -0700293 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
294 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700295 mFormulaText.setOnPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700296 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700297
298 onInverseToggled(mInverseToggle.isSelected());
299 onModeChanged(mEvaluator.getDegreeMode());
300
Hans Boehm84614952014-11-25 18:46:17 -0800301 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700302 // Just reevaluate.
303 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800304 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800305 mEvaluator.requireResult();
306 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700307 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800308 }
309 // TODO: We're currently not saving and restoring scroll position.
310 // We probably should. Details may require care to deal with:
311 // - new display size
312 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700313 }
314
315 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700316 protected void onSaveInstanceState(@NonNull Bundle outState) {
317 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
318 if (mCurrentAnimator != null) {
319 mCurrentAnimator.cancel();
320 }
321
Justin Klaassen4b3af052014-05-27 17:53:10 -0700322 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800323 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700324 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800325 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
326 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
327 mEvaluator.saveInstanceState(out);
328 } catch (IOException e) {
329 // Impossible; No IO involved.
330 throw new AssertionError("Impossible IO exception", e);
331 }
332 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700333 }
334
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700335 // Set the state, updating delete label and display colors.
336 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700337 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700338 private void setState(CalculatorState state) {
339 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800340 if (state == CalculatorState.INPUT) {
341 restoreDisplayPositions();
342 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700343 mCurrentState = state;
344
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700345 if (mCurrentState == CalculatorState.RESULT) {
346 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700347 mDeleteButton.setVisibility(View.GONE);
348 mClearButton.setVisibility(View.VISIBLE);
349 } else {
350 mDeleteButton.setVisibility(View.VISIBLE);
351 mClearButton.setVisibility(View.GONE);
352 }
353
Hans Boehm84614952014-11-25 18:46:17 -0800354 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700355 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700356 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700357 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700358 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700359 } else if (mCurrentState != CalculatorState.RESULT) {
360 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
361 mResultText.setTextColor(getColor(R.color.display_result_text_color));
362 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700363 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700364
365 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700366 }
367 }
368
Hans Boehm1176f232015-05-11 16:26:03 -0700369 // Stop any active ActionMode. Return true if there was one.
370 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700371 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700372 return true;
373 }
374 if (mFormulaText.stopActionMode()) {
375 return true;
376 }
377 return false;
378 }
379
Justin Klaassen4b3af052014-05-27 17:53:10 -0700380 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100381 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700382 if (!stopActionMode()) {
383 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
384 // Select the previous pad.
385 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
386 } else {
387 // If the user is currently looking at the first pad (or the pad is not paged),
388 // allow the system to handle the Back button.
389 super.onBackPressed();
390 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100391 }
392 }
393
394 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700395 public void onUserInteraction() {
396 super.onUserInteraction();
397
Hans Boehmc1ea0912015-06-19 15:05:07 -0700398 // If there's an animation in progress, end it immediately, so the user interaction can
399 // be handled.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700400 if (mCurrentAnimator != null) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700401 mCurrentAnimator.end();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700402 }
403 }
404
Justin Klaassene2711cb2015-05-28 11:13:17 -0700405 /**
406 * Invoked whenever the inverse button is toggled to update the UI.
407 *
408 * @param showInverse {@code true} if inverse functions should be shown
409 */
410 private void onInverseToggled(boolean showInverse) {
411 if (showInverse) {
412 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700413 for (View invertibleButton : mInvertibleButtons) {
414 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700415 }
416 for (View inverseButton : mInverseButtons) {
417 inverseButton.setVisibility(View.VISIBLE);
418 }
419 } else {
420 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700421 for (View invertibleButton : mInvertibleButtons) {
422 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700423 }
424 for (View inverseButton : mInverseButtons) {
425 inverseButton.setVisibility(View.GONE);
426 }
427 }
428 }
429
430 /**
431 * Invoked whenever the deg/rad mode may have changed to update the UI.
432 *
433 * @param degreeMode {@code true} if in degree mode
434 */
435 private void onModeChanged(boolean degreeMode) {
436 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700437 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700438 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
439
440 mModeToggle.setText(R.string.mode_rad);
441 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700442 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700443 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700444 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
445
446 mModeToggle.setText(R.string.mode_deg);
447 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700448 }
449 }
Hans Boehm84614952014-11-25 18:46:17 -0800450
Hans Boehm5d79d102015-09-16 16:33:47 -0700451 /**
452 * Switch to INPUT from RESULT state in response to input of the specified button_id.
453 * View.NO_ID is treated as an incomplete function id.
454 */
455 private void switchToInput(int button_id) {
456 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
457 mEvaluator.collapse();
458 } else {
459 announceClearedForAccessibility();
460 mEvaluator.clear();
461 }
462 setState(CalculatorState.INPUT);
463 }
464
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700465 // Add the given button id to input expression.
466 // If appropriate, clear the expression before doing so.
467 private void addKeyToExpr(int id) {
468 if (mCurrentState == CalculatorState.ERROR) {
469 setState(CalculatorState.INPUT);
470 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700471 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700472 }
473 if (!mEvaluator.append(id)) {
474 // TODO: Some user visible feedback?
475 }
476 }
477
Hans Boehm017de982015-06-10 17:46:03 -0700478 /**
479 * Add the given button id to input expression, assuming it was explicitly
480 * typed/touched.
481 * We perform slightly more aggressive correction than in pasted expressions.
482 */
483 private void addExplicitKeyToExpr(int id) {
484 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
485 mEvaluator.getExpr().removeTrailingAdditiveOperators();
486 }
487 addKeyToExpr(id);
488 }
489
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700490 private void redisplayAfterFormulaChange() {
491 // TODO: Could do this more incrementally.
492 redisplayFormula();
493 setState(CalculatorState.INPUT);
Hans Boehmc023b732015-04-29 11:30:47 -0700494 if (mEvaluator.getExpr().hasInterestingOps()) {
495 mEvaluator.evaluateAndShowResult();
496 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700497 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700498 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700499 }
500
Justin Klaassen4b3af052014-05-27 17:53:10 -0700501 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700502 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700503 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700504 stopActionMode();
Hans Boehmc1ea0912015-06-19 15:05:07 -0700505 // See onKey above for the rationale behind some of the behavior below:
506 if (mCurrentState != CalculatorState.EVALUATE) {
507 // Cancel evaluations that were not specifically requested.
508 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800509 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700510 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800511 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700512 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700513 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700514 break;
515 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700516 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700517 break;
518 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700519 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700520 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700521 case R.id.toggle_inv:
522 final boolean selected = !mInverseToggle.isSelected();
523 mInverseToggle.setSelected(selected);
524 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700525 if (mCurrentState == CalculatorState.RESULT) {
526 mResultText.redisplay(); // In case we cancelled reevaluation.
527 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700528 break;
529 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700530 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700531 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700532 if (mCurrentState == CalculatorState.RESULT) {
533 mEvaluator.collapse(); // Capture result evaluated in old mode
534 redisplayFormula();
535 }
536 // In input mode, we reinterpret already entered trig functions.
537 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700538 onModeChanged(mode);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700539 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700540 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700541 if (mEvaluator.getExpr().hasInterestingOps()) {
542 mEvaluator.evaluateAndShowResult();
543 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700544 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700545 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700546 cancelIfEvaluating(false);
Hans Boehm017de982015-06-10 17:46:03 -0700547 addExplicitKeyToExpr(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700548 redisplayAfterFormulaChange();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700549 break;
550 }
551 }
552
Hans Boehm84614952014-11-25 18:46:17 -0800553 void redisplayFormula() {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700554 SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700555 if (mUnprocessedChars != null) {
556 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700557 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
558 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700559 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700560 mFormulaText.changeTextTo(formula);
Hans Boehm84614952014-11-25 18:46:17 -0800561 }
562
Justin Klaassen4b3af052014-05-27 17:53:10 -0700563 @Override
564 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700565 mCurrentButton = view;
566
Justin Klaassen4b3af052014-05-27 17:53:10 -0700567 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700568 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700569 return true;
570 }
571 return false;
572 }
573
Hans Boehm84614952014-11-25 18:46:17 -0800574 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700575 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
576 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700577 // Invalidate any options that may depend on the current result.
578 invalidateOptionsMenu();
579
Hans Boehma0e45f32015-05-30 13:20:35 -0700580 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700581 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800582 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700583 }
Hans Boehm84614952014-11-25 18:46:17 -0800584 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700585
Hans Boehmc1ea0912015-06-19 15:05:07 -0700586 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800587 public void onCancelled() {
588 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800589 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700590 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800591 }
592
593 // Reevaluation completed; ask result to redisplay current value.
594 public void onReevaluate()
595 {
Justin Klaassen44595162015-05-28 17:55:20 -0700596 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700597 }
598
Justin Klaassenfed941a2014-06-09 18:42:40 +0100599 @Override
600 public void onTextSizeChanged(final TextView textView, float oldSize) {
601 if (mCurrentState != CalculatorState.INPUT) {
602 // Only animate text changes that occur from user input.
603 return;
604 }
605
606 // Calculate the values needed to perform the scale and translation animations,
607 // maintaining the same apparent baseline for the displayed text.
608 final float textScale = oldSize / textView.getTextSize();
609 final float translationX = (1.0f - textScale) *
610 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
611 final float translationY = (1.0f - textScale) *
612 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
613
614 final AnimatorSet animatorSet = new AnimatorSet();
615 animatorSet.playTogether(
616 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
617 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
618 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
619 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700620 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100621 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
622 animatorSet.start();
623 }
624
Hans Boehmc1ea0912015-06-19 15:05:07 -0700625 /**
626 * Cancel any in-progress explicitly requested evaluations.
627 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
628 value, and certainly changes the display, so it seems reasonable to warn.
629 * @return true if there was such an evaluation
630 */
631 private boolean cancelIfEvaluating(boolean quiet) {
632 if (mCurrentState == CalculatorState.EVALUATE) {
633 mEvaluator.cancelAll(quiet);
634 return true;
635 } else {
636 return false;
637 }
638 }
639
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700640 private void onEquals() {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700641 // In non-INPUT state assume this was redundant and ignore it.
Hans Boehmc023b732015-04-29 11:30:47 -0700642 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700643 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800644 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700645 }
646 }
647
648 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700649 // Delete works like backspace; remove the last character or operator from the expression.
650 // Note that we handle keyboard delete exactly like the delete button. For
651 // example the delete button can be used to delete a character from an incomplete
652 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700653 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700654 // If there is an in-progress explicit evaluation, just cancel it and return.
655 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700656 setState(CalculatorState.INPUT);
657 if (mUnprocessedChars != null) {
658 int len = mUnprocessedChars.length();
659 if (len > 0) {
660 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
661 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700662 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700663 }
664 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700665 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700666 }
Hans Boehmdb6f9992015-08-19 12:32:56 -0700667 if (mEvaluator.getExpr().isEmpty()
668 && (mUnprocessedChars == null || mUnprocessedChars.isEmpty())) {
669 // Resulting formula won't be announced, since it's empty.
670 announceClearedForAccessibility();
671 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700672 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700673 }
674
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700675 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700676 final ViewGroupOverlay groupOverlay =
677 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700678
679 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700680 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700681
682 // Make reveal cover the display and status bar.
683 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700684 revealView.setBottom(displayRect.bottom);
685 revealView.setLeft(displayRect.left);
686 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700687 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700688 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700689
Justin Klaassen4b3af052014-05-27 17:53:10 -0700690 final int[] clearLocation = new int[2];
691 sourceView.getLocationInWindow(clearLocation);
692 clearLocation[0] += sourceView.getWidth() / 2;
693 clearLocation[1] += sourceView.getHeight() / 2;
694
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700695 final int revealCenterX = clearLocation[0] - revealView.getLeft();
696 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700697
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700698 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
699 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
700 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700701 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
702
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700703 final Animator revealAnimator =
704 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700705 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700706 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700707 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700708 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700709
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700710 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700711 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700712 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700713
714 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700715 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700716 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
717 animatorSet.addListener(new AnimatorListenerAdapter() {
718 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700719 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700720 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700721 mCurrentAnimator = null;
722 }
723 });
724
725 mCurrentAnimator = animatorSet;
726 animatorSet.start();
727 }
728
Hans Boehmdb6f9992015-08-19 12:32:56 -0700729 private void announceClearedForAccessibility() {
730 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700731 }
732
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700733 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800734 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700735 return;
736 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700737 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -0700738 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700739 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
740 @Override
741 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700742 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700743 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700744 mEvaluator.clear();
745 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800746 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700747 }
748 });
749 }
750
Hans Boehm84614952014-11-25 18:46:17 -0800751 // Evaluation encountered en error. Display the error.
752 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700753 if (mCurrentState == CalculatorState.EVALUATE) {
754 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -0700755 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -0700756 reveal(mCurrentButton, R.color.calculator_error_color,
757 new AnimatorListenerAdapter() {
758 @Override
759 public void onAnimationEnd(Animator animation) {
760 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700761 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700762 }
763 });
764 } else if (mCurrentState == CalculatorState.INIT) {
765 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700766 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700767 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700768 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700769 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700770 }
771
Hans Boehm84614952014-11-25 18:46:17 -0800772
773 // Animate movement of result into the top formula slot.
774 // Result window now remains translated in the top slot while the result is displayed.
775 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700776 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800777 // formula and result displays back at the end of the animation. We no longer do that,
778 // so that we can continue to properly support scrolling of the result.
779 // We assume the result already contains the text to be expanded.
780 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700781 // Calculate the textSize that would be used to display the result in the formula.
782 // For scrollable results just use the minimum textSize to maximize the number of digits
783 // that are visible on screen.
784 float textSize = mFormulaText.getMinimumTextSize();
785 if (!mResultText.isScrollable()) {
786 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
787 }
788
789 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
790 // when a result is reused in a subsequent expression.
791 final float resultScale = textSize / mResultText.getTextSize();
792
793 // Set the result's pivot to match its gravity.
794 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
795 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
796
797 // Calculate the necessary translations so the result takes the place of the formula and
798 // the formula moves off the top of the screen.
799 final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
800 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
Hans Boehm08e8f322015-04-21 13:18:38 -0700801 final float formulaTranslationY = -mFormulaText.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700802
Justin Klaassen44595162015-05-28 17:55:20 -0700803 // Change the result's textColor to match the formula.
804 final int formulaTextColor = mFormulaText.getCurrentTextColor();
805
Hans Boehm84614952014-11-25 18:46:17 -0800806 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -0700807 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
808 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -0700809 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800810 final AnimatorSet animatorSet = new AnimatorSet();
811 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700812 ObjectAnimator.ofPropertyValuesHolder(mResultText,
813 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
814 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
815 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
816 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
817 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
818 animatorSet.setDuration(getResources().getInteger(
819 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800820 animatorSet.addListener(new AnimatorListenerAdapter() {
821 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800822 public void onAnimationEnd(Animator animation) {
823 setState(CalculatorState.RESULT);
824 mCurrentAnimator = null;
825 }
826 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700827
Hans Boehm84614952014-11-25 18:46:17 -0800828 mCurrentAnimator = animatorSet;
829 animatorSet.start();
830 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700831 mResultText.setScaleX(resultScale);
832 mResultText.setScaleY(resultScale);
833 mResultText.setTranslationY(resultTranslationY);
834 mResultText.setTextColor(formulaTextColor);
Hans Boehm08e8f322015-04-21 13:18:38 -0700835 mFormulaText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700836 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800837 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700838 }
Hans Boehm84614952014-11-25 18:46:17 -0800839
840 // Restore positions of the formula and result displays back to their original,
841 // pre-animation state.
842 private void restoreDisplayPositions() {
843 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700844 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800845 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700846 mResultText.setScaleX(1.0f);
847 mResultText.setScaleY(1.0f);
848 mResultText.setTranslationX(0.0f);
849 mResultText.setTranslationY(0.0f);
Hans Boehm08e8f322015-04-21 13:18:38 -0700850 mFormulaText.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800851
Hans Boehm08e8f322015-04-21 13:18:38 -0700852 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700853 }
854
855 @Override
856 public void onClick(AlertDialogFragment fragment, int which) {
857 if (which == DialogInterface.BUTTON_POSITIVE) {
858 // Timeout extension request.
859 mEvaluator.setLongTimeOut();
860 }
861 }
Hans Boehm84614952014-11-25 18:46:17 -0800862
Justin Klaassend48b7562015-04-16 16:51:38 -0700863 @Override
864 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700865 super.onCreateOptionsMenu(menu);
866
867 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700868 return true;
869 }
870
871 @Override
872 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700873 super.onPrepareOptionsMenu(menu);
874
875 // Show the leading option when displaying a result.
876 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
877
878 // Show the fraction option when displaying a rational result.
879 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
880 && mEvaluator.getRational() != null);
881
Justin Klaassend48b7562015-04-16 16:51:38 -0700882 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800883 }
884
885 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700886 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800887 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700888 case R.id.menu_leading:
889 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800890 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700891 case R.id.menu_fraction:
892 displayFraction();
893 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700894 case R.id.menu_licenses:
895 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700896 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800897 default:
898 return super.onOptionsItemSelected(item);
899 }
900 }
901
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700902 private void displayMessage(String s) {
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700903 AlertDialogFragment.showMessageDialog(this, s, null);
Hans Boehm84614952014-11-25 18:46:17 -0800904 }
905
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700906 private void displayFraction() {
907 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700908 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700909 }
910
911 // Display full result to currently evaluated precision
912 private void displayFull() {
913 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -0700914 String msg = mResultText.getFullText() + " ";
915 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700916 msg += res.getString(R.string.exact);
917 } else {
918 msg += res.getString(R.string.approximate);
919 }
920 displayMessage(msg);
921 }
922
Hans Boehm017de982015-06-10 17:46:03 -0700923 /**
924 * Add input characters to the end of the expression.
925 * Map them to the appropriate button pushes when possible. Leftover characters
926 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
927 * added characters.
928 * @param moreChars Characters to be added.
Hans Boehm0b9806f2015-06-29 16:07:15 -0700929 * @param explicit These characters were explicitly typed by the user, not pasted.
Hans Boehm017de982015-06-10 17:46:03 -0700930 */
931 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700932 if (mUnprocessedChars != null) {
933 moreChars = mUnprocessedChars + moreChars;
934 }
935 int current = 0;
936 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -0700937 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -0700938 if (mCurrentState == CalculatorState.RESULT && len != 0) {
939 // Clear display immediately for incomplete function name.
940 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
941 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700942 while (current < len) {
943 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -0700944 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -0700945 if (!explicit) {
946 int expEnd;
947 if (lastWasDigit && current !=
948 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
949 // Process scientific notation with 'E' when pasting, in spite of ambiguity
950 // with base of natural log.
951 // Otherwise the 10^x key is the user's friend.
952 mEvaluator.addExponent(moreChars, current, expEnd);
953 current = expEnd;
954 lastWasDigit = false;
955 continue;
956 } else {
957 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
958 if (current == 0 && (isDigit || k == R.id.dec_point)
959 && mEvaluator.getExpr().hasTrailingConstant()) {
960 // Refuse to concatenate pasted content to trailing constant.
961 // This makes pasting of calculator results more consistent, whether or
962 // not the old calculator instance is still around.
963 addKeyToExpr(R.id.op_mul);
964 }
965 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
966 }
967 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700968 if (k != View.NO_ID) {
969 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -0700970 if (explicit) {
971 addExplicitKeyToExpr(k);
972 } else {
973 addKeyToExpr(k);
974 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700975 if (Character.isSurrogate(c)) {
976 current += 2;
977 } else {
978 ++current;
979 }
980 continue;
981 }
Hans Boehm013969e2015-04-13 20:29:47 -0700982 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700983 if (f != View.NO_ID) {
984 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -0700985 if (explicit) {
986 addExplicitKeyToExpr(f);
987 } else {
988 addKeyToExpr(f);
989 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700990 if (f == R.id.op_sqrt) {
991 // Square root entered as function; don't lose the parenthesis.
992 addKeyToExpr(R.id.lparen);
993 }
994 current = moreChars.indexOf('(', current) + 1;
995 continue;
996 }
997 // There are characters left, but we can't convert them to button presses.
998 mUnprocessedChars = moreChars.substring(current);
999 redisplayAfterFormulaChange();
1000 return;
1001 }
1002 mUnprocessedChars = null;
1003 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -08001004 }
1005
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001006 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001007 public boolean onPaste(ClipData clip) {
1008 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1009 if (item == null) {
1010 // nothing to paste, bail early...
1011 return false;
1012 }
1013
1014 // Check if the item is a previously copied result, otherwise paste as raw text.
1015 final Uri uri = item.getUri();
1016 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001017 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001018 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001019 setState(CalculatorState.INPUT);
1020 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -08001021 }
Hans Boehm3666e632015-07-27 18:33:12 -07001022 mEvaluator.appendSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001023 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001024 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001025 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001026 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001027 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001028 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001029}