blob: 51a7572d9f349fff02f5c871aa84b36a03c0072b [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 Klaassen9d33cdc2016-02-21 14:16:14 -080034import android.app.ActionBar;
Justin Klaassen4b3af052014-05-27 17:53:10 -070035import android.app.Activity;
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 Boehm8a4f81c2015-07-09 10:41:25 -070046import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070047import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080048import android.text.TextUtils;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070049import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070050import android.util.Property;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070051import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070052import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080053import android.view.Menu;
54import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070055import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070056import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070057import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070058import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070059import android.view.ViewGroupOverlay;
Justin Klaassen4b3af052014-05-27 17:53:10 -070060import android.view.animation.AccelerateDecelerateInterpolator;
Justin Klaassenfed941a2014-06-09 18:42:40 +010061import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070062import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010063
Hans Boehm08e8f322015-04-21 13:18:38 -070064import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080065
66import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080067import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080068import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070069import java.io.ObjectInput;
70import java.io.ObjectInputStream;
71import java.io.ObjectOutput;
72import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070073
Justin Klaassen04f79c72014-06-27 17:25:35 -070074public class Calculator extends Activity
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070075 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
76 AlertDialogFragment.OnClickListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070077
78 /**
79 * Constant for an invalid resource id.
80 */
81 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070082
83 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080084 INPUT, // Result and formula both visible, no evaluation requested,
85 // Though result may be visible on bottom line.
86 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070087 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080088 INIT, // Very temporary state used as alternative to EVALUATE
89 // during reinitialization. Do not animate on completion.
90 ANIMATE, // Result computed, animation to enlarge result window in progress.
91 RESULT, // Result displayed, formula invisible.
92 // If we are in RESULT state, the formula was evaluated without
93 // error to initial precision.
94 ERROR // Error displayed: Formula visible, result shows error message.
95 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -070096 }
Hans Boehm84614952014-11-25 18:46:17 -080097 // Normal transition sequence is
98 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
99 // A RESULT -> ERROR transition is possible in rare corner cases, in which
100 // a higher precision evaluation exposes an error. This is possible, since we
101 // initially evaluate assuming we were given a well-defined problem. If we
102 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
103 // unless we are asked for enough precision that we can distinguish the argument from zero.
104 // TODO: Consider further heuristics to reduce the chance of observing this?
105 // It already seems to be observable only in contrived cases.
106 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
107 // is restarted in that state. This leads us to recompute and redisplay the result
108 // ASAP.
109 // TODO: Possibly save a bit more information, e.g. its initial display string
110 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700111
Justin Klaassen44595162015-05-28 17:55:20 -0700112 private final Property<TextView, Integer> TEXT_COLOR =
113 new Property<TextView, Integer>(Integer.class, "textColor") {
114 @Override
115 public Integer get(TextView textView) {
116 return textView.getCurrentTextColor();
117 }
118
119 @Override
120 public void set(TextView textView, Integer textColor) {
121 textView.setTextColor(textColor);
122 }
123 };
124
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700125 // We currently assume that the formula does not change out from under us in
126 // any way. We explicitly handle all input to the formula here.
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700127 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
128 @Override
129 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm1176f232015-05-11 16:26:03 -0700130 stopActionMode();
Justin Klaassen06c49442015-06-04 14:39:27 -0700131 // Never consume DPAD key events.
132 switch (keyCode) {
133 case KeyEvent.KEYCODE_DPAD_UP:
134 case KeyEvent.KEYCODE_DPAD_DOWN:
135 case KeyEvent.KEYCODE_DPAD_LEFT:
136 case KeyEvent.KEYCODE_DPAD_RIGHT:
137 return false;
138 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700139 // Always cancel unrequested in-progress evaluation, so that we don't have
140 // to worry about subsequent asynchronous completion.
141 // Requested in-progress evaluations are handled below.
142 if (mCurrentState != CalculatorState.EVALUATE) {
143 mEvaluator.cancelAll(true);
144 }
145 // In other cases we go ahead and process the input normally after cancelling:
Justin Klaassen06c49442015-06-04 14:39:27 -0700146 if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
147 return true;
148 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700149 switch (keyCode) {
150 case KeyEvent.KEYCODE_NUMPAD_ENTER:
151 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700152 case KeyEvent.KEYCODE_DPAD_CENTER:
153 mCurrentButton = mEqualButton;
154 onEquals();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800155 break;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700156 case KeyEvent.KEYCODE_DEL:
157 mCurrentButton = mDeleteButton;
158 onDelete();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800159 break;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700160 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700161 cancelIfEvaluating(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700162 final int raw = keyEvent.getKeyCharacterMap()
Justin Klaassen44595162015-05-28 17:55:20 -0700163 .get(keyCode, keyEvent.getMetaState());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700164 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
165 return true; // discard
166 }
167 // Try to discard non-printing characters and the like.
168 // The user will have to explicitly delete other junk that gets past us.
169 if (Character.isIdentifierIgnorable(raw)
Justin Klaassen44595162015-05-28 17:55:20 -0700170 || Character.isWhitespace(raw)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700171 return true;
172 }
Justin Klaassen44595162015-05-28 17:55:20 -0700173 char c = (char) raw;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700174 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700175 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700176 onEquals();
177 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700178 addChars(String.valueOf(c), true);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700179 redisplayAfterFormulaChange();
180 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700181 }
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800182 return true;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700183 }
184 };
185
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800186 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800187 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700188 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800189 /**
190 * Associated value is a byte array holding the evaluator state.
191 */
Hans Boehm84614952014-11-25 18:46:17 -0800192 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800193 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Justin Klaassen741471e2014-06-11 09:43:44 -0700194
Justin Klaassen4b3af052014-05-27 17:53:10 -0700195 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800196 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700197
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800198 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700199 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700200 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700201 private CalculatorResult mResultText;
Justin Klaassend48b7562015-04-16 16:51:38 -0700202
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100203 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700204 private View mDeleteButton;
205 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700206 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700207
208 private TextView mInverseToggle;
209 private TextView mModeToggle;
210
Justin Klaassen721ec842015-05-28 14:30:08 -0700211 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700212 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700213
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700214 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700215 private Animator mCurrentAnimator;
216
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700217 // Characters that were recently entered at the end of the display that have not yet
218 // been added to the underlying expression.
219 private String mUnprocessedChars = null;
220
221 // Color to highlight unprocessed characters from physical keyboard.
222 // TODO: should probably match this to the error color?
223 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700224
Justin Klaassen4b3af052014-05-27 17:53:10 -0700225 @Override
226 protected void onCreate(Bundle savedInstanceState) {
227 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700228 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700229 setActionBar((Toolbar) findViewById(R.id.toolbar));
230
231 // Hide all default options in the ActionBar.
232 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700233
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800234 // Ensure the toolbar stays visible while the options menu is displayed.
235 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
236 @Override
237 public void onMenuVisibilityChanged(boolean isVisible) {
238 mDisplayView.setForceToolbarVisible(isVisible);
239 }
240 });
241
242 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700243 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700244 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700245 mResultText = (CalculatorResult) findViewById(R.id.result);
Justin Klaassend48b7562015-04-16 16:51:38 -0700246
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100247 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700248 mDeleteButton = findViewById(R.id.del);
249 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700250 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
251 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
252 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
253 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700254
255 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
256 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
257
Justin Klaassen721ec842015-05-28 14:30:08 -0700258 mInvertibleButtons = new View[] {
259 findViewById(R.id.fun_sin),
260 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700261 findViewById(R.id.fun_tan),
262 findViewById(R.id.fun_ln),
263 findViewById(R.id.fun_log),
264 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700265 };
266 mInverseButtons = new View[] {
267 findViewById(R.id.fun_arcsin),
268 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700269 findViewById(R.id.fun_arctan),
270 findViewById(R.id.fun_exp),
271 findViewById(R.id.fun_10pow),
272 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700273 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700274
Justin Klaassen44595162015-05-28 17:55:20 -0700275 mEvaluator = new Evaluator(this, mResultText);
276 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700277 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700278
Hans Boehm84614952014-11-25 18:46:17 -0800279 if (savedInstanceState != null) {
280 setState(CalculatorState.values()[
281 savedInstanceState.getInt(KEY_DISPLAY_STATE,
282 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700283 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
284 if (unprocessed != null) {
285 mUnprocessedChars = unprocessed.toString();
286 }
287 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800288 if (state != null) {
289 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
290 mEvaluator.restoreInstanceState(in);
291 } catch (Throwable ignored) {
292 // When in doubt, revert to clean state
293 mCurrentState = CalculatorState.INPUT;
294 mEvaluator.clear();
295 }
296 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700297 } else {
298 mCurrentState = CalculatorState.INPUT;
299 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800300 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700301
Hans Boehm08e8f322015-04-21 13:18:38 -0700302 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
303 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700304 mFormulaText.setOnPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700305 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700306
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800307 onInverseToggled(savedInstanceState != null
308 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Justin Klaassene2711cb2015-05-28 11:13:17 -0700309 onModeChanged(mEvaluator.getDegreeMode());
310
Hans Boehm84614952014-11-25 18:46:17 -0800311 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700312 // Just reevaluate.
313 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800314 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800315 mEvaluator.requireResult();
316 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700317 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800318 }
319 // TODO: We're currently not saving and restoring scroll position.
320 // We probably should. Details may require care to deal with:
321 // - new display size
322 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700323 }
324
325 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800326 protected void onResume() {
327 super.onResume();
328
329 // Always show the toolbar initially on launch.
330 mDisplayView.showToolbar();
331 }
332
333 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700334 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800335 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700336 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
337 if (mCurrentAnimator != null) {
338 mCurrentAnimator.cancel();
339 }
340
Justin Klaassen4b3af052014-05-27 17:53:10 -0700341 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800342 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700343 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800344 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
345 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
346 mEvaluator.saveInstanceState(out);
347 } catch (IOException e) {
348 // Impossible; No IO involved.
349 throw new AssertionError("Impossible IO exception", e);
350 }
351 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800352 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700353 }
354
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700355 // Set the state, updating delete label and display colors.
356 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700357 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700358 private void setState(CalculatorState state) {
359 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800360 if (state == CalculatorState.INPUT) {
361 restoreDisplayPositions();
362 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700363 mCurrentState = state;
364
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700365 if (mCurrentState == CalculatorState.RESULT) {
366 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700367 mDeleteButton.setVisibility(View.GONE);
368 mClearButton.setVisibility(View.VISIBLE);
369 } else {
370 mDeleteButton.setVisibility(View.VISIBLE);
371 mClearButton.setVisibility(View.GONE);
372 }
373
Hans Boehm84614952014-11-25 18:46:17 -0800374 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700375 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700376 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700377 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700378 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700379 } else if (mCurrentState != CalculatorState.RESULT) {
380 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
381 mResultText.setTextColor(getColor(R.color.display_result_text_color));
382 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700383 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700384
385 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700386 }
387 }
388
Hans Boehm1176f232015-05-11 16:26:03 -0700389 // Stop any active ActionMode. Return true if there was one.
390 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700391 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700392 return true;
393 }
394 if (mFormulaText.stopActionMode()) {
395 return true;
396 }
397 return false;
398 }
399
Justin Klaassen4b3af052014-05-27 17:53:10 -0700400 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100401 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700402 if (!stopActionMode()) {
403 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
404 // Select the previous pad.
405 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
406 } else {
407 // If the user is currently looking at the first pad (or the pad is not paged),
408 // allow the system to handle the Back button.
409 super.onBackPressed();
410 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100411 }
412 }
413
414 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700415 public void onUserInteraction() {
416 super.onUserInteraction();
417
Hans Boehmc1ea0912015-06-19 15:05:07 -0700418 // If there's an animation in progress, end it immediately, so the user interaction can
419 // be handled.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700420 if (mCurrentAnimator != null) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700421 mCurrentAnimator.end();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700422 }
423 }
424
Justin Klaassene2711cb2015-05-28 11:13:17 -0700425 /**
426 * Invoked whenever the inverse button is toggled to update the UI.
427 *
428 * @param showInverse {@code true} if inverse functions should be shown
429 */
430 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800431 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700432 if (showInverse) {
433 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700434 for (View invertibleButton : mInvertibleButtons) {
435 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700436 }
437 for (View inverseButton : mInverseButtons) {
438 inverseButton.setVisibility(View.VISIBLE);
439 }
440 } else {
441 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700442 for (View invertibleButton : mInvertibleButtons) {
443 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700444 }
445 for (View inverseButton : mInverseButtons) {
446 inverseButton.setVisibility(View.GONE);
447 }
448 }
449 }
450
451 /**
452 * Invoked whenever the deg/rad mode may have changed to update the UI.
453 *
454 * @param degreeMode {@code true} if in degree mode
455 */
456 private void onModeChanged(boolean degreeMode) {
457 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700458 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700459 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
460
461 mModeToggle.setText(R.string.mode_rad);
462 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700463 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700464 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700465 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
466
467 mModeToggle.setText(R.string.mode_deg);
468 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700469 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800470
471 // Show the toolbar to highlight the mode change.
472 mDisplayView.showToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700473 }
Hans Boehm84614952014-11-25 18:46:17 -0800474
Hans Boehm5d79d102015-09-16 16:33:47 -0700475 /**
476 * Switch to INPUT from RESULT state in response to input of the specified button_id.
477 * View.NO_ID is treated as an incomplete function id.
478 */
479 private void switchToInput(int button_id) {
480 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
481 mEvaluator.collapse();
482 } else {
483 announceClearedForAccessibility();
484 mEvaluator.clear();
485 }
486 setState(CalculatorState.INPUT);
487 }
488
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700489 // Add the given button id to input expression.
490 // If appropriate, clear the expression before doing so.
491 private void addKeyToExpr(int id) {
492 if (mCurrentState == CalculatorState.ERROR) {
493 setState(CalculatorState.INPUT);
494 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700495 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700496 }
497 if (!mEvaluator.append(id)) {
498 // TODO: Some user visible feedback?
499 }
500 }
501
Hans Boehm017de982015-06-10 17:46:03 -0700502 /**
503 * Add the given button id to input expression, assuming it was explicitly
504 * typed/touched.
505 * We perform slightly more aggressive correction than in pasted expressions.
506 */
507 private void addExplicitKeyToExpr(int id) {
508 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
509 mEvaluator.getExpr().removeTrailingAdditiveOperators();
510 }
511 addKeyToExpr(id);
512 }
513
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700514 private void redisplayAfterFormulaChange() {
515 // TODO: Could do this more incrementally.
516 redisplayFormula();
517 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800518 if (haveUnprocessed()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700519 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800520 // Force reevaluation when text is deleted, even if expression is unchanged.
521 mEvaluator.touch();
522 } else {
523 if (mEvaluator.getExpr().hasInterestingOps()) {
524 mEvaluator.evaluateAndShowResult();
525 } else {
526 mResultText.clear();
527 }
Hans Boehmc023b732015-04-29 11:30:47 -0700528 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700529 }
530
Justin Klaassen4b3af052014-05-27 17:53:10 -0700531 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700532 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700533 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700534 stopActionMode();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800535
536 // Attempt to hide the toolbar whenever an interaction has occurred.
537 mDisplayView.hideToolbar();
538
Hans Boehmc1ea0912015-06-19 15:05:07 -0700539 // See onKey above for the rationale behind some of the behavior below:
540 if (mCurrentState != CalculatorState.EVALUATE) {
541 // Cancel evaluations that were not specifically requested.
542 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800543 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800544
Justin Klaassend48b7562015-04-16 16:51:38 -0700545 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800546 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700547 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700548 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700549 break;
550 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700551 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700552 break;
553 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700554 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700555 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700556 case R.id.toggle_inv:
557 final boolean selected = !mInverseToggle.isSelected();
558 mInverseToggle.setSelected(selected);
559 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700560 if (mCurrentState == CalculatorState.RESULT) {
561 mResultText.redisplay(); // In case we cancelled reevaluation.
562 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700563 break;
564 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700565 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700566 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700567 if (mCurrentState == CalculatorState.RESULT) {
568 mEvaluator.collapse(); // Capture result evaluated in old mode
569 redisplayFormula();
570 }
571 // In input mode, we reinterpret already entered trig functions.
572 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700573 onModeChanged(mode);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700574 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700575 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800576 if (!haveUnprocessed() && mEvaluator.getExpr().hasInterestingOps()) {
Hans Boehmc023b732015-04-29 11:30:47 -0700577 mEvaluator.evaluateAndShowResult();
578 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700579 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700580 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700581 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800582 if (haveUnprocessed()) {
583 // For consistency, append as uninterpreted characters.
584 // This may actually be useful for a left parenthesis.
585 addChars(KeyMaps.toString(this, id), true);
586 } else {
587 addExplicitKeyToExpr(id);
588 redisplayAfterFormulaChange();
589 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700590 break;
591 }
592 }
593
Hans Boehm84614952014-11-25 18:46:17 -0800594 void redisplayFormula() {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700595 SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700596 if (mUnprocessedChars != null) {
597 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700598 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
599 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700600 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700601 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800602 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
603 ? getString(R.string.desc_formula) : formula.toString());
Hans Boehm84614952014-11-25 18:46:17 -0800604 }
605
Justin Klaassen4b3af052014-05-27 17:53:10 -0700606 @Override
607 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700608 mCurrentButton = view;
609
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800610 // Attempt to hide the toolbar whenever an interaction has occurred.
611 mDisplayView.hideToolbar();
612
Justin Klaassen4b3af052014-05-27 17:53:10 -0700613 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700614 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700615 return true;
616 }
617 return false;
618 }
619
Hans Boehm84614952014-11-25 18:46:17 -0800620 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700621 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
622 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700623 // Invalidate any options that may depend on the current result.
624 invalidateOptionsMenu();
625
Hans Boehma0e45f32015-05-30 13:20:35 -0700626 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700627 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800628 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700629 }
Hans Boehm84614952014-11-25 18:46:17 -0800630 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700631
Hans Boehmc1ea0912015-06-19 15:05:07 -0700632 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800633 public void onCancelled() {
634 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800635 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700636 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800637 }
638
639 // Reevaluation completed; ask result to redisplay current value.
640 public void onReevaluate()
641 {
Justin Klaassen44595162015-05-28 17:55:20 -0700642 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700643 }
644
Justin Klaassenfed941a2014-06-09 18:42:40 +0100645 @Override
646 public void onTextSizeChanged(final TextView textView, float oldSize) {
647 if (mCurrentState != CalculatorState.INPUT) {
648 // Only animate text changes that occur from user input.
649 return;
650 }
651
652 // Calculate the values needed to perform the scale and translation animations,
653 // maintaining the same apparent baseline for the displayed text.
654 final float textScale = oldSize / textView.getTextSize();
655 final float translationX = (1.0f - textScale) *
656 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
657 final float translationY = (1.0f - textScale) *
658 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
659
660 final AnimatorSet animatorSet = new AnimatorSet();
661 animatorSet.playTogether(
662 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
663 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
664 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
665 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700666 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100667 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
668 animatorSet.start();
669 }
670
Hans Boehmc1ea0912015-06-19 15:05:07 -0700671 /**
672 * Cancel any in-progress explicitly requested evaluations.
673 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
674 value, and certainly changes the display, so it seems reasonable to warn.
675 * @return true if there was such an evaluation
676 */
677 private boolean cancelIfEvaluating(boolean quiet) {
678 if (mCurrentState == CalculatorState.EVALUATE) {
679 mEvaluator.cancelAll(quiet);
680 return true;
681 } else {
682 return false;
683 }
684 }
685
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800686 private boolean haveUnprocessed() {
687 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
688 }
689
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700690 private void onEquals() {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700691 // In non-INPUT state assume this was redundant and ignore it.
Hans Boehmc023b732015-04-29 11:30:47 -0700692 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700693 setState(CalculatorState.EVALUATE);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800694 if (haveUnprocessed()) {
695 onError(R.string.error_syntax);
696 } else {
697 mEvaluator.requireResult();
698 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700699 }
700 }
701
702 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700703 // Delete works like backspace; remove the last character or operator from the expression.
704 // Note that we handle keyboard delete exactly like the delete button. For
705 // example the delete button can be used to delete a character from an incomplete
706 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700707 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700708 // If there is an in-progress explicit evaluation, just cancel it and return.
709 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700710 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800711 if (haveUnprocessed()) {
712 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700713 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700714 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700715 }
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800716 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700717 // Resulting formula won't be announced, since it's empty.
718 announceClearedForAccessibility();
719 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700720 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700721 }
722
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700723 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700724 final ViewGroupOverlay groupOverlay =
725 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700726
727 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700728 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700729
730 // Make reveal cover the display and status bar.
731 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700732 revealView.setBottom(displayRect.bottom);
733 revealView.setLeft(displayRect.left);
734 revealView.setRight(displayRect.right);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800735 revealView.setBackgroundColor(getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700736 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700737
Justin Klaassen4b3af052014-05-27 17:53:10 -0700738 final int[] clearLocation = new int[2];
739 sourceView.getLocationInWindow(clearLocation);
740 clearLocation[0] += sourceView.getWidth() / 2;
741 clearLocation[1] += sourceView.getHeight() / 2;
742
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700743 final int revealCenterX = clearLocation[0] - revealView.getLeft();
744 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700745
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700746 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
747 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
748 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700749 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
750
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700751 final Animator revealAnimator =
752 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700753 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700754 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700755 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700756 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700757
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700758 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700759 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700760 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700761
762 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700763 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700764 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
765 animatorSet.addListener(new AnimatorListenerAdapter() {
766 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700767 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700768 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700769 mCurrentAnimator = null;
770 }
771 });
772
773 mCurrentAnimator = animatorSet;
774 animatorSet.start();
775 }
776
Hans Boehmdb6f9992015-08-19 12:32:56 -0700777 private void announceClearedForAccessibility() {
778 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700779 }
780
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700781 private void onClear() {
Justin Klaassen1a428cf2016-02-24 15:58:18 -0800782 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700783 return;
784 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700785 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -0700786 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700787 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
788 @Override
789 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700790 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700791 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700792 mEvaluator.clear();
793 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800794 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700795 }
796 });
797 }
798
Hans Boehm84614952014-11-25 18:46:17 -0800799 // Evaluation encountered en error. Display the error.
800 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700801 if (mCurrentState == CalculatorState.EVALUATE) {
802 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -0700803 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -0700804 reveal(mCurrentButton, R.color.calculator_error_color,
805 new AnimatorListenerAdapter() {
806 @Override
807 public void onAnimationEnd(Animator animation) {
808 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700809 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700810 }
811 });
812 } else if (mCurrentState == CalculatorState.INIT) {
813 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700814 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700815 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700816 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700817 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700818 }
819
Hans Boehm84614952014-11-25 18:46:17 -0800820 // Animate movement of result into the top formula slot.
821 // Result window now remains translated in the top slot while the result is displayed.
822 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700823 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800824 // formula and result displays back at the end of the animation. We no longer do that,
825 // so that we can continue to properly support scrolling of the result.
826 // We assume the result already contains the text to be expanded.
827 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700828 // Calculate the textSize that would be used to display the result in the formula.
829 // For scrollable results just use the minimum textSize to maximize the number of digits
830 // that are visible on screen.
831 float textSize = mFormulaText.getMinimumTextSize();
832 if (!mResultText.isScrollable()) {
833 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
834 }
835
836 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
837 // when a result is reused in a subsequent expression.
838 final float resultScale = textSize / mResultText.getTextSize();
839
840 // Set the result's pivot to match its gravity.
841 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
842 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
843
844 // Calculate the necessary translations so the result takes the place of the formula and
845 // the formula moves off the top of the screen.
846 final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
847 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
Hans Boehm08e8f322015-04-21 13:18:38 -0700848 final float formulaTranslationY = -mFormulaText.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700849
Justin Klaassen44595162015-05-28 17:55:20 -0700850 // Change the result's textColor to match the formula.
851 final int formulaTextColor = mFormulaText.getCurrentTextColor();
852
Hans Boehm84614952014-11-25 18:46:17 -0800853 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -0700854 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
855 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -0700856 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800857 final AnimatorSet animatorSet = new AnimatorSet();
858 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700859 ObjectAnimator.ofPropertyValuesHolder(mResultText,
860 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
861 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
862 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
863 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
864 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
865 animatorSet.setDuration(getResources().getInteger(
866 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800867 animatorSet.addListener(new AnimatorListenerAdapter() {
868 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800869 public void onAnimationEnd(Animator animation) {
870 setState(CalculatorState.RESULT);
871 mCurrentAnimator = null;
872 }
873 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700874
Hans Boehm84614952014-11-25 18:46:17 -0800875 mCurrentAnimator = animatorSet;
876 animatorSet.start();
877 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700878 mResultText.setScaleX(resultScale);
879 mResultText.setScaleY(resultScale);
880 mResultText.setTranslationY(resultTranslationY);
881 mResultText.setTextColor(formulaTextColor);
Hans Boehm08e8f322015-04-21 13:18:38 -0700882 mFormulaText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700883 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800884 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700885 }
Hans Boehm84614952014-11-25 18:46:17 -0800886
887 // Restore positions of the formula and result displays back to their original,
888 // pre-animation state.
889 private void restoreDisplayPositions() {
890 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700891 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800892 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700893 mResultText.setScaleX(1.0f);
894 mResultText.setScaleY(1.0f);
895 mResultText.setTranslationX(0.0f);
896 mResultText.setTranslationY(0.0f);
Hans Boehm08e8f322015-04-21 13:18:38 -0700897 mFormulaText.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800898
Hans Boehm08e8f322015-04-21 13:18:38 -0700899 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700900 }
901
902 @Override
903 public void onClick(AlertDialogFragment fragment, int which) {
904 if (which == DialogInterface.BUTTON_POSITIVE) {
905 // Timeout extension request.
906 mEvaluator.setLongTimeOut();
907 }
908 }
Hans Boehm84614952014-11-25 18:46:17 -0800909
Justin Klaassend48b7562015-04-16 16:51:38 -0700910 @Override
911 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700912 super.onCreateOptionsMenu(menu);
913
914 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700915 return true;
916 }
917
918 @Override
919 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700920 super.onPrepareOptionsMenu(menu);
921
922 // Show the leading option when displaying a result.
923 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
924
925 // Show the fraction option when displaying a rational result.
926 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
927 && mEvaluator.getRational() != null);
928
Justin Klaassend48b7562015-04-16 16:51:38 -0700929 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800930 }
931
932 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700933 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800934 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700935 case R.id.menu_leading:
936 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800937 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700938 case R.id.menu_fraction:
939 displayFraction();
940 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700941 case R.id.menu_licenses:
942 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700943 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800944 default:
945 return super.onOptionsItemSelected(item);
946 }
947 }
948
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700949 private void displayMessage(String s) {
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700950 AlertDialogFragment.showMessageDialog(this, s, null);
Hans Boehm84614952014-11-25 18:46:17 -0800951 }
952
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700953 private void displayFraction() {
954 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700955 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700956 }
957
958 // Display full result to currently evaluated precision
959 private void displayFull() {
960 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -0700961 String msg = mResultText.getFullText() + " ";
962 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700963 msg += res.getString(R.string.exact);
964 } else {
965 msg += res.getString(R.string.approximate);
966 }
967 displayMessage(msg);
968 }
969
Hans Boehm017de982015-06-10 17:46:03 -0700970 /**
971 * Add input characters to the end of the expression.
972 * Map them to the appropriate button pushes when possible. Leftover characters
973 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
974 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -0800975 * @param moreChars characters to be added
976 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -0700977 */
978 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700979 if (mUnprocessedChars != null) {
980 moreChars = mUnprocessedChars + moreChars;
981 }
982 int current = 0;
983 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -0700984 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -0700985 if (mCurrentState == CalculatorState.RESULT && len != 0) {
986 // Clear display immediately for incomplete function name.
987 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
988 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700989 while (current < len) {
990 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -0700991 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -0700992 if (!explicit) {
993 int expEnd;
994 if (lastWasDigit && current !=
995 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
996 // Process scientific notation with 'E' when pasting, in spite of ambiguity
997 // with base of natural log.
998 // Otherwise the 10^x key is the user's friend.
999 mEvaluator.addExponent(moreChars, current, expEnd);
1000 current = expEnd;
1001 lastWasDigit = false;
1002 continue;
1003 } else {
1004 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1005 if (current == 0 && (isDigit || k == R.id.dec_point)
1006 && mEvaluator.getExpr().hasTrailingConstant()) {
1007 // Refuse to concatenate pasted content to trailing constant.
1008 // This makes pasting of calculator results more consistent, whether or
1009 // not the old calculator instance is still around.
1010 addKeyToExpr(R.id.op_mul);
1011 }
1012 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1013 }
1014 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001015 if (k != View.NO_ID) {
1016 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001017 if (explicit) {
1018 addExplicitKeyToExpr(k);
1019 } else {
1020 addKeyToExpr(k);
1021 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001022 if (Character.isSurrogate(c)) {
1023 current += 2;
1024 } else {
1025 ++current;
1026 }
1027 continue;
1028 }
Hans Boehm013969e2015-04-13 20:29:47 -07001029 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001030 if (f != View.NO_ID) {
1031 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001032 if (explicit) {
1033 addExplicitKeyToExpr(f);
1034 } else {
1035 addKeyToExpr(f);
1036 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001037 if (f == R.id.op_sqrt) {
1038 // Square root entered as function; don't lose the parenthesis.
1039 addKeyToExpr(R.id.lparen);
1040 }
1041 current = moreChars.indexOf('(', current) + 1;
1042 continue;
1043 }
1044 // There are characters left, but we can't convert them to button presses.
1045 mUnprocessedChars = moreChars.substring(current);
1046 redisplayAfterFormulaChange();
1047 return;
1048 }
1049 mUnprocessedChars = null;
1050 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -08001051 }
1052
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001053 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001054 public boolean onPaste(ClipData clip) {
1055 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1056 if (item == null) {
1057 // nothing to paste, bail early...
1058 return false;
1059 }
1060
1061 // Check if the item is a previously copied result, otherwise paste as raw text.
1062 final Uri uri = item.getUri();
1063 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001064 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001065 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001066 setState(CalculatorState.INPUT);
1067 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -08001068 }
Hans Boehm3666e632015-07-27 18:33:12 -07001069 mEvaluator.appendSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001070 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001071 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001072 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001073 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001074 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001075 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001076}