blob: 919d1c89d9cab7c5d69643c8eb4514fe053b14bd [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;
Annie Chine918fd22016-03-09 11:07:54 -080046import android.text.Editable;
Hans Boehm8a4f81c2015-07-09 10:41:25 -070047import android.text.SpannableStringBuilder;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070048import android.text.Spanned;
Annie Chinf360ef02016-03-10 13:45:39 -080049import android.text.TextUtils;
Annie Chine918fd22016-03-09 11:07:54 -080050import android.text.TextWatcher;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070051import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070052import android.util.Property;
Annie Chine918fd22016-03-09 11:07:54 -080053import android.view.ActionMode;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070054import android.view.KeyCharacterMap;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070055import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080056import android.view.Menu;
57import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070058import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070059import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070060import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070061import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070062import android.view.ViewGroupOverlay;
Annie Chine918fd22016-03-09 11:07:54 -080063import android.view.ViewTreeObserver;
Justin Klaassen4b3af052014-05-27 17:53:10 -070064import android.view.animation.AccelerateDecelerateInterpolator;
Annie Chine918fd22016-03-09 11:07:54 -080065import android.widget.HorizontalScrollView;
Justin Klaassenfed941a2014-06-09 18:42:40 +010066import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070067import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010068
Hans Boehm08e8f322015-04-21 13:18:38 -070069import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080070
71import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080072import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080073import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070074import java.io.ObjectInput;
75import java.io.ObjectInputStream;
76import java.io.ObjectOutput;
77import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070078
Justin Klaassen04f79c72014-06-27 17:25:35 -070079public class Calculator extends Activity
Hans Boehm5e6a0ca2015-09-22 17:09:01 -070080 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener,
81 AlertDialogFragment.OnClickListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070082
83 /**
84 * Constant for an invalid resource id.
85 */
86 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070087
88 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080089 INPUT, // Result and formula both visible, no evaluation requested,
90 // Though result may be visible on bottom line.
91 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070092 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080093 INIT, // Very temporary state used as alternative to EVALUATE
94 // during reinitialization. Do not animate on completion.
95 ANIMATE, // Result computed, animation to enlarge result window in progress.
96 RESULT, // Result displayed, formula invisible.
97 // If we are in RESULT state, the formula was evaluated without
98 // error to initial precision.
99 ERROR // Error displayed: Formula visible, result shows error message.
100 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700101 }
Hans Boehm84614952014-11-25 18:46:17 -0800102 // Normal transition sequence is
103 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
104 // A RESULT -> ERROR transition is possible in rare corner cases, in which
105 // a higher precision evaluation exposes an error. This is possible, since we
106 // initially evaluate assuming we were given a well-defined problem. If we
107 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
108 // unless we are asked for enough precision that we can distinguish the argument from zero.
109 // TODO: Consider further heuristics to reduce the chance of observing this?
110 // It already seems to be observable only in contrived cases.
111 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
112 // is restarted in that state. This leads us to recompute and redisplay the result
113 // ASAP.
114 // TODO: Possibly save a bit more information, e.g. its initial display string
115 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700116
Justin Klaassen44595162015-05-28 17:55:20 -0700117 private final Property<TextView, Integer> TEXT_COLOR =
118 new Property<TextView, Integer>(Integer.class, "textColor") {
119 @Override
120 public Integer get(TextView textView) {
121 return textView.getCurrentTextColor();
122 }
123
124 @Override
125 public void set(TextView textView, Integer textColor) {
126 textView.setTextColor(textColor);
127 }
128 };
129
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700130 // We currently assume that the formula does not change out from under us in
131 // any way. We explicitly handle all input to the formula here.
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700132 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
133 @Override
134 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm1176f232015-05-11 16:26:03 -0700135 stopActionMode();
Justin Klaassen06c49442015-06-04 14:39:27 -0700136 // Never consume DPAD key events.
137 switch (keyCode) {
138 case KeyEvent.KEYCODE_DPAD_UP:
139 case KeyEvent.KEYCODE_DPAD_DOWN:
140 case KeyEvent.KEYCODE_DPAD_LEFT:
141 case KeyEvent.KEYCODE_DPAD_RIGHT:
142 return false;
143 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700144 // Always cancel unrequested in-progress evaluation, so that we don't have
145 // to worry about subsequent asynchronous completion.
146 // Requested in-progress evaluations are handled below.
147 if (mCurrentState != CalculatorState.EVALUATE) {
148 mEvaluator.cancelAll(true);
149 }
150 // In other cases we go ahead and process the input normally after cancelling:
Justin Klaassen06c49442015-06-04 14:39:27 -0700151 if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
152 return true;
153 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700154 switch (keyCode) {
155 case KeyEvent.KEYCODE_NUMPAD_ENTER:
156 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700157 case KeyEvent.KEYCODE_DPAD_CENTER:
158 mCurrentButton = mEqualButton;
159 onEquals();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800160 break;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700161 case KeyEvent.KEYCODE_DEL:
162 mCurrentButton = mDeleteButton;
163 onDelete();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800164 break;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700165 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700166 cancelIfEvaluating(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700167 final int raw = keyEvent.getKeyCharacterMap()
Justin Klaassen44595162015-05-28 17:55:20 -0700168 .get(keyCode, keyEvent.getMetaState());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700169 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
170 return true; // discard
171 }
172 // Try to discard non-printing characters and the like.
173 // The user will have to explicitly delete other junk that gets past us.
174 if (Character.isIdentifierIgnorable(raw)
Justin Klaassen44595162015-05-28 17:55:20 -0700175 || Character.isWhitespace(raw)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700176 return true;
177 }
Justin Klaassen44595162015-05-28 17:55:20 -0700178 char c = (char) raw;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700179 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700180 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700181 onEquals();
182 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700183 addChars(String.valueOf(c), true);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700184 redisplayAfterFormulaChange();
185 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700186 }
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800187 return true;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700188 }
189 };
190
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800191 private static final String NAME = "Calculator";
Hans Boehm84614952014-11-25 18:46:17 -0800192 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700193 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800194 /**
195 * Associated value is a byte array holding the evaluator state.
196 */
Hans Boehm84614952014-11-25 18:46:17 -0800197 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800198 private static final String KEY_INVERSE_MODE = NAME + "_inverse_mode";
Justin Klaassen741471e2014-06-11 09:43:44 -0700199
Annie Chine918fd22016-03-09 11:07:54 -0800200 private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
201 new ViewTreeObserver.OnPreDrawListener() {
202 @Override
203 public boolean onPreDraw() {
204 mFormulaContainer.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
205 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
206 if (observer.isAlive()) {
207 observer.removeOnPreDrawListener(this);
208 }
209 return false;
210 }
211 };
212
213 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
214 @Override
215 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
216 }
217
218 @Override
219 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
220 }
221
222 @Override
223 public void afterTextChanged(Editable editable) {
224 final ViewTreeObserver observer = mFormulaContainer.getViewTreeObserver();
225 if (observer.isAlive()) {
226 observer.removeOnPreDrawListener(mPreDrawListener);
227 observer.addOnPreDrawListener(mPreDrawListener);
228 }
229 }
230 };
231
Justin Klaassen4b3af052014-05-27 17:53:10 -0700232 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800233 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700234
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800235 private CalculatorDisplay mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700236 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700237 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700238 private CalculatorResult mResultText;
Annie Chine918fd22016-03-09 11:07:54 -0800239 private HorizontalScrollView mFormulaContainer;
Justin Klaassend48b7562015-04-16 16:51:38 -0700240
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100241 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700242 private View mDeleteButton;
243 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700244 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700245
246 private TextView mInverseToggle;
247 private TextView mModeToggle;
248
Justin Klaassen721ec842015-05-28 14:30:08 -0700249 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700250 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700251
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700252 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700253 private Animator mCurrentAnimator;
254
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700255 // Characters that were recently entered at the end of the display that have not yet
256 // been added to the underlying expression.
257 private String mUnprocessedChars = null;
258
259 // Color to highlight unprocessed characters from physical keyboard.
260 // TODO: should probably match this to the error color?
261 private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700262
Justin Klaassen4b3af052014-05-27 17:53:10 -0700263 @Override
264 protected void onCreate(Bundle savedInstanceState) {
265 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700266 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700267 setActionBar((Toolbar) findViewById(R.id.toolbar));
268
269 // Hide all default options in the ActionBar.
270 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700271
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800272 // Ensure the toolbar stays visible while the options menu is displayed.
273 getActionBar().addOnMenuVisibilityListener(new ActionBar.OnMenuVisibilityListener() {
274 @Override
275 public void onMenuVisibilityChanged(boolean isVisible) {
276 mDisplayView.setForceToolbarVisible(isVisible);
277 }
278 });
279
280 mDisplayView = (CalculatorDisplay) findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700281 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700282 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700283 mResultText = (CalculatorResult) findViewById(R.id.result);
Annie Chine918fd22016-03-09 11:07:54 -0800284 mFormulaContainer = (HorizontalScrollView) findViewById(R.id.formula_container);
Justin Klaassend48b7562015-04-16 16:51:38 -0700285
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100286 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700287 mDeleteButton = findViewById(R.id.del);
288 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700289 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
290 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
291 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
292 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700293
294 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
295 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
296
Justin Klaassen721ec842015-05-28 14:30:08 -0700297 mInvertibleButtons = new View[] {
298 findViewById(R.id.fun_sin),
299 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700300 findViewById(R.id.fun_tan),
301 findViewById(R.id.fun_ln),
302 findViewById(R.id.fun_log),
303 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700304 };
305 mInverseButtons = new View[] {
306 findViewById(R.id.fun_arcsin),
307 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700308 findViewById(R.id.fun_arctan),
309 findViewById(R.id.fun_exp),
310 findViewById(R.id.fun_10pow),
311 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700312 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700313
Justin Klaassen44595162015-05-28 17:55:20 -0700314 mEvaluator = new Evaluator(this, mResultText);
315 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700316 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700317
Hans Boehm84614952014-11-25 18:46:17 -0800318 if (savedInstanceState != null) {
319 setState(CalculatorState.values()[
320 savedInstanceState.getInt(KEY_DISPLAY_STATE,
321 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700322 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
323 if (unprocessed != null) {
324 mUnprocessedChars = unprocessed.toString();
325 }
326 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800327 if (state != null) {
328 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
329 mEvaluator.restoreInstanceState(in);
330 } catch (Throwable ignored) {
331 // When in doubt, revert to clean state
332 mCurrentState = CalculatorState.INPUT;
333 mEvaluator.clear();
334 }
335 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700336 } else {
337 mCurrentState = CalculatorState.INPUT;
338 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800339 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700340
Hans Boehm08e8f322015-04-21 13:18:38 -0700341 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
342 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700343 mFormulaText.setOnPasteListener(this);
Annie Chine918fd22016-03-09 11:07:54 -0800344 mFormulaText.addTextChangedListener(mFormulaTextWatcher);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700345 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700346
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800347 onInverseToggled(savedInstanceState != null
348 && savedInstanceState.getBoolean(KEY_INVERSE_MODE));
Justin Klaassene2711cb2015-05-28 11:13:17 -0700349 onModeChanged(mEvaluator.getDegreeMode());
350
Hans Boehm84614952014-11-25 18:46:17 -0800351 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700352 // Just reevaluate.
353 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800354 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800355 mEvaluator.requireResult();
356 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700357 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800358 }
359 // TODO: We're currently not saving and restoring scroll position.
360 // We probably should. Details may require care to deal with:
361 // - new display size
362 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700363 }
364
365 @Override
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800366 protected void onResume() {
367 super.onResume();
368
369 // Always show the toolbar initially on launch.
370 mDisplayView.showToolbar();
371 }
372
373 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700374 protected void onSaveInstanceState(@NonNull Bundle outState) {
Hans Boehm40125442016-01-22 10:35:35 -0800375 mEvaluator.cancelAll(true);
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700376 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
377 if (mCurrentAnimator != null) {
378 mCurrentAnimator.cancel();
379 }
380
Justin Klaassen4b3af052014-05-27 17:53:10 -0700381 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800382 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700383 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800384 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
385 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
386 mEvaluator.saveInstanceState(out);
387 } catch (IOException e) {
388 // Impossible; No IO involved.
389 throw new AssertionError("Impossible IO exception", e);
390 }
391 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800392 outState.putBoolean(KEY_INVERSE_MODE, mInverseToggle.isSelected());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700393 }
394
Annie Chine918fd22016-03-09 11:07:54 -0800395 @Override
396 public void onActionModeStarted(ActionMode mode) {
397 super.onActionModeStarted(mode);
398 if (mode.getTag() == CalculatorText.TAG_ACTION_MODE) {
399 mFormulaContainer.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
400 }
401 }
402
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700403 // Set the state, updating delete label and display colors.
404 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700405 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700406 private void setState(CalculatorState state) {
407 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800408 if (state == CalculatorState.INPUT) {
409 restoreDisplayPositions();
410 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700411 mCurrentState = state;
412
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700413 if (mCurrentState == CalculatorState.RESULT) {
414 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700415 mDeleteButton.setVisibility(View.GONE);
416 mClearButton.setVisibility(View.VISIBLE);
417 } else {
418 mDeleteButton.setVisibility(View.VISIBLE);
419 mClearButton.setVisibility(View.GONE);
420 }
421
Hans Boehm84614952014-11-25 18:46:17 -0800422 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700423 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700424 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700425 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700426 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700427 } else if (mCurrentState != CalculatorState.RESULT) {
428 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
429 mResultText.setTextColor(getColor(R.color.display_result_text_color));
430 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700431 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700432
433 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700434 }
435 }
436
Hans Boehm1176f232015-05-11 16:26:03 -0700437 // Stop any active ActionMode. Return true if there was one.
438 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700439 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700440 return true;
441 }
442 if (mFormulaText.stopActionMode()) {
443 return true;
444 }
445 return false;
446 }
447
Justin Klaassen4b3af052014-05-27 17:53:10 -0700448 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100449 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700450 if (!stopActionMode()) {
451 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
452 // Select the previous pad.
453 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
454 } else {
455 // If the user is currently looking at the first pad (or the pad is not paged),
456 // allow the system to handle the Back button.
457 super.onBackPressed();
458 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100459 }
460 }
461
462 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700463 public void onUserInteraction() {
464 super.onUserInteraction();
465
Hans Boehmc1ea0912015-06-19 15:05:07 -0700466 // If there's an animation in progress, end it immediately, so the user interaction can
467 // be handled.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700468 if (mCurrentAnimator != null) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700469 mCurrentAnimator.end();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700470 }
471 }
472
Justin Klaassene2711cb2015-05-28 11:13:17 -0700473 /**
474 * Invoked whenever the inverse button is toggled to update the UI.
475 *
476 * @param showInverse {@code true} if inverse functions should be shown
477 */
478 private void onInverseToggled(boolean showInverse) {
Justin Klaassen3e223ea2016-02-05 14:18:06 -0800479 mInverseToggle.setSelected(showInverse);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700480 if (showInverse) {
481 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700482 for (View invertibleButton : mInvertibleButtons) {
483 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700484 }
485 for (View inverseButton : mInverseButtons) {
486 inverseButton.setVisibility(View.VISIBLE);
487 }
488 } else {
489 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700490 for (View invertibleButton : mInvertibleButtons) {
491 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700492 }
493 for (View inverseButton : mInverseButtons) {
494 inverseButton.setVisibility(View.GONE);
495 }
496 }
497 }
498
499 /**
500 * Invoked whenever the deg/rad mode may have changed to update the UI.
501 *
502 * @param degreeMode {@code true} if in degree mode
503 */
504 private void onModeChanged(boolean degreeMode) {
505 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700506 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700507 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
508
509 mModeToggle.setText(R.string.mode_rad);
510 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700511 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700512 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700513 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
514
515 mModeToggle.setText(R.string.mode_deg);
516 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700517 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800518
519 // Show the toolbar to highlight the mode change.
520 mDisplayView.showToolbar();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700521 }
Hans Boehm84614952014-11-25 18:46:17 -0800522
Hans Boehm5d79d102015-09-16 16:33:47 -0700523 /**
524 * Switch to INPUT from RESULT state in response to input of the specified button_id.
525 * View.NO_ID is treated as an incomplete function id.
526 */
527 private void switchToInput(int button_id) {
528 if (KeyMaps.isBinary(button_id) || KeyMaps.isSuffix(button_id)) {
529 mEvaluator.collapse();
530 } else {
531 announceClearedForAccessibility();
532 mEvaluator.clear();
533 }
534 setState(CalculatorState.INPUT);
535 }
536
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700537 // Add the given button id to input expression.
538 // If appropriate, clear the expression before doing so.
539 private void addKeyToExpr(int id) {
540 if (mCurrentState == CalculatorState.ERROR) {
541 setState(CalculatorState.INPUT);
542 } else if (mCurrentState == CalculatorState.RESULT) {
Hans Boehm5d79d102015-09-16 16:33:47 -0700543 switchToInput(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700544 }
545 if (!mEvaluator.append(id)) {
546 // TODO: Some user visible feedback?
547 }
548 }
549
Hans Boehm017de982015-06-10 17:46:03 -0700550 /**
551 * Add the given button id to input expression, assuming it was explicitly
552 * typed/touched.
553 * We perform slightly more aggressive correction than in pasted expressions.
554 */
555 private void addExplicitKeyToExpr(int id) {
556 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
557 mEvaluator.getExpr().removeTrailingAdditiveOperators();
558 }
559 addKeyToExpr(id);
560 }
561
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700562 private void redisplayAfterFormulaChange() {
563 // TODO: Could do this more incrementally.
564 redisplayFormula();
565 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800566 if (haveUnprocessed()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700567 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800568 // Force reevaluation when text is deleted, even if expression is unchanged.
569 mEvaluator.touch();
570 } else {
571 if (mEvaluator.getExpr().hasInterestingOps()) {
572 mEvaluator.evaluateAndShowResult();
573 } else {
574 mResultText.clear();
575 }
Hans Boehmc023b732015-04-29 11:30:47 -0700576 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700577 }
578
Justin Klaassen4b3af052014-05-27 17:53:10 -0700579 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700580 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700581 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700582 stopActionMode();
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800583
584 // Attempt to hide the toolbar whenever an interaction has occurred.
585 mDisplayView.hideToolbar();
586
Hans Boehmc1ea0912015-06-19 15:05:07 -0700587 // See onKey above for the rationale behind some of the behavior below:
588 if (mCurrentState != CalculatorState.EVALUATE) {
589 // Cancel evaluations that were not specifically requested.
590 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800591 }
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800592
Justin Klaassend48b7562015-04-16 16:51:38 -0700593 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800594 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700595 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700596 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700597 break;
598 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700599 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700600 break;
601 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700602 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700603 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700604 case R.id.toggle_inv:
605 final boolean selected = !mInverseToggle.isSelected();
606 mInverseToggle.setSelected(selected);
607 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700608 if (mCurrentState == CalculatorState.RESULT) {
609 mResultText.redisplay(); // In case we cancelled reevaluation.
610 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700611 break;
612 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700613 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700614 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700615 if (mCurrentState == CalculatorState.RESULT) {
616 mEvaluator.collapse(); // Capture result evaluated in old mode
617 redisplayFormula();
618 }
619 // In input mode, we reinterpret already entered trig functions.
620 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700621 onModeChanged(mode);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700622 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700623 mResultText.clear();
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800624 if (!haveUnprocessed() && mEvaluator.getExpr().hasInterestingOps()) {
Hans Boehmc023b732015-04-29 11:30:47 -0700625 mEvaluator.evaluateAndShowResult();
626 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700627 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700628 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700629 cancelIfEvaluating(false);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800630 if (haveUnprocessed()) {
631 // For consistency, append as uninterpreted characters.
632 // This may actually be useful for a left parenthesis.
633 addChars(KeyMaps.toString(this, id), true);
634 } else {
635 addExplicitKeyToExpr(id);
636 redisplayAfterFormulaChange();
637 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700638 break;
639 }
640 }
641
Hans Boehm84614952014-11-25 18:46:17 -0800642 void redisplayFormula() {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700643 SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700644 if (mUnprocessedChars != null) {
645 // Add and highlight characters we couldn't process.
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700646 formula.append(mUnprocessedChars, mUnprocessedColorSpan,
647 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700648 }
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700649 mFormulaText.changeTextTo(formula);
Annie Chinf360ef02016-03-10 13:45:39 -0800650 mFormulaText.setContentDescription(TextUtils.isEmpty(formula)
651 ? getString(R.string.desc_formula) : formula.toString());
Hans Boehm84614952014-11-25 18:46:17 -0800652 }
653
Justin Klaassen4b3af052014-05-27 17:53:10 -0700654 @Override
655 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700656 mCurrentButton = view;
657
Justin Klaassen9d33cdc2016-02-21 14:16:14 -0800658 // Attempt to hide the toolbar whenever an interaction has occurred.
659 mDisplayView.hideToolbar();
660
Justin Klaassen4b3af052014-05-27 17:53:10 -0700661 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700662 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700663 return true;
664 }
665 return false;
666 }
667
Hans Boehm84614952014-11-25 18:46:17 -0800668 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700669 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
670 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700671 // Invalidate any options that may depend on the current result.
672 invalidateOptionsMenu();
673
Hans Boehma0e45f32015-05-30 13:20:35 -0700674 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700675 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800676 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700677 }
Hans Boehm84614952014-11-25 18:46:17 -0800678 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700679
Hans Boehmc1ea0912015-06-19 15:05:07 -0700680 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800681 public void onCancelled() {
682 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800683 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700684 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800685 }
686
687 // Reevaluation completed; ask result to redisplay current value.
688 public void onReevaluate()
689 {
Justin Klaassen44595162015-05-28 17:55:20 -0700690 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700691 }
692
Justin Klaassenfed941a2014-06-09 18:42:40 +0100693 @Override
694 public void onTextSizeChanged(final TextView textView, float oldSize) {
695 if (mCurrentState != CalculatorState.INPUT) {
696 // Only animate text changes that occur from user input.
697 return;
698 }
699
700 // Calculate the values needed to perform the scale and translation animations,
701 // maintaining the same apparent baseline for the displayed text.
702 final float textScale = oldSize / textView.getTextSize();
703 final float translationX = (1.0f - textScale) *
704 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
705 final float translationY = (1.0f - textScale) *
706 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
707
708 final AnimatorSet animatorSet = new AnimatorSet();
709 animatorSet.playTogether(
710 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
711 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
712 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
713 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700714 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100715 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
716 animatorSet.start();
717 }
718
Hans Boehmc1ea0912015-06-19 15:05:07 -0700719 /**
720 * Cancel any in-progress explicitly requested evaluations.
721 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
722 value, and certainly changes the display, so it seems reasonable to warn.
723 * @return true if there was such an evaluation
724 */
725 private boolean cancelIfEvaluating(boolean quiet) {
726 if (mCurrentState == CalculatorState.EVALUATE) {
727 mEvaluator.cancelAll(quiet);
728 return true;
729 } else {
730 return false;
731 }
732 }
733
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800734 private boolean haveUnprocessed() {
735 return mUnprocessedChars != null && !mUnprocessedChars.isEmpty();
736 }
737
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700738 private void onEquals() {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700739 // In non-INPUT state assume this was redundant and ignore it.
Hans Boehmc023b732015-04-29 11:30:47 -0700740 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700741 setState(CalculatorState.EVALUATE);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800742 if (haveUnprocessed()) {
743 onError(R.string.error_syntax);
744 } else {
745 mEvaluator.requireResult();
746 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700747 }
748 }
749
750 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700751 // Delete works like backspace; remove the last character or operator from the expression.
752 // Note that we handle keyboard delete exactly like the delete button. For
753 // example the delete button can be used to delete a character from an incomplete
754 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700755 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700756 // If there is an in-progress explicit evaluation, just cancel it and return.
757 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700758 setState(CalculatorState.INPUT);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800759 if (haveUnprocessed()) {
760 mUnprocessedChars = mUnprocessedChars.substring(0, mUnprocessedChars.length() - 1);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700761 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700762 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700763 }
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800764 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Hans Boehmdb6f9992015-08-19 12:32:56 -0700765 // Resulting formula won't be announced, since it's empty.
766 announceClearedForAccessibility();
767 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700768 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700769 }
770
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700771 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700772 final ViewGroupOverlay groupOverlay =
773 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700774
775 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700776 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700777
778 // Make reveal cover the display and status bar.
779 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700780 revealView.setBottom(displayRect.bottom);
781 revealView.setLeft(displayRect.left);
782 revealView.setRight(displayRect.right);
Hans Boehmaf04c3a2016-01-27 14:50:08 -0800783 revealView.setBackgroundColor(getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700784 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700785
Justin Klaassen4b3af052014-05-27 17:53:10 -0700786 final int[] clearLocation = new int[2];
787 sourceView.getLocationInWindow(clearLocation);
788 clearLocation[0] += sourceView.getWidth() / 2;
789 clearLocation[1] += sourceView.getHeight() / 2;
790
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700791 final int revealCenterX = clearLocation[0] - revealView.getLeft();
792 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700793
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700794 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
795 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
796 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700797 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
798
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700799 final Animator revealAnimator =
800 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700801 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700802 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700803 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700804 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700805
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700806 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700807 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700808 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700809
810 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700811 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700812 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
813 animatorSet.addListener(new AnimatorListenerAdapter() {
814 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700815 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700816 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700817 mCurrentAnimator = null;
818 }
819 });
820
821 mCurrentAnimator = animatorSet;
822 animatorSet.start();
823 }
824
Hans Boehmdb6f9992015-08-19 12:32:56 -0700825 private void announceClearedForAccessibility() {
826 mResultText.announceForAccessibility(getResources().getString(R.string.cleared));
Hans Boehmccc55662015-07-07 14:16:59 -0700827 }
828
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700829 private void onClear() {
Justin Klaassen1a428cf2016-02-24 15:58:18 -0800830 if (mEvaluator.getExpr().isEmpty() && !haveUnprocessed()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700831 return;
832 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700833 cancelIfEvaluating(true);
Hans Boehmdb6f9992015-08-19 12:32:56 -0700834 announceClearedForAccessibility();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700835 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
836 @Override
837 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700838 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700839 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700840 mEvaluator.clear();
841 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800842 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700843 }
844 });
845 }
846
Hans Boehm84614952014-11-25 18:46:17 -0800847 // Evaluation encountered en error. Display the error.
848 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700849 if (mCurrentState == CalculatorState.EVALUATE) {
850 setState(CalculatorState.ANIMATE);
Hans Boehmccc55662015-07-07 14:16:59 -0700851 mResultText.announceForAccessibility(getResources().getString(errorResourceId));
Hans Boehmfbcef702015-04-27 18:07:47 -0700852 reveal(mCurrentButton, R.color.calculator_error_color,
853 new AnimatorListenerAdapter() {
854 @Override
855 public void onAnimationEnd(Animator animation) {
856 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700857 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700858 }
859 });
860 } else if (mCurrentState == CalculatorState.INIT) {
861 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700862 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700863 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700864 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700865 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700866 }
867
Hans Boehm84614952014-11-25 18:46:17 -0800868 // Animate movement of result into the top formula slot.
869 // Result window now remains translated in the top slot while the result is displayed.
870 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700871 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800872 // formula and result displays back at the end of the animation. We no longer do that,
873 // so that we can continue to properly support scrolling of the result.
874 // We assume the result already contains the text to be expanded.
875 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700876 // Calculate the textSize that would be used to display the result in the formula.
877 // For scrollable results just use the minimum textSize to maximize the number of digits
878 // that are visible on screen.
879 float textSize = mFormulaText.getMinimumTextSize();
880 if (!mResultText.isScrollable()) {
881 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
882 }
883
884 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
885 // when a result is reused in a subsequent expression.
886 final float resultScale = textSize / mResultText.getTextSize();
887
888 // Set the result's pivot to match its gravity.
889 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
890 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
891
892 // Calculate the necessary translations so the result takes the place of the formula and
893 // the formula moves off the top of the screen.
Annie Chine918fd22016-03-09 11:07:54 -0800894 final float resultTranslationY = (mFormulaContainer.getBottom() - mResultText.getBottom())
895 - (mFormulaContainer.getPaddingBottom() - mResultText.getPaddingBottom());
896 final float formulaTranslationY = -mFormulaContainer.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700897
Justin Klaassen44595162015-05-28 17:55:20 -0700898 // Change the result's textColor to match the formula.
899 final int formulaTextColor = mFormulaText.getCurrentTextColor();
900
Hans Boehm84614952014-11-25 18:46:17 -0800901 if (animate) {
Hans Boehmccc55662015-07-07 14:16:59 -0700902 mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
903 mResultText.announceForAccessibility(mResultText.getText());
Hans Boehmc1ea0912015-06-19 15:05:07 -0700904 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800905 final AnimatorSet animatorSet = new AnimatorSet();
906 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700907 ObjectAnimator.ofPropertyValuesHolder(mResultText,
908 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
909 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
910 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
911 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
Annie Chine918fd22016-03-09 11:07:54 -0800912 ObjectAnimator.ofFloat(mFormulaContainer, View.TRANSLATION_Y,
913 formulaTranslationY));
Justin Klaassen44595162015-05-28 17:55:20 -0700914 animatorSet.setDuration(getResources().getInteger(
915 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800916 animatorSet.addListener(new AnimatorListenerAdapter() {
917 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800918 public void onAnimationEnd(Animator animation) {
919 setState(CalculatorState.RESULT);
920 mCurrentAnimator = null;
921 }
922 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700923
Hans Boehm84614952014-11-25 18:46:17 -0800924 mCurrentAnimator = animatorSet;
925 animatorSet.start();
926 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700927 mResultText.setScaleX(resultScale);
928 mResultText.setScaleY(resultScale);
929 mResultText.setTranslationY(resultTranslationY);
930 mResultText.setTextColor(formulaTextColor);
Annie Chine918fd22016-03-09 11:07:54 -0800931 mFormulaContainer.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700932 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800933 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700934 }
Hans Boehm84614952014-11-25 18:46:17 -0800935
936 // Restore positions of the formula and result displays back to their original,
937 // pre-animation state.
938 private void restoreDisplayPositions() {
939 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700940 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800941 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700942 mResultText.setScaleX(1.0f);
943 mResultText.setScaleY(1.0f);
944 mResultText.setTranslationX(0.0f);
945 mResultText.setTranslationY(0.0f);
Annie Chine918fd22016-03-09 11:07:54 -0800946 mFormulaContainer.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800947
Hans Boehm08e8f322015-04-21 13:18:38 -0700948 mFormulaText.requestFocus();
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700949 }
950
951 @Override
952 public void onClick(AlertDialogFragment fragment, int which) {
953 if (which == DialogInterface.BUTTON_POSITIVE) {
954 // Timeout extension request.
955 mEvaluator.setLongTimeOut();
956 }
957 }
Hans Boehm84614952014-11-25 18:46:17 -0800958
Justin Klaassend48b7562015-04-16 16:51:38 -0700959 @Override
960 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700961 super.onCreateOptionsMenu(menu);
962
963 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700964 return true;
965 }
966
967 @Override
968 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700969 super.onPrepareOptionsMenu(menu);
970
971 // Show the leading option when displaying a result.
972 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
973
974 // Show the fraction option when displaying a rational result.
975 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
976 && mEvaluator.getRational() != null);
977
Justin Klaassend48b7562015-04-16 16:51:38 -0700978 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800979 }
980
981 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700982 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800983 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700984 case R.id.menu_leading:
985 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800986 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700987 case R.id.menu_fraction:
988 displayFraction();
989 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700990 case R.id.menu_licenses:
991 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700992 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800993 default:
994 return super.onOptionsItemSelected(item);
995 }
996 }
997
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700998 private void displayMessage(String s) {
Hans Boehm5e6a0ca2015-09-22 17:09:01 -0700999 AlertDialogFragment.showMessageDialog(this, s, null);
Hans Boehm84614952014-11-25 18:46:17 -08001000 }
1001
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001002 private void displayFraction() {
1003 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -07001004 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001005 }
1006
1007 // Display full result to currently evaluated precision
1008 private void displayFull() {
1009 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -07001010 String msg = mResultText.getFullText() + " ";
1011 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001012 msg += res.getString(R.string.exact);
1013 } else {
1014 msg += res.getString(R.string.approximate);
1015 }
1016 displayMessage(msg);
1017 }
1018
Hans Boehm017de982015-06-10 17:46:03 -07001019 /**
1020 * Add input characters to the end of the expression.
1021 * Map them to the appropriate button pushes when possible. Leftover characters
1022 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
1023 * added characters.
Hans Boehm65a99a42016-02-03 18:16:07 -08001024 * @param moreChars characters to be added
1025 * @param explicit these characters were explicitly typed by the user, not pasted
Hans Boehm017de982015-06-10 17:46:03 -07001026 */
1027 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001028 if (mUnprocessedChars != null) {
1029 moreChars = mUnprocessedChars + moreChars;
1030 }
1031 int current = 0;
1032 int len = moreChars.length();
Hans Boehm0b9806f2015-06-29 16:07:15 -07001033 boolean lastWasDigit = false;
Hans Boehm5d79d102015-09-16 16:33:47 -07001034 if (mCurrentState == CalculatorState.RESULT && len != 0) {
1035 // Clear display immediately for incomplete function name.
1036 switchToInput(KeyMaps.keyForChar(moreChars.charAt(current)));
1037 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001038 while (current < len) {
1039 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -07001040 int k = KeyMaps.keyForChar(c);
Hans Boehm0b9806f2015-06-29 16:07:15 -07001041 if (!explicit) {
1042 int expEnd;
1043 if (lastWasDigit && current !=
1044 (expEnd = Evaluator.exponentEnd(moreChars, current))) {
1045 // Process scientific notation with 'E' when pasting, in spite of ambiguity
1046 // with base of natural log.
1047 // Otherwise the 10^x key is the user's friend.
1048 mEvaluator.addExponent(moreChars, current, expEnd);
1049 current = expEnd;
1050 lastWasDigit = false;
1051 continue;
1052 } else {
1053 boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
1054 if (current == 0 && (isDigit || k == R.id.dec_point)
1055 && mEvaluator.getExpr().hasTrailingConstant()) {
1056 // Refuse to concatenate pasted content to trailing constant.
1057 // This makes pasting of calculator results more consistent, whether or
1058 // not the old calculator instance is still around.
1059 addKeyToExpr(R.id.op_mul);
1060 }
1061 lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
1062 }
1063 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001064 if (k != View.NO_ID) {
1065 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -07001066 if (explicit) {
1067 addExplicitKeyToExpr(k);
1068 } else {
1069 addKeyToExpr(k);
1070 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001071 if (Character.isSurrogate(c)) {
1072 current += 2;
1073 } else {
1074 ++current;
1075 }
1076 continue;
1077 }
Hans Boehm013969e2015-04-13 20:29:47 -07001078 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001079 if (f != View.NO_ID) {
1080 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -07001081 if (explicit) {
1082 addExplicitKeyToExpr(f);
1083 } else {
1084 addKeyToExpr(f);
1085 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001086 if (f == R.id.op_sqrt) {
1087 // Square root entered as function; don't lose the parenthesis.
1088 addKeyToExpr(R.id.lparen);
1089 }
1090 current = moreChars.indexOf('(', current) + 1;
1091 continue;
1092 }
1093 // There are characters left, but we can't convert them to button presses.
1094 mUnprocessedChars = moreChars.substring(current);
1095 redisplayAfterFormulaChange();
1096 return;
1097 }
1098 mUnprocessedChars = null;
1099 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -08001100 }
1101
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001102 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001103 public boolean onPaste(ClipData clip) {
1104 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
1105 if (item == null) {
1106 // nothing to paste, bail early...
1107 return false;
1108 }
1109
1110 // Check if the item is a previously copied result, otherwise paste as raw text.
1111 final Uri uri = item.getUri();
1112 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001113 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001114 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001115 setState(CalculatorState.INPUT);
1116 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -08001117 }
Hans Boehm3666e632015-07-27 18:33:12 -07001118 mEvaluator.appendSaved();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001119 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001120 } else {
Hans Boehm017de982015-06-10 17:46:03 -07001121 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -08001122 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -07001123 return true;
Hans Boehm84614952014-11-25 18:46:17 -08001124 }
Justin Klaassen4b3af052014-05-27 17:53:10 -07001125}