blob: 87e011a417f0491259133f530940df0268618894 [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// FIXME: Menu handling, particularly for cut/paste, is very ugly
18// and not the way it was intended.
19// Other menus are not handled brilliantly either.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070020// TODO: Better indication of when the result is known to be exact.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070021// TODO: Check and possibly fix accessability issues.
Hans Boehm013969e2015-04-13 20:29:47 -070022// TODO: Copy & more general paste in formula? Note that this requires
23// great care: Currently the text version of a displayed formula
24// is not directly useful for re-evaluating the formula later, since
25// it contains ellipses representing subexpressions evaluated with
26// a different degree mode. Rather than supporting copy from the
27// formula window, we may eventually want to support generation of a
28// more useful text version in a separate window. It's not clear
29// this is worth the added (code and user) complexity.
Hans Boehm84614952014-11-25 18:46:17 -080030
Justin Klaassen4b3af052014-05-27 17:53:10 -070031package com.android.calculator2;
32
33import android.animation.Animator;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070034import android.animation.Animator.AnimatorListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070035import android.animation.AnimatorListenerAdapter;
36import android.animation.AnimatorSet;
Justin Klaassen4b3af052014-05-27 17:53:10 -070037import android.animation.ObjectAnimator;
Justin Klaassen44595162015-05-28 17:55:20 -070038import android.animation.PropertyValuesHolder;
Justin Klaassen4b3af052014-05-27 17:53:10 -070039import android.app.Activity;
Hans Boehm84614952014-11-25 18:46:17 -080040import android.app.AlertDialog;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070041import android.content.ClipData;
Justin Klaassend36d63e2015-05-05 12:59:36 -070042import android.content.Intent;
Hans Boehmbfe8c222015-04-02 16:26:07 -070043import android.content.res.Resources;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070044import android.graphics.Color;
Justin Klaassen8fff1442014-06-19 10:43:29 -070045import android.graphics.Rect;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070046import android.net.Uri;
Justin Klaassen4b3af052014-05-27 17:53:10 -070047import android.os.Bundle;
Justin Klaassenf79d6f62014-08-26 12:27:08 -070048import android.support.annotation.NonNull;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +010049import android.support.v4.view.ViewPager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070050import android.text.SpannableString;
51import android.text.Spanned;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070052import android.text.style.ForegroundColorSpan;
Justin Klaassen44595162015-05-28 17:55:20 -070053import android.util.Property;
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;
Justin Klaassen4b3af052014-05-27 17:53:10 -070063import android.view.animation.AccelerateDecelerateInterpolator;
Justin Klaassenfed941a2014-06-09 18:42:40 +010064import android.widget.TextView;
Justin Klaassend48b7562015-04-16 16:51:38 -070065import android.widget.Toolbar;
Justin Klaassenfed941a2014-06-09 18:42:40 +010066
Hans Boehm08e8f322015-04-21 13:18:38 -070067import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080068
69import java.io.ByteArrayInputStream;
Hans Boehm84614952014-11-25 18:46:17 -080070import java.io.ByteArrayOutputStream;
Hans Boehm84614952014-11-25 18:46:17 -080071import java.io.IOException;
Justin Klaassen721ec842015-05-28 14:30:08 -070072import java.io.ObjectInput;
73import java.io.ObjectInputStream;
74import java.io.ObjectOutput;
75import java.io.ObjectOutputStream;
Justin Klaassen4b3af052014-05-27 17:53:10 -070076
Justin Klaassen04f79c72014-06-27 17:25:35 -070077public class Calculator extends Activity
Justin Klaassenfc5ac822015-06-18 13:15:17 -070078 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -070079
80 /**
81 * Constant for an invalid resource id.
82 */
83 public static final int INVALID_RES_ID = -1;
Justin Klaassen4b3af052014-05-27 17:53:10 -070084
85 private enum CalculatorState {
Hans Boehm84614952014-11-25 18:46:17 -080086 INPUT, // Result and formula both visible, no evaluation requested,
87 // Though result may be visible on bottom line.
88 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete.
89 INIT, // Very temporary state used as alternative to EVALUATE
90 // during reinitialization. Do not animate on completion.
91 ANIMATE, // Result computed, animation to enlarge result window in progress.
92 RESULT, // Result displayed, formula invisible.
93 // If we are in RESULT state, the formula was evaluated without
94 // error to initial precision.
95 ERROR // Error displayed: Formula visible, result shows error message.
96 // Display similar to INPUT state.
Justin Klaassen4b3af052014-05-27 17:53:10 -070097 }
Hans Boehm84614952014-11-25 18:46:17 -080098 // Normal transition sequence is
99 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
100 // A RESULT -> ERROR transition is possible in rare corner cases, in which
101 // a higher precision evaluation exposes an error. This is possible, since we
102 // initially evaluate assuming we were given a well-defined problem. If we
103 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
104 // unless we are asked for enough precision that we can distinguish the argument from zero.
105 // TODO: Consider further heuristics to reduce the chance of observing this?
106 // It already seems to be observable only in contrived cases.
107 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
108 // is restarted in that state. This leads us to recompute and redisplay the result
109 // ASAP.
110 // TODO: Possibly save a bit more information, e.g. its initial display string
111 // or most significant digit position, to speed up restart.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700112
Justin Klaassen44595162015-05-28 17:55:20 -0700113 private final Property<TextView, Integer> TEXT_COLOR =
114 new Property<TextView, Integer>(Integer.class, "textColor") {
115 @Override
116 public Integer get(TextView textView) {
117 return textView.getCurrentTextColor();
118 }
119
120 @Override
121 public void set(TextView textView, Integer textColor) {
122 textView.setTextColor(textColor);
123 }
124 };
125
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700126 // We currently assume that the formula does not change out from under us in
127 // any way. We explicitly handle all input to the formula here.
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700128 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
129 @Override
130 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Hans Boehm1176f232015-05-11 16:26:03 -0700131 stopActionMode();
Justin Klaassen06c49442015-06-04 14:39:27 -0700132
133 // Never consume DPAD key events.
134 switch (keyCode) {
135 case KeyEvent.KEYCODE_DPAD_UP:
136 case KeyEvent.KEYCODE_DPAD_DOWN:
137 case KeyEvent.KEYCODE_DPAD_LEFT:
138 case KeyEvent.KEYCODE_DPAD_RIGHT:
139 return false;
140 }
141
142 if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
143 return true;
144 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700145 switch (keyCode) {
146 case KeyEvent.KEYCODE_NUMPAD_ENTER:
147 case KeyEvent.KEYCODE_ENTER:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700148 case KeyEvent.KEYCODE_DPAD_CENTER:
149 mCurrentButton = mEqualButton;
150 onEquals();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700151 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700152 case KeyEvent.KEYCODE_DEL:
153 mCurrentButton = mDeleteButton;
154 onDelete();
155 return true;
156 default:
157 final int raw = keyEvent.getKeyCharacterMap()
Justin Klaassen44595162015-05-28 17:55:20 -0700158 .get(keyCode, keyEvent.getMetaState());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700159 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
160 return true; // discard
161 }
162 // Try to discard non-printing characters and the like.
163 // The user will have to explicitly delete other junk that gets past us.
164 if (Character.isIdentifierIgnorable(raw)
Justin Klaassen44595162015-05-28 17:55:20 -0700165 || Character.isWhitespace(raw)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700166 return true;
167 }
Justin Klaassen44595162015-05-28 17:55:20 -0700168 char c = (char) raw;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700169 if (c == '=') {
Hans Boehme57fb012015-05-07 19:52:32 -0700170 mCurrentButton = mEqualButton;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700171 onEquals();
172 } else {
173 addChars(String.valueOf(c));
174 redisplayAfterFormulaChange();
175 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700176 }
177 return false;
178 }
179 };
180
Hans Boehm84614952014-11-25 18:46:17 -0800181 private static final String NAME = Calculator.class.getName();
182 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
Hans Boehm760a9dc2015-04-20 10:27:12 -0700183 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
Hans Boehm84614952014-11-25 18:46:17 -0800184 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
185 // Associated value is a byte array holding both mCalculatorState
186 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700187
Justin Klaassen4b3af052014-05-27 17:53:10 -0700188 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800189 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700190
Justin Klaassen06360f92014-08-28 11:08:44 -0700191 private View mDisplayView;
Justin Klaassend48b7562015-04-16 16:51:38 -0700192 private TextView mModeView;
Hans Boehm08e8f322015-04-21 13:18:38 -0700193 private CalculatorText mFormulaText;
Justin Klaassen44595162015-05-28 17:55:20 -0700194 private CalculatorResult mResultText;
Justin Klaassend48b7562015-04-16 16:51:38 -0700195
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100196 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700197 private View mDeleteButton;
198 private View mClearButton;
Justin Klaassend48b7562015-04-16 16:51:38 -0700199 private View mEqualButton;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700200
201 private TextView mInverseToggle;
202 private TextView mModeToggle;
203
Justin Klaassen721ec842015-05-28 14:30:08 -0700204 private View[] mInvertibleButtons;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700205 private View[] mInverseButtons;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700206
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700207 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700208 private Animator mCurrentAnimator;
209
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700210 private String mUnprocessedChars = null; // Characters that were recently entered
211 // at the end of the display that have not yet
212 // been added to the underlying expression.
213
Justin Klaassen4b3af052014-05-27 17:53:10 -0700214 @Override
215 protected void onCreate(Bundle savedInstanceState) {
216 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700217 setContentView(R.layout.activity_calculator);
Justin Klaassend48b7562015-04-16 16:51:38 -0700218 setActionBar((Toolbar) findViewById(R.id.toolbar));
219
220 // Hide all default options in the ActionBar.
221 getActionBar().setDisplayOptions(0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700222
Justin Klaassen06360f92014-08-28 11:08:44 -0700223 mDisplayView = findViewById(R.id.display);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700224 mModeView = (TextView) findViewById(R.id.mode);
Hans Boehm08e8f322015-04-21 13:18:38 -0700225 mFormulaText = (CalculatorText) findViewById(R.id.formula);
Justin Klaassen44595162015-05-28 17:55:20 -0700226 mResultText = (CalculatorResult) findViewById(R.id.result);
Justin Klaassend48b7562015-04-16 16:51:38 -0700227
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100228 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700229 mDeleteButton = findViewById(R.id.del);
230 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700231 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
232 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
233 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
234 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700235
236 mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
237 mModeToggle = (TextView) findViewById(R.id.toggle_mode);
238
Justin Klaassen721ec842015-05-28 14:30:08 -0700239 mInvertibleButtons = new View[] {
240 findViewById(R.id.fun_sin),
241 findViewById(R.id.fun_cos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700242 findViewById(R.id.fun_tan),
243 findViewById(R.id.fun_ln),
244 findViewById(R.id.fun_log),
245 findViewById(R.id.op_sqrt)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700246 };
247 mInverseButtons = new View[] {
248 findViewById(R.id.fun_arcsin),
249 findViewById(R.id.fun_arccos),
Hans Boehm4db31b42015-05-31 12:19:05 -0700250 findViewById(R.id.fun_arctan),
251 findViewById(R.id.fun_exp),
252 findViewById(R.id.fun_10pow),
253 findViewById(R.id.op_sqr)
Justin Klaassene2711cb2015-05-28 11:13:17 -0700254 };
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700255
Justin Klaassen44595162015-05-28 17:55:20 -0700256 mEvaluator = new Evaluator(this, mResultText);
257 mResultText.setEvaluator(mEvaluator);
Hans Boehm013969e2015-04-13 20:29:47 -0700258 KeyMaps.setActivity(this);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700259
Hans Boehm84614952014-11-25 18:46:17 -0800260 if (savedInstanceState != null) {
261 setState(CalculatorState.values()[
262 savedInstanceState.getInt(KEY_DISPLAY_STATE,
263 CalculatorState.INPUT.ordinal())]);
Hans Boehm760a9dc2015-04-20 10:27:12 -0700264 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
265 if (unprocessed != null) {
266 mUnprocessedChars = unprocessed.toString();
267 }
268 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
Hans Boehm84614952014-11-25 18:46:17 -0800269 if (state != null) {
270 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
271 mEvaluator.restoreInstanceState(in);
272 } catch (Throwable ignored) {
273 // When in doubt, revert to clean state
274 mCurrentState = CalculatorState.INPUT;
275 mEvaluator.clear();
276 }
277 }
Hans Boehmfbcef702015-04-27 18:07:47 -0700278 } else {
279 mCurrentState = CalculatorState.INPUT;
280 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800281 }
Justin Klaassene2711cb2015-05-28 11:13:17 -0700282
Hans Boehm08e8f322015-04-21 13:18:38 -0700283 mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
284 mFormulaText.setOnTextSizeChangeListener(this);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700285 mFormulaText.setOnPasteListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700286 mDeleteButton.setOnLongClickListener(this);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700287
288 onInverseToggled(mInverseToggle.isSelected());
289 onModeChanged(mEvaluator.getDegreeMode());
290
Hans Boehm84614952014-11-25 18:46:17 -0800291 if (mCurrentState != CalculatorState.INPUT) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700292 // Just reevaluate.
293 redisplayFormula();
Hans Boehm84614952014-11-25 18:46:17 -0800294 setState(CalculatorState.INIT);
Hans Boehm84614952014-11-25 18:46:17 -0800295 mEvaluator.requireResult();
296 } else {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700297 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800298 }
299 // TODO: We're currently not saving and restoring scroll position.
300 // We probably should. Details may require care to deal with:
301 // - new display size
302 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700303 }
304
305 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700306 protected void onSaveInstanceState(@NonNull Bundle outState) {
307 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
308 if (mCurrentAnimator != null) {
309 mCurrentAnimator.cancel();
310 }
311
Justin Klaassen4b3af052014-05-27 17:53:10 -0700312 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800313 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
Hans Boehm760a9dc2015-04-20 10:27:12 -0700314 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
Hans Boehm84614952014-11-25 18:46:17 -0800315 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
316 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
317 mEvaluator.saveInstanceState(out);
318 } catch (IOException e) {
319 // Impossible; No IO involved.
320 throw new AssertionError("Impossible IO exception", e);
321 }
322 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700323 }
324
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700325 // Set the state, updating delete label and display colors.
326 // This restores display positions on moving to INPUT.
Justin Klaassend48b7562015-04-16 16:51:38 -0700327 // But movement/animation for moving to RESULT has already been done.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700328 private void setState(CalculatorState state) {
329 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800330 if (state == CalculatorState.INPUT) {
331 restoreDisplayPositions();
332 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700333 mCurrentState = state;
334
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700335 if (mCurrentState == CalculatorState.RESULT) {
336 // No longer do this for ERROR; allow mistakes to be corrected.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700337 mDeleteButton.setVisibility(View.GONE);
338 mClearButton.setVisibility(View.VISIBLE);
339 } else {
340 mDeleteButton.setVisibility(View.VISIBLE);
341 mClearButton.setVisibility(View.GONE);
342 }
343
Hans Boehm84614952014-11-25 18:46:17 -0800344 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen44595162015-05-28 17:55:20 -0700345 final int errorColor = getColor(R.color.calculator_error_color);
Hans Boehm08e8f322015-04-21 13:18:38 -0700346 mFormulaText.setTextColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700347 mResultText.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700348 getWindow().setStatusBarColor(errorColor);
Justin Klaassen44595162015-05-28 17:55:20 -0700349 } else if (mCurrentState != CalculatorState.RESULT) {
350 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
351 mResultText.setTextColor(getColor(R.color.display_result_text_color));
352 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700353 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700354
355 invalidateOptionsMenu();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700356 }
357 }
358
Hans Boehm1176f232015-05-11 16:26:03 -0700359 // Stop any active ActionMode. Return true if there was one.
360 private boolean stopActionMode() {
Justin Klaassen44595162015-05-28 17:55:20 -0700361 if (mResultText.stopActionMode()) {
Hans Boehm1176f232015-05-11 16:26:03 -0700362 return true;
363 }
364 if (mFormulaText.stopActionMode()) {
365 return true;
366 }
367 return false;
368 }
369
Justin Klaassen4b3af052014-05-27 17:53:10 -0700370 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100371 public void onBackPressed() {
Hans Boehm1176f232015-05-11 16:26:03 -0700372 if (!stopActionMode()) {
373 if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
374 // Select the previous pad.
375 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
376 } else {
377 // If the user is currently looking at the first pad (or the pad is not paged),
378 // allow the system to handle the Back button.
379 super.onBackPressed();
380 }
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100381 }
382 }
383
384 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700385 public void onUserInteraction() {
386 super.onUserInteraction();
387
388 // If there's an animation in progress, cancel it so the user interaction can be handled
389 // immediately.
390 if (mCurrentAnimator != null) {
391 mCurrentAnimator.cancel();
392 }
393 }
394
Justin Klaassene2711cb2015-05-28 11:13:17 -0700395 /**
396 * Invoked whenever the inverse button is toggled to update the UI.
397 *
398 * @param showInverse {@code true} if inverse functions should be shown
399 */
400 private void onInverseToggled(boolean showInverse) {
401 if (showInverse) {
402 mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
Justin Klaassen721ec842015-05-28 14:30:08 -0700403 for (View invertibleButton : mInvertibleButtons) {
404 invertibleButton.setVisibility(View.GONE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700405 }
406 for (View inverseButton : mInverseButtons) {
407 inverseButton.setVisibility(View.VISIBLE);
408 }
409 } else {
410 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
Justin Klaassen721ec842015-05-28 14:30:08 -0700411 for (View invertibleButton : mInvertibleButtons) {
412 invertibleButton.setVisibility(View.VISIBLE);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700413 }
414 for (View inverseButton : mInverseButtons) {
415 inverseButton.setVisibility(View.GONE);
416 }
417 }
418 }
419
420 /**
421 * Invoked whenever the deg/rad mode may have changed to update the UI.
422 *
423 * @param degreeMode {@code true} if in degree mode
424 */
425 private void onModeChanged(boolean degreeMode) {
426 if (degreeMode) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700427 mModeView.setText(R.string.mode_deg);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700428 mModeView.setContentDescription(getString(R.string.desc_mode_deg));
429
430 mModeToggle.setText(R.string.mode_rad);
431 mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700432 } else {
Justin Klaassend48b7562015-04-16 16:51:38 -0700433 mModeView.setText(R.string.mode_rad);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700434 mModeView.setContentDescription(getString(R.string.desc_mode_rad));
435
436 mModeToggle.setText(R.string.mode_deg);
437 mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
Hans Boehmbfe8c222015-04-02 16:26:07 -0700438 }
439 }
Hans Boehm84614952014-11-25 18:46:17 -0800440
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700441 // Add the given button id to input expression.
442 // If appropriate, clear the expression before doing so.
443 private void addKeyToExpr(int id) {
444 if (mCurrentState == CalculatorState.ERROR) {
445 setState(CalculatorState.INPUT);
446 } else if (mCurrentState == CalculatorState.RESULT) {
447 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
448 mEvaluator.collapse();
449 } else {
450 mEvaluator.clear();
451 }
452 setState(CalculatorState.INPUT);
453 }
454 if (!mEvaluator.append(id)) {
455 // TODO: Some user visible feedback?
456 }
457 }
458
459 private void redisplayAfterFormulaChange() {
460 // TODO: Could do this more incrementally.
461 redisplayFormula();
462 setState(CalculatorState.INPUT);
Hans Boehmc023b732015-04-29 11:30:47 -0700463 if (mEvaluator.getExpr().hasInterestingOps()) {
464 mEvaluator.evaluateAndShowResult();
465 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700466 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700467 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700468 }
469
Justin Klaassen4b3af052014-05-27 17:53:10 -0700470 public void onButtonClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700471 mCurrentButton = view;
Hans Boehm1176f232015-05-11 16:26:03 -0700472 stopActionMode();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700473
Hans Boehm84614952014-11-25 18:46:17 -0800474 // Always cancel in-progress evaluation.
475 // If we were waiting for the result, do nothing else.
476 mEvaluator.cancelAll();
Justin Klaassend48b7562015-04-16 16:51:38 -0700477
Hans Boehm84614952014-11-25 18:46:17 -0800478 if (mCurrentState == CalculatorState.EVALUATE
479 || mCurrentState == CalculatorState.ANIMATE) {
480 onCancelled();
481 return;
482 }
Justin Klaassend48b7562015-04-16 16:51:38 -0700483
Justin Klaassend48b7562015-04-16 16:51:38 -0700484 final int id = view.getId();
Hans Boehm84614952014-11-25 18:46:17 -0800485 switch (id) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700486 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700487 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700488 break;
489 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700490 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700491 break;
492 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700493 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700494 break;
Justin Klaassene2711cb2015-05-28 11:13:17 -0700495 case R.id.toggle_inv:
496 final boolean selected = !mInverseToggle.isSelected();
497 mInverseToggle.setSelected(selected);
498 onInverseToggled(selected);
499 break;
500 case R.id.toggle_mode:
501 final boolean mode = !mEvaluator.getDegreeMode();
Hans Boehmbfe8c222015-04-02 16:26:07 -0700502 if (mCurrentState == CalculatorState.RESULT) {
503 mEvaluator.collapse(); // Capture result evaluated in old mode
504 redisplayFormula();
505 }
506 // In input mode, we reinterpret already entered trig functions.
507 mEvaluator.setDegreeMode(mode);
Justin Klaassene2711cb2015-05-28 11:13:17 -0700508 onModeChanged(mode);
509
Hans Boehmbfe8c222015-04-02 16:26:07 -0700510 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700511 mResultText.clear();
Hans Boehmc023b732015-04-29 11:30:47 -0700512 if (mEvaluator.getExpr().hasInterestingOps()) {
513 mEvaluator.evaluateAndShowResult();
514 }
Hans Boehmbfe8c222015-04-02 16:26:07 -0700515 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700516 default:
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700517 addKeyToExpr(id);
518 redisplayAfterFormulaChange();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700519 break;
520 }
521 }
522
Hans Boehm84614952014-11-25 18:46:17 -0800523 void redisplayFormula() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700524 String formula = mEvaluator.getExpr().toString(this);
525 if (mUnprocessedChars != null) {
526 // Add and highlight characters we couldn't process.
527 SpannableString formatted = new SpannableString(formula + mUnprocessedChars);
528 // TODO: should probably match this to the error color.
529 formatted.setSpan(new ForegroundColorSpan(Color.RED),
530 formula.length(), formatted.length(),
531 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Hans Boehm08e8f322015-04-21 13:18:38 -0700532 mFormulaText.setText(formatted);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700533 } else {
Hans Boehm08e8f322015-04-21 13:18:38 -0700534 mFormulaText.setText(formula);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700535 }
Hans Boehm84614952014-11-25 18:46:17 -0800536 }
537
Justin Klaassen4b3af052014-05-27 17:53:10 -0700538 @Override
539 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700540 mCurrentButton = view;
541
Justin Klaassen4b3af052014-05-27 17:53:10 -0700542 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700543 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700544 return true;
545 }
546 return false;
547 }
548
Hans Boehm84614952014-11-25 18:46:17 -0800549 // Initial evaluation completed successfully. Initiate display.
Hans Boehma0e45f32015-05-30 13:20:35 -0700550 public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
551 String truncatedWholeNumber) {
Justin Klaassend48b7562015-04-16 16:51:38 -0700552 // Invalidate any options that may depend on the current result.
553 invalidateOptionsMenu();
554
Hans Boehma0e45f32015-05-30 13:20:35 -0700555 mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
Hans Boehm61568a12015-05-18 18:25:41 -0700556 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
Hans Boehm84614952014-11-25 18:46:17 -0800557 onResult(mCurrentState != CalculatorState.INIT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700558 }
Hans Boehm84614952014-11-25 18:46:17 -0800559 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700560
Hans Boehm84614952014-11-25 18:46:17 -0800561 public void onCancelled() {
562 // We should be in EVALUATE state.
563 // Display is still in input state.
564 setState(CalculatorState.INPUT);
Justin Klaassen44595162015-05-28 17:55:20 -0700565 mResultText.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800566 }
567
568 // Reevaluation completed; ask result to redisplay current value.
569 public void onReevaluate()
570 {
Justin Klaassen44595162015-05-28 17:55:20 -0700571 mResultText.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700572 }
573
Justin Klaassenfed941a2014-06-09 18:42:40 +0100574 @Override
575 public void onTextSizeChanged(final TextView textView, float oldSize) {
576 if (mCurrentState != CalculatorState.INPUT) {
577 // Only animate text changes that occur from user input.
578 return;
579 }
580
581 // Calculate the values needed to perform the scale and translation animations,
582 // maintaining the same apparent baseline for the displayed text.
583 final float textScale = oldSize / textView.getTextSize();
584 final float translationX = (1.0f - textScale) *
585 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
586 final float translationY = (1.0f - textScale) *
587 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
588
589 final AnimatorSet animatorSet = new AnimatorSet();
590 animatorSet.playTogether(
591 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
592 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
593 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
594 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700595 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100596 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
597 animatorSet.start();
598 }
599
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700600 private void onEquals() {
Hans Boehmc023b732015-04-29 11:30:47 -0700601 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700602 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800603 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700604 }
605 }
606
607 private void onDelete() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700608 // Delete works like backspace; remove the last character or operator from the expression.
609 // Note that we handle keyboard delete exactly like the delete button. For
610 // example the delete button can be used to delete a character from an incomplete
611 // function name typed on a physical keyboard.
Hans Boehm84614952014-11-25 18:46:17 -0800612 mEvaluator.cancelAll();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700613 // This should be impossible in RESULT state.
614 setState(CalculatorState.INPUT);
615 if (mUnprocessedChars != null) {
616 int len = mUnprocessedChars.length();
617 if (len > 0) {
618 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
619 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700620 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700621 }
622 } else {
Hans Boehmc023b732015-04-29 11:30:47 -0700623 mEvaluator.delete();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700624 }
625 redisplayAfterFormulaChange();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700626 }
627
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700628 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700629 final ViewGroupOverlay groupOverlay =
630 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700631
632 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700633 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700634
635 // Make reveal cover the display and status bar.
636 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700637 revealView.setBottom(displayRect.bottom);
638 revealView.setLeft(displayRect.left);
639 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700640 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700641 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700642
Justin Klaassen4b3af052014-05-27 17:53:10 -0700643 final int[] clearLocation = new int[2];
644 sourceView.getLocationInWindow(clearLocation);
645 clearLocation[0] += sourceView.getWidth() / 2;
646 clearLocation[1] += sourceView.getHeight() / 2;
647
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700648 final int revealCenterX = clearLocation[0] - revealView.getLeft();
649 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700650
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700651 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
652 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
653 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700654 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
655
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700656 final Animator revealAnimator =
657 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700658 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700659 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700660 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700661 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700662
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700663 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700664 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700665 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700666
667 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700668 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700669 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
670 animatorSet.addListener(new AnimatorListenerAdapter() {
671 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700672 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700673 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700674 mCurrentAnimator = null;
675 }
676 });
677
678 mCurrentAnimator = animatorSet;
679 animatorSet.start();
680 }
681
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700682 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800683 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700684 return;
685 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700686 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
687 @Override
688 public void onAnimationEnd(Animator animation) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700689 mUnprocessedChars = null;
Justin Klaassen44595162015-05-28 17:55:20 -0700690 mResultText.clear();
Hans Boehm760a9dc2015-04-20 10:27:12 -0700691 mEvaluator.clear();
692 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800693 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700694 }
695 });
696 }
697
Hans Boehm84614952014-11-25 18:46:17 -0800698 // Evaluation encountered en error. Display the error.
699 void onError(final int errorResourceId) {
Hans Boehmfbcef702015-04-27 18:07:47 -0700700 if (mCurrentState == CalculatorState.EVALUATE) {
701 setState(CalculatorState.ANIMATE);
702 reveal(mCurrentButton, R.color.calculator_error_color,
703 new AnimatorListenerAdapter() {
704 @Override
705 public void onAnimationEnd(Animator animation) {
706 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700707 mResultText.displayError(errorResourceId);
Hans Boehmfbcef702015-04-27 18:07:47 -0700708 }
709 });
710 } else if (mCurrentState == CalculatorState.INIT) {
711 setState(CalculatorState.ERROR);
Justin Klaassen44595162015-05-28 17:55:20 -0700712 mResultText.displayError(errorResourceId);
Hans Boehmc023b732015-04-29 11:30:47 -0700713 } else {
Justin Klaassen44595162015-05-28 17:55:20 -0700714 mResultText.clear();
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700715 }
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700716 }
717
Hans Boehm84614952014-11-25 18:46:17 -0800718
719 // Animate movement of result into the top formula slot.
720 // Result window now remains translated in the top slot while the result is displayed.
721 // (We convert it back to formula use only when the user provides new input.)
Justin Klaassen44595162015-05-28 17:55:20 -0700722 // Historical note: In the Lollipop version, this invisibly and instantaneously moved
Hans Boehm84614952014-11-25 18:46:17 -0800723 // formula and result displays back at the end of the animation. We no longer do that,
724 // so that we can continue to properly support scrolling of the result.
725 // We assume the result already contains the text to be expanded.
726 private void onResult(boolean animate) {
Justin Klaassen44595162015-05-28 17:55:20 -0700727 // Calculate the textSize that would be used to display the result in the formula.
728 // For scrollable results just use the minimum textSize to maximize the number of digits
729 // that are visible on screen.
730 float textSize = mFormulaText.getMinimumTextSize();
731 if (!mResultText.isScrollable()) {
732 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
733 }
734
735 // Scale the result to match the calculated textSize, minimizing the jump-cut transition
736 // when a result is reused in a subsequent expression.
737 final float resultScale = textSize / mResultText.getTextSize();
738
739 // Set the result's pivot to match its gravity.
740 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
741 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
742
743 // Calculate the necessary translations so the result takes the place of the formula and
744 // the formula moves off the top of the screen.
745 final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
746 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
Hans Boehm08e8f322015-04-21 13:18:38 -0700747 final float formulaTranslationY = -mFormulaText.getBottom();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700748
Justin Klaassen44595162015-05-28 17:55:20 -0700749 // Change the result's textColor to match the formula.
750 final int formulaTextColor = mFormulaText.getCurrentTextColor();
751
Hans Boehm84614952014-11-25 18:46:17 -0800752 if (animate) {
753 final AnimatorSet animatorSet = new AnimatorSet();
754 animatorSet.playTogether(
Justin Klaassen44595162015-05-28 17:55:20 -0700755 ObjectAnimator.ofPropertyValuesHolder(mResultText,
756 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
757 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
758 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
759 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
760 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
761 animatorSet.setDuration(getResources().getInteger(
762 android.R.integer.config_longAnimTime));
Hans Boehm84614952014-11-25 18:46:17 -0800763 animatorSet.addListener(new AnimatorListenerAdapter() {
764 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800765 public void onAnimationEnd(Animator animation) {
766 setState(CalculatorState.RESULT);
767 mCurrentAnimator = null;
768 }
769 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700770
Hans Boehm84614952014-11-25 18:46:17 -0800771 mCurrentAnimator = animatorSet;
772 animatorSet.start();
773 } else /* No animation desired; get there fast, e.g. when restarting */ {
Justin Klaassen44595162015-05-28 17:55:20 -0700774 mResultText.setScaleX(resultScale);
775 mResultText.setScaleY(resultScale);
776 mResultText.setTranslationY(resultTranslationY);
777 mResultText.setTextColor(formulaTextColor);
Hans Boehm08e8f322015-04-21 13:18:38 -0700778 mFormulaText.setTranslationY(formulaTranslationY);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700779 setState(CalculatorState.RESULT);
Hans Boehm84614952014-11-25 18:46:17 -0800780 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700781 }
Hans Boehm84614952014-11-25 18:46:17 -0800782
783 // Restore positions of the formula and result displays back to their original,
784 // pre-animation state.
785 private void restoreDisplayPositions() {
786 // Clear result.
Justin Klaassen44595162015-05-28 17:55:20 -0700787 mResultText.setText("");
Hans Boehm84614952014-11-25 18:46:17 -0800788 // Reset all of the values modified during the animation.
Justin Klaassen44595162015-05-28 17:55:20 -0700789 mResultText.setScaleX(1.0f);
790 mResultText.setScaleY(1.0f);
791 mResultText.setTranslationX(0.0f);
792 mResultText.setTranslationY(0.0f);
Hans Boehm08e8f322015-04-21 13:18:38 -0700793 mFormulaText.setTranslationY(0.0f);
Hans Boehm84614952014-11-25 18:46:17 -0800794
Hans Boehm08e8f322015-04-21 13:18:38 -0700795 mFormulaText.requestFocus();
Hans Boehm84614952014-11-25 18:46:17 -0800796 }
797
Justin Klaassend48b7562015-04-16 16:51:38 -0700798 @Override
799 public boolean onCreateOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700800 super.onCreateOptionsMenu(menu);
801
802 getMenuInflater().inflate(R.menu.activity_calculator, menu);
Justin Klaassend48b7562015-04-16 16:51:38 -0700803 return true;
804 }
805
806 @Override
807 public boolean onPrepareOptionsMenu(Menu menu) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700808 super.onPrepareOptionsMenu(menu);
809
810 // Show the leading option when displaying a result.
811 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
812
813 // Show the fraction option when displaying a rational result.
814 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
815 && mEvaluator.getRational() != null);
816
Justin Klaassend48b7562015-04-16 16:51:38 -0700817 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800818 }
819
820 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700821 public boolean onOptionsItemSelected(MenuItem item) {
Hans Boehm84614952014-11-25 18:46:17 -0800822 switch (item.getItemId()) {
Justin Klaassend36d63e2015-05-05 12:59:36 -0700823 case R.id.menu_leading:
824 displayFull();
Hans Boehm84614952014-11-25 18:46:17 -0800825 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700826 case R.id.menu_fraction:
827 displayFraction();
828 return true;
Justin Klaassend36d63e2015-05-05 12:59:36 -0700829 case R.id.menu_licenses:
830 startActivity(new Intent(this, Licenses.class));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700831 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800832 default:
833 return super.onOptionsItemSelected(item);
834 }
835 }
836
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700837 private void displayMessage(String s) {
Justin Klaassen44595162015-05-28 17:55:20 -0700838 new AlertDialog.Builder(this)
839 .setMessage(s)
840 .setNegativeButton(R.string.dismiss, null /* listener */)
841 .show();
Hans Boehm84614952014-11-25 18:46:17 -0800842 }
843
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700844 private void displayFraction() {
845 BoundedRational result = mEvaluator.getRational();
Hans Boehm013969e2015-04-13 20:29:47 -0700846 displayMessage(KeyMaps.translateResult(result.toNiceString()));
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700847 }
848
849 // Display full result to currently evaluated precision
850 private void displayFull() {
851 Resources res = getResources();
Justin Klaassen44595162015-05-28 17:55:20 -0700852 String msg = mResultText.getFullText() + " ";
853 if (mResultText.fullTextIsExact()) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700854 msg += res.getString(R.string.exact);
855 } else {
856 msg += res.getString(R.string.approximate);
857 }
858 displayMessage(msg);
859 }
860
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700861 // Add input characters to the end of the expression by mapping them to
862 // the appropriate button pushes when possible. Leftover characters
863 // are added to mUnprocessedChars, which is presumed to immediately
864 // precede the newly added characters.
865 private void addChars(String moreChars) {
866 if (mUnprocessedChars != null) {
867 moreChars = mUnprocessedChars + moreChars;
868 }
869 int current = 0;
870 int len = moreChars.length();
871 while (current < len) {
872 char c = moreChars.charAt(current);
Hans Boehm013969e2015-04-13 20:29:47 -0700873 int k = KeyMaps.keyForChar(c);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700874 if (k != View.NO_ID) {
875 mCurrentButton = findViewById(k);
876 addKeyToExpr(k);
877 if (Character.isSurrogate(c)) {
878 current += 2;
879 } else {
880 ++current;
881 }
882 continue;
883 }
Hans Boehm013969e2015-04-13 20:29:47 -0700884 int f = KeyMaps.funForString(moreChars, current);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700885 if (f != View.NO_ID) {
886 mCurrentButton = findViewById(f);
887 addKeyToExpr(f);
888 if (f == R.id.op_sqrt) {
889 // Square root entered as function; don't lose the parenthesis.
890 addKeyToExpr(R.id.lparen);
891 }
892 current = moreChars.indexOf('(', current) + 1;
893 continue;
894 }
895 // There are characters left, but we can't convert them to button presses.
896 mUnprocessedChars = moreChars.substring(current);
897 redisplayAfterFormulaChange();
898 return;
899 }
900 mUnprocessedChars = null;
901 redisplayAfterFormulaChange();
Hans Boehm84614952014-11-25 18:46:17 -0800902 }
903
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700904 @Override
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700905 public boolean onPaste(ClipData clip) {
906 final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
907 if (item == null) {
908 // nothing to paste, bail early...
909 return false;
910 }
911
912 // Check if the item is a previously copied result, otherwise paste as raw text.
913 final Uri uri = item.getUri();
914 if (uri != null && mEvaluator.isLastSaved(uri)) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700915 if (mCurrentState == CalculatorState.ERROR
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700916 || mCurrentState == CalculatorState.RESULT) {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700917 setState(CalculatorState.INPUT);
918 mEvaluator.clear();
Hans Boehm84614952014-11-25 18:46:17 -0800919 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700920 mEvaluator.addSaved();
921 redisplayAfterFormulaChange();
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700922 } else {
923 addChars(item.coerceToText(this).toString());
Hans Boehm84614952014-11-25 18:46:17 -0800924 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700925 return true;
Hans Boehm84614952014-11-25 18:46:17 -0800926 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700927}