blob: 65c668bc1684d5854d022b30dd8484ee8a0d8af6 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
Justin Klaassen44595162015-05-28 17:55:20 -07002 * Copyright (C) 2015 The Android Open Source Project
Justin Klaassen4b3af052014-05-27 17:53:10 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070017// TODO: Better indication of when the result is known to be exact.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070018// TODO: Check and possibly fix accessability issues.
Hans Boehm013969e2015-04-13 20:29:47 -070019// TODO: Copy & more general paste in formula? Note that this requires
20// great care: Currently the text version of a displayed formula
21// is not directly useful for re-evaluating the formula later, since
22// it contains ellipses representing subexpressions evaluated with
23// a different degree mode. Rather than supporting copy from the
24// formula window, we may eventually want to support generation of a
25// more useful text version in a separate window. It's not clear
26// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080027
Justin Klaassen4b3af052014-05-27 17:53:10 -070028package com.android.calculator2;
29
30import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070031import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070032import android.animation.AnimatorListenerAdapter;
33import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070034import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070035import android.animation.PropertyValuesHolder;
Justin Klaassen4b3af052014-05-27 17:53:10 -070036import android.app.Activity;
Hans Boehm84614952014-11-25 18:46:17 -080037import android.app.AlertDialog;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070038import android.content.ClipData;
Justin Klaassend36d63e2015-05-05 12:59:36 -070039import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070040import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070041import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070042import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070043import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070044import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070045import android.support.annotation.NonNull;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010046import android.support.v4.view.ViewPager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070047import android.text.SpannableString;
48import android.text.Spanned;
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
Justin Klaassenfc5ac822015-06-18 13:15:17 -070075 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070076
77 /**
78 * Constant for an invalid resource id.
79 */
80 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070081
82 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080083 INPUT, // Result and formula both visible, no evaluation requested,
84 // Though result may be visible on bottom line.
85 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
Hans Boehmc1ea0912015-06-19 15:05:07 -070086 // Not used for instant result evaluation.
Hans Boehm84614952014-11-25 18:46:17 -080087 INIT, // Very temporary state used as alternative to EVALUATE
88 // during reinitialization. Do not animate on completion.
89 ANIMATE, // Result computed, animation to enlarge result window in progress.
90 RESULT, // Result displayed, formula invisible.
91 // If we are in RESULT state, the formula was evaluated without
92 // error to initial precision.
93 ERROR // Error displayed: Formula visible, result shows error message.
94 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -070095 }
Hans Boehm84614952014-11-25 18:46:17 -080096 // Normal transition sequence is
97 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
98 // A RESULT -> ERROR transition is possible in rare corner cases, in which
99 // a higher precision evaluation exposes an error. This is possible, since we
100 // initially evaluate assuming we were given a well-defined problem. If we
101 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
102 // unless we are asked for enough precision that we can distinguish the argument from zero.
103 // TODO: Consider further heuristics to reduce the chance of observing this?
104 // It already seems to be observable only in contrived cases.
105 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
106 // is restarted in that state. This leads us to recompute and redisplay the result
107 // ASAP.
108 // TODO: Possibly save a bit more information, e.g. its initial display string
109 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700110
Justin Klaassen44595162015-05-28 17:55:20 -0700111 private final Property<TextView, Integer> TEXT_COLOR =
112 new Property<TextView, Integer>(Integer.class, "textColor") {
113 @Override
114 public Integer get(TextView textView) {
115 return textView.getCurrentTextColor();
116 }
117
118 @Override
119 public void set(TextView textView, Integer textColor) {
120 textView.setTextColor(textColor);
121 }
122 };
123
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700124 // We currently assume that the formula does not change out from under us in
125 // any way. We explicitly handle all input to the formula here.
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700126 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
127 @Override
128 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm1176f232015-05-11 16:26:03 -0700129 stopActionMode();
Justin Klaassen06c49442015-06-04 14:39:27 -0700130 // Never consume DPAD key events.
131 switch (keyCode) {
132 case KeyEvent.KEYCODE_DPAD_UP:
133 case KeyEvent.KEYCODE_DPAD_DOWN:
134 case KeyEvent.KEYCODE_DPAD_LEFT:
135 case KeyEvent.KEYCODE_DPAD_RIGHT:
136 return false;
137 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700138 // Always cancel unrequested in-progress evaluation, so that we don't have
139 // to worry about subsequent asynchronous completion.
140 // Requested in-progress evaluations are handled below.
141 if (mCurrentState != CalculatorState.EVALUATE) {
142 mEvaluator.cancelAll(true);
143 }
144 // In other cases we go ahead and process the input normally after cancelling:
Justin Klaassen06c49442015-06-04 14:39:27 -0700145 if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
146 return true;
147 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700148 switch (keyCode) {
149 case KeyEvent.KEYCODE_NUMPAD_ENTER:
150 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700151 case KeyEvent.KEYCODE_DPAD_CENTER:
152 mCurrentButton = mEqualButton;
153 onEquals();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700154 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700155 case KeyEvent.KEYCODE_DEL:
156 mCurrentButton = mDeleteButton;
157 onDelete();
158 return true;
159 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700160 cancelIfEvaluating(false);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700161 final int raw = keyEvent.getKeyCharacterMap()
Justin Klaassen44595162015-05-28 17:55:20 -0700162 .get(keyCode, keyEvent.getMetaState());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700163 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
164 return true; // discard
165 }
166 // Try to discard non-printing characters and the like.
167 // The user will have to explicitly delete other junk that gets past us.
168 if (Character.isIdentifierIgnorable(raw)
Justin Klaassen44595162015-05-28 17:55:20 -0700169 || Character.isWhitespace(raw)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700170 return true;
171 }
Justin Klaassen44595162015-05-28 17:55:20 -0700172 char c = (char) raw;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700173 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700174 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700175 onEquals();
176 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700177 addChars(String.valueOf(c), true);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700178 redisplayAfterFormulaChange();
179 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700180 }
181 return false;
182 }
183 };
184
Hans Boehm84614952014-11-25 18:46:17 -0800185 private static final String NAME = Calculator.class.getName();
186 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700187 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Hans Boehm84614952014-11-25 18:46:17 -0800188 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
189 // Associated value is a byte array holding both mCalculatorState
190 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700191
Justin Klaassen4b3af052014-05-27 17:53:10 -0700192 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800193 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700194
Justin Klaassen06360f92014-08-28 11:08:44 -0700195 private View mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700196 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700197 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700198 private CalculatorResult mResultText;
Justin Klaassend48b7562015-04-16 16:51:38 -0700199
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100200 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700201 private View mDeleteButton;
202 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700203 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700204
205 private TextView mInverseToggle;
206 private TextView mModeToggle;
207
Justin Klaassen721ec842015-05-28 14:30:08 -0700208 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700209 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700210
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700211 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700212 private Animator mCurrentAnimator;
213
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700214 private String mUnprocessedChars = null; // Characters that were recently entered
215 // at the end of the display that have not yet
216 // been added to the underlying expression.
217
Justin Klaassen4b3af052014-05-27 17:53:10 -0700218 @Override
219 protected void onCreate(Bundle savedInstanceState) {
220 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700221 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700222 setActionBar((Toolbar) findViewById(R.id.toolbar));
223
224 // Hide all default options in the ActionBar.
225 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700226
Justin Klaassen06360f92014-08-28 11:08:44 -0700227 mDisplayView = findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700228 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700229 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700230 mResultText = (CalculatorResult) findViewById(R.id.result);
Justin Klaassend48b7562015-04-16 16:51:38 -0700231
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100232 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700233 mDeleteButton = findViewById(R.id.del);
234 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700235 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
236 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
237 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
238 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700239
240 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
241 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
242
Justin Klaassen721ec842015-05-28 14:30:08 -0700243 mInvertibleButtons = new View[] {
244 findViewById(R.id.fun_sin),
245 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700246 findViewById(R.id.fun_tan),
247 findViewById(R.id.fun_ln),
248 findViewById(R.id.fun_log),
249 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700250 };
251 mInverseButtons = new View[] {
252 findViewById(R.id.fun_arcsin),
253 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700254 findViewById(R.id.fun_arctan),
255 findViewById(R.id.fun_exp),
256 findViewById(R.id.fun_10pow),
257 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700258 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700259
Justin Klaassen44595162015-05-28 17:55:20 -0700260 mEvaluator = new Evaluator(this, mResultText);
261 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700262 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700263
Hans Boehm84614952014-11-25 18:46:17 -0800264 if (savedInstanceState != null) {
265 setState(CalculatorState.values()[
266 savedInstanceState.getInt(KEY_DISPLAY_STATE,
267 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700268 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
269 if (unprocessed != null) {
270 mUnprocessedChars = unprocessed.toString();
271 }
272 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800273 if (state != null) {
274 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
275 mEvaluator.restoreInstanceState(in);
276 } catch (Throwable ignored) {
277 // When in doubt, revert to clean state
278 mCurrentState = CalculatorState.INPUT;
279 mEvaluator.clear();
280 }
281 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700282 } else {
283 mCurrentState = CalculatorState.INPUT;
284 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800285 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700286
Hans Boehm08e8f322015-04-21 13:18:38 -0700287 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
288 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700289 mFormulaText.setOnPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700290 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700291
292 onInverseToggled(mInverseToggle.isSelected());
293 onModeChanged(mEvaluator.getDegreeMode());
294
Hans Boehm84614952014-11-25 18:46:17 -0800295 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700296 // Just reevaluate.
297 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800298 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800299 mEvaluator.requireResult();
300 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700301 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800302 }
303 // TODO: We're currently not saving and restoring scroll position.
304 // We probably should. Details may require care to deal with:
305 // - new display size
306 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700307 }
308
309 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700310 protected void onSaveInstanceState(@NonNull Bundle outState) {
311 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
312 if (mCurrentAnimator != null) {
313 mCurrentAnimator.cancel();
314 }
315
Justin Klaassen4b3af052014-05-27 17:53:10 -0700316 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800317 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700318 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800319 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
320 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
321 mEvaluator.saveInstanceState(out);
322 } catch (IOException e) {
323 // Impossible; No IO involved.
324 throw new AssertionError("Impossible IO exception", e);
325 }
326 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700327 }
328
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700329 // Set the state, updating delete label and display colors.
330 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700331 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700332 private void setState(CalculatorState state) {
333 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800334 if (state == CalculatorState.INPUT) {
335 restoreDisplayPositions();
336 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700337 mCurrentState = state;
338
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700339 if (mCurrentState == CalculatorState.RESULT) {
340 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700341 mDeleteButton.setVisibility(View.GONE);
342 mClearButton.setVisibility(View.VISIBLE);
343 } else {
344 mDeleteButton.setVisibility(View.VISIBLE);
345 mClearButton.setVisibility(View.GONE);
346 }
347
Hans Boehm84614952014-11-25 18:46:17 -0800348 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700349 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700350 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700351 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700352 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700353 } else if (mCurrentState != CalculatorState.RESULT) {
354 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
355 mResultText.setTextColor(getColor(R.color.display_result_text_color));
356 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700357 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700358
359 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700360 }
361 }
362
Hans Boehm1176f232015-05-11 16:26:03 -0700363 // Stop any active ActionMode. Return true if there was one.
364 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700365 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700366 return true;
367 }
368 if (mFormulaText.stopActionMode()) {
369 return true;
370 }
371 return false;
372 }
373
Justin Klaassen4b3af052014-05-27 17:53:10 -0700374 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100375 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700376 if (!stopActionMode()) {
377 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
378 // Select the previous pad.
379 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
380 } else {
381 // If the user is currently looking at the first pad (or the pad is not paged),
382 // allow the system to handle the Back button.
383 super.onBackPressed();
384 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100385 }
386 }
387
388 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700389 public void onUserInteraction() {
390 super.onUserInteraction();
391
Hans Boehmc1ea0912015-06-19 15:05:07 -0700392 // If there's an animation in progress, end it immediately, so the user interaction can
393 // be handled.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700394 if (mCurrentAnimator != null) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700395 mCurrentAnimator.end();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700396 }
397 }
398
Justin Klaassene2711cb2015-05-28 11:13:17 -0700399 /**
400 * Invoked whenever the inverse button is toggled to update the UI.
401 *
402 * @param showInverse {@code true} if inverse functions should be shown
403 */
404 private void onInverseToggled(boolean showInverse) {
405 if (showInverse) {
406 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700407 for (View invertibleButton : mInvertibleButtons) {
408 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700409 }
410 for (View inverseButton : mInverseButtons) {
411 inverseButton.setVisibility(View.VISIBLE);
412 }
413 } else {
414 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700415 for (View invertibleButton : mInvertibleButtons) {
416 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700417 }
418 for (View inverseButton : mInverseButtons) {
419 inverseButton.setVisibility(View.GONE);
420 }
421 }
422 }
423
424 /**
425 * Invoked whenever the deg/rad mode may have changed to update the UI.
426 *
427 * @param degreeMode {@code true} if in degree mode
428 */
429 private void onModeChanged(boolean degreeMode) {
430 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700431 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700432 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
433
434 mModeToggle.setText(R.string.mode_rad);
435 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700436 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700437 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700438 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
439
440 mModeToggle.setText(R.string.mode_deg);
441 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700442 }
443 }
Hans Boehm84614952014-11-25 18:46:17 -0800444
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700445 // Add the given button id to input expression.
446 // If appropriate, clear the expression before doing so.
447 private void addKeyToExpr(int id) {
Hans Boehm017de982015-06-10 17:46:03 -0700448 // FIXME: Other states?
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700449 if (mCurrentState == CalculatorState.ERROR) {
450 setState(CalculatorState.INPUT);
451 } else if (mCurrentState == CalculatorState.RESULT) {
452 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
453 mEvaluator.collapse();
454 } else {
455 mEvaluator.clear();
456 }
457 setState(CalculatorState.INPUT);
458 }
459 if (!mEvaluator.append(id)) {
460 // TODO: Some user visible feedback?
461 }
462 }
463
Hans Boehm017de982015-06-10 17:46:03 -0700464 /**
465 * Add the given button id to input expression, assuming it was explicitly
466 * typed/touched.
467 * We perform slightly more aggressive correction than in pasted expressions.
468 */
469 private void addExplicitKeyToExpr(int id) {
470 if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
471 mEvaluator.getExpr().removeTrailingAdditiveOperators();
472 }
473 addKeyToExpr(id);
474 }
475
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700476 private void redisplayAfterFormulaChange() {
477 // TODO: Could do this more incrementally.
478 redisplayFormula();
479 setState(CalculatorState.INPUT);
Hans Boehmc023b732015-04-29 11:30:47 -0700480 if (mEvaluator.getExpr().hasInterestingOps()) {
481 mEvaluator.evaluateAndShowResult();
482 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700483 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700484 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700485 }
486
Justin Klaassen4b3af052014-05-27 17:53:10 -0700487 public void onButtonClick(View view) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700488 // Any animation is ended before we get here.
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700489 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700490 stopActionMode();
Hans Boehmc1ea0912015-06-19 15:05:07 -0700491 // See onKey above for the rationale behind some of the behavior below:
492 if (mCurrentState != CalculatorState.EVALUATE) {
493 // Cancel evaluations that were not specifically requested.
494 mEvaluator.cancelAll(true);
Hans Boehm84614952014-11-25 18:46:17 -0800495 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700496 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800497 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700498 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700499 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700500 break;
501 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700502 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700503 break;
504 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700505 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700506 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700507 case R.id.toggle_inv:
508 final boolean selected = !mInverseToggle.isSelected();
509 mInverseToggle.setSelected(selected);
510 onInverseToggled(selected);
Hans Boehmc1ea0912015-06-19 15:05:07 -0700511 if (mCurrentState == CalculatorState.RESULT) {
512 mResultText.redisplay(); // In case we cancelled reevaluation.
513 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700514 break;
515 case R.id.toggle_mode:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700516 cancelIfEvaluating(false);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700517 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700518 if (mCurrentState == CalculatorState.RESULT) {
519 mEvaluator.collapse(); // Capture result evaluated in old mode
520 redisplayFormula();
521 }
522 // In input mode, we reinterpret already entered trig functions.
523 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700524 onModeChanged(mode);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700525 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700526 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700527 if (mEvaluator.getExpr().hasInterestingOps()) {
528 mEvaluator.evaluateAndShowResult();
529 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700530 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700531 default:
Hans Boehmc1ea0912015-06-19 15:05:07 -0700532 cancelIfEvaluating(false);
Hans Boehm017de982015-06-10 17:46:03 -0700533 addExplicitKeyToExpr(id);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700534 redisplayAfterFormulaChange();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700535 break;
536 }
537 }
538
Hans Boehm84614952014-11-25 18:46:17 -0800539 void redisplayFormula() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700540 String formula = mEvaluator.getExpr().toString(this);
541 if (mUnprocessedChars != null) {
542 // Add and highlight characters we couldn't process.
543 SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
544 // TODO: should probably match this to the error color.
545 formatted.setSpan(new ForegroundColorSpan(Color.RED),
546 formula.length(), formatted.length(),
547 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm08e8f322015-04-21 13:18:38 -0700548 mFormulaText.setText(formatted);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700549 } else {
Hans Boehm08e8f322015-04-21 13:18:38 -0700550 mFormulaText.setText(formula);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700551 }
Hans Boehm84614952014-11-25 18:46:17 -0800552 }
553
Justin Klaassen4b3af052014-05-27 17:53:10 -0700554 @Override
555 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700556 mCurrentButton = view;
557
Justin Klaassen4b3af052014-05-27 17:53:10 -0700558 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700559 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700560 return true;
561 }
562 return false;
563 }
564
Hans Boehm84614952014-11-25 18:46:17 -0800565 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700566 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
567 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700568 // Invalidate any options that may depend on the current result.
569 invalidateOptionsMenu();
570
Hans Boehma0e45f32015-05-30 13:20:35 -0700571 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700572 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800573 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700574 }
Hans Boehm84614952014-11-25 18:46:17 -0800575 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700576
Hans Boehmc1ea0912015-06-19 15:05:07 -0700577 // Reset state to reflect evaluator cancellation. Invoked by evaluator.
Hans Boehm84614952014-11-25 18:46:17 -0800578 public void onCancelled() {
579 // We should be in EVALUATE state.
Hans Boehm84614952014-11-25 18:46:17 -0800580 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700581 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800582 }
583
584 // Reevaluation completed; ask result to redisplay current value.
585 public void onReevaluate()
586 {
Justin Klaassen44595162015-05-28 17:55:20 -0700587 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700588 }
589
Justin Klaassenfed941a2014-06-09 18:42:40 +0100590 @Override
591 public void onTextSizeChanged(final TextView textView, float oldSize) {
592 if (mCurrentState != CalculatorState.INPUT) {
593 // Only animate text changes that occur from user input.
594 return;
595 }
596
597 // Calculate the values needed to perform the scale and translation animations,
598 // maintaining the same apparent baseline for the displayed text.
599 final float textScale = oldSize / textView.getTextSize();
600 final float translationX = (1.0f - textScale) *
601 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
602 final float translationY = (1.0f - textScale) *
603 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
604
605 final AnimatorSet animatorSet = new AnimatorSet();
606 animatorSet.playTogether(
607 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
608 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
609 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
610 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700611 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100612 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
613 animatorSet.start();
614 }
615
Hans Boehmc1ea0912015-06-19 15:05:07 -0700616 /**
617 * Cancel any in-progress explicitly requested evaluations.
618 * @param quiet suppress pop-up message. Explicit evaluation can change the expression
619 value, and certainly changes the display, so it seems reasonable to warn.
620 * @return true if there was such an evaluation
621 */
622 private boolean cancelIfEvaluating(boolean quiet) {
623 if (mCurrentState == CalculatorState.EVALUATE) {
624 mEvaluator.cancelAll(quiet);
625 return true;
626 } else {
627 return false;
628 }
629 }
630
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700631 private void onEquals() {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700632 // In non-INPUT state assume this was redundant and ignore it.
Hans Boehmc023b732015-04-29 11:30:47 -0700633 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700634 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800635 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700636 }
637 }
638
639 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700640 // Delete works like backspace; remove the last character or operator from the expression.
641 // Note that we handle keyboard delete exactly like the delete button. For
642 // example the delete button can be used to delete a character from an incomplete
643 // function name typed on a physical keyboard.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700644 // This should be impossible in RESULT state.
Hans Boehmc1ea0912015-06-19 15:05:07 -0700645 // If there is an in-progress explicit evaluation, just cancel it and return.
646 if (cancelIfEvaluating(false)) return;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700647 setState(CalculatorState.INPUT);
648 if (mUnprocessedChars != null) {
649 int len = mUnprocessedChars.length();
650 if (len > 0) {
651 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
652 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700653 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700654 }
655 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700656 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700657 }
658 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700659 }
660
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700661 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700662 final ViewGroupOverlay groupOverlay =
663 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700664
665 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700666 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700667
668 // Make reveal cover the display and status bar.
669 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700670 revealView.setBottom(displayRect.bottom);
671 revealView.setLeft(displayRect.left);
672 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700673 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700674 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700675
Justin Klaassen4b3af052014-05-27 17:53:10 -0700676 final int[] clearLocation = new int[2];
677 sourceView.getLocationInWindow(clearLocation);
678 clearLocation[0] += sourceView.getWidth() / 2;
679 clearLocation[1] += sourceView.getHeight() / 2;
680
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700681 final int revealCenterX = clearLocation[0] - revealView.getLeft();
682 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700683
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700684 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
685 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
686 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700687 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
688
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700689 final Animator revealAnimator =
690 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700691 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700692 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700693 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700694 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700695
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700696 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700697 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700698 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700699
700 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700701 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700702 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
703 animatorSet.addListener(new AnimatorListenerAdapter() {
704 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700705 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700706 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700707 mCurrentAnimator = null;
708 }
709 });
710
711 mCurrentAnimator = animatorSet;
712 animatorSet.start();
713 }
714
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700715 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800716 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700717 return;
718 }
Hans Boehmc1ea0912015-06-19 15:05:07 -0700719 cancelIfEvaluating(true);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700720 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
721 @Override
722 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700723 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700724 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700725 mEvaluator.clear();
726 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800727 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700728 }
729 });
730 }
731
Hans Boehm84614952014-11-25 18:46:17 -0800732 // Evaluation encountered en error. Display the error.
733 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700734 if (mCurrentState == CalculatorState.EVALUATE) {
735 setState(CalculatorState.ANIMATE);
736 reveal(mCurrentButton, R.color.calculator_error_color,
737 new AnimatorListenerAdapter() {
738 @Override
739 public void onAnimationEnd(Animator animation) {
740 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700741 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700742 }
743 });
744 } else if (mCurrentState == CalculatorState.INIT) {
745 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700746 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700747 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700748 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700749 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700750 }
751
Hans Boehm84614952014-11-25 18:46:17 -0800752
753 // Animate movement of result into the top formula slot.
754 // Result window now remains translated in the top slot while the result is displayed.
755 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700756 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800757 // formula and result displays back at the end of the animation. We no longer do that,
758 // so that we can continue to properly support scrolling of the result.
759 // We assume the result already contains the text to be expanded.
760 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700761 // Calculate the textSize that would be used to display the result in the formula.
762 // For scrollable results just use the minimum textSize to maximize the number of digits
763 // that are visible on screen.
764 float textSize = mFormulaText.getMinimumTextSize();
765 if (!mResultText.isScrollable()) {
766 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
767 }
768
769 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
770 // when a result is reused in a subsequent expression.
771 final float resultScale = textSize / mResultText.getTextSize();
772
773 // Set the result's pivot to match its gravity.
774 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
775 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
776
777 // Calculate the necessary translations so the result takes the place of the formula and
778 // the formula moves off the top of the screen.
779 final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
780 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
Hans Boehm08e8f322015-04-21 13:18:38 -0700781 final float formulaTranslationY = -mFormulaText.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700782
Justin Klaassen44595162015-05-28 17:55:20 -0700783 // Change the result's textColor to match the formula.
784 final int formulaTextColor = mFormulaText.getCurrentTextColor();
785
Hans Boehm84614952014-11-25 18:46:17 -0800786 if (animate) {
Hans Boehmc1ea0912015-06-19 15:05:07 -0700787 setState(CalculatorState.ANIMATE);
Hans Boehm84614952014-11-25 18:46:17 -0800788 final AnimatorSet animatorSet = new AnimatorSet();
789 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700790 ObjectAnimator.ofPropertyValuesHolder(mResultText,
791 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
792 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
793 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
794 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
795 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
796 animatorSet.setDuration(getResources().getInteger(
797 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800798 animatorSet.addListener(new AnimatorListenerAdapter() {
799 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800800 public void onAnimationEnd(Animator animation) {
801 setState(CalculatorState.RESULT);
802 mCurrentAnimator = null;
803 }
804 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700805
Hans Boehm84614952014-11-25 18:46:17 -0800806 mCurrentAnimator = animatorSet;
807 animatorSet.start();
808 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700809 mResultText.setScaleX(resultScale);
810 mResultText.setScaleY(resultScale);
811 mResultText.setTranslationY(resultTranslationY);
812 mResultText.setTextColor(formulaTextColor);
Hans Boehm08e8f322015-04-21 13:18:38 -0700813 mFormulaText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700814 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800815 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700816 }
Hans Boehm84614952014-11-25 18:46:17 -0800817
818 // Restore positions of the formula and result displays back to their original,
819 // pre-animation state.
820 private void restoreDisplayPositions() {
821 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700822 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800823 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700824 mResultText.setScaleX(1.0f);
825 mResultText.setScaleY(1.0f);
826 mResultText.setTranslationX(0.0f);
827 mResultText.setTranslationY(0.0f);
Hans Boehm08e8f322015-04-21 13:18:38 -0700828 mFormulaText.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800829
Hans Boehm08e8f322015-04-21 13:18:38 -0700830 mFormulaText.requestFocus();
Hans Boehm84614952014-11-25 18:46:17 -0800831 }
832
Justin Klaassend48b7562015-04-16 16:51:38 -0700833 @Override
834 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700835 super.onCreateOptionsMenu(menu);
836
837 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700838 return true;
839 }
840
841 @Override
842 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700843 super.onPrepareOptionsMenu(menu);
844
845 // Show the leading option when displaying a result.
846 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
847
848 // Show the fraction option when displaying a rational result.
849 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
850 && mEvaluator.getRational() != null);
851
Justin Klaassend48b7562015-04-16 16:51:38 -0700852 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800853 }
854
855 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700856 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800857 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700858 case R.id.menu_leading:
859 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800860 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700861 case R.id.menu_fraction:
862 displayFraction();
863 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700864 case R.id.menu_licenses:
865 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700866 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800867 default:
868 return super.onOptionsItemSelected(item);
869 }
870 }
871
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700872 private void displayMessage(String s) {
Justin Klaassen44595162015-05-28 17:55:20 -0700873 new AlertDialog.Builder(this)
874 .setMessage(s)
875 .setNegativeButton(R.string.dismiss, null /* listener */)
876 .show();
Hans Boehm84614952014-11-25 18:46:17 -0800877 }
878
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700879 private void displayFraction() {
880 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700881 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700882 }
883
884 // Display full result to currently evaluated precision
885 private void displayFull() {
886 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -0700887 String msg = mResultText.getFullText() + " ";
888 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700889 msg += res.getString(R.string.exact);
890 } else {
891 msg += res.getString(R.string.approximate);
892 }
893 displayMessage(msg);
894 }
895
Hans Boehm017de982015-06-10 17:46:03 -0700896 /**
897 * Add input characters to the end of the expression.
898 * Map them to the appropriate button pushes when possible. Leftover characters
899 * are added to mUnprocessedChars, which is presumed to immediately precede the newly
900 * added characters.
901 * @param moreChars Characters to be added.
902 * @param explicit These characters were explicitly typed by the user.
903 */
904 private void addChars(String moreChars, boolean explicit) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700905 if (mUnprocessedChars != null) {
906 moreChars = mUnprocessedChars + moreChars;
907 }
908 int current = 0;
909 int len = moreChars.length();
910 while (current < len) {
911 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -0700912 int k = KeyMaps.keyForChar(c);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700913 if (k != View.NO_ID) {
914 mCurrentButton = findViewById(k);
Hans Boehm017de982015-06-10 17:46:03 -0700915 if (explicit) {
916 addExplicitKeyToExpr(k);
917 } else {
918 addKeyToExpr(k);
919 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700920 if (Character.isSurrogate(c)) {
921 current += 2;
922 } else {
923 ++current;
924 }
925 continue;
926 }
Hans Boehm013969e2015-04-13 20:29:47 -0700927 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700928 if (f != View.NO_ID) {
929 mCurrentButton = findViewById(f);
Hans Boehm017de982015-06-10 17:46:03 -0700930 if (explicit) {
931 addExplicitKeyToExpr(f);
932 } else {
933 addKeyToExpr(f);
934 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700935 if (f == R.id.op_sqrt) {
936 // Square root entered as function; don't lose the parenthesis.
937 addKeyToExpr(R.id.lparen);
938 }
939 current = moreChars.indexOf('(', current) + 1;
940 continue;
941 }
942 // There are characters left, but we can't convert them to button presses.
943 mUnprocessedChars = moreChars.substring(current);
944 redisplayAfterFormulaChange();
945 return;
946 }
947 mUnprocessedChars = null;
948 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800949 }
950
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700951 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700952 public boolean onPaste(ClipData clip) {
953 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
954 if (item == null) {
955 // nothing to paste, bail early...
956 return false;
957 }
958
959 // Check if the item is a previously copied result, otherwise paste as raw text.
960 final Uri uri = item.getUri();
961 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700962 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700963 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700964 setState(CalculatorState.INPUT);
965 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800966 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700967 mEvaluator.addSaved();
968 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700969 } else {
Hans Boehm017de982015-06-10 17:46:03 -0700970 addChars(item.coerceToText(this).toString(), false);
Hans Boehm84614952014-11-25 18:46:17 -0800971 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700972 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800973 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700974}