blob: 2730e733a19789a74a49043de76cc7006c8af932 [file] [log] [blame]
Justin Klaassen4b3af052014-05-27 17:53:10 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
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 Boehm84614952014-11-25 18:46:17 -080017// TODO: Fix evaluation interface so the evaluator returns entire
18// result, and display can properly handle variable width font.
19// TODO: Fix font handling and scaling in result display.
20// TODO: Fix placement of inverse trig buttons.
21// TODO: Add Degree/Radian switch and display.
22// TODO: Handle physical keyboard correctly.
23// TODO: Fix internationalization, including result.
24// TODO: Check and fix accessability issues.
25// TODO: Support pasting of at least full result. (Rounding?)
26// TODO: Copy/paste in formula.
27
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;
34import android.animation.ArgbEvaluator;
35import android.animation.ObjectAnimator;
36import android.animation.ValueAnimator;
37import android.animation.ValueAnimator.AnimatorUpdateListener;
38import android.app.Activity;
Hans Boehm84614952014-11-25 18:46:17 -080039import android.app.AlertDialog;
40import android.content.Context;
41import android.content.DialogInterface;
Hans Boehmbfe8c222015-04-02 16:26:07 -070042import android.content.res.Resources;
Justin Klaassen8fff1442014-06-19 10:43:29 -070043import android.graphics.Rect;
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;
Justin Klaassen4b3af052014-05-27 17:53:10 -070047import android.text.Editable;
48import android.text.TextUtils;
49import android.text.TextWatcher;
Hans Boehm84614952014-11-25 18:46:17 -080050import android.util.Log;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070051import android.view.KeyEvent;
Hans Boehm84614952014-11-25 18:46:17 -080052import android.view.Menu;
53import android.view.MenuItem;
Justin Klaassen4b3af052014-05-27 17:53:10 -070054import android.view.View;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -070055import android.view.View.OnKeyListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070056import android.view.View.OnLongClickListener;
Justin Klaassen5f2a3342014-06-11 17:40:22 -070057import android.view.ViewAnimationUtils;
Justin Klaassen8fff1442014-06-19 10:43:29 -070058import android.view.ViewGroupOverlay;
Justin Klaassen4b3af052014-05-27 17:53:10 -070059import android.view.animation.AccelerateDecelerateInterpolator;
Hans Boehm84614952014-11-25 18:46:17 -080060import android.webkit.WebView;
Justin Klaassen4b3af052014-05-27 17:53:10 -070061import android.widget.Button;
Hans Boehm84614952014-11-25 18:46:17 -080062import android.widget.PopupMenu;
63import android.widget.PopupMenu.OnMenuItemClickListener;
Justin Klaassenfed941a2014-06-09 18:42:40 +010064import android.widget.TextView;
65
66import com.android.calculator2.CalculatorEditText.OnTextSizeChangeListener;
Hans Boehm84614952014-11-25 18:46:17 -080067
68import java.io.ByteArrayInputStream;
69import java.io.ObjectInputStream;
70import java.io.ByteArrayOutputStream;
71import java.io.ObjectOutputStream;
72import java.io.ObjectInput;
73import java.io.ObjectOutput;
74import java.io.IOException;
75import java.text.DecimalFormatSymbols; // TODO: May eventually not need this here.
Justin Klaassen4b3af052014-05-27 17:53:10 -070076
Justin Klaassen04f79c72014-06-27 17:25:35 -070077public class Calculator extends Activity
Hans Boehm84614952014-11-25 18:46:17 -080078 implements OnTextSizeChangeListener, OnLongClickListener, OnMenuItemClickListener {
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
113 private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
114 @Override
115 public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
116 }
117
118 @Override
119 public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
120 }
121
122 @Override
123 public void afterTextChanged(Editable editable) {
124 setState(CalculatorState.INPUT);
Hans Boehm84614952014-11-25 18:46:17 -0800125 mEvaluator.evaluateAndShowResult();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700126 }
127 };
128
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700129 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
130 @Override
131 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
132 switch (keyCode) {
133 case KeyEvent.KEYCODE_NUMPAD_ENTER:
134 case KeyEvent.KEYCODE_ENTER:
135 if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
136 mCurrentButton = mEqualButton;
137 onEquals();
138 }
139 // ignore all other actions
140 return true;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700141 }
142 return false;
143 }
144 };
145
Hans Boehm84614952014-11-25 18:46:17 -0800146 private static final String NAME = Calculator.class.getName();
147 private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
148 private static final String KEY_EVAL_STATE = NAME + "_eval_state";
149 // Associated value is a byte array holding both mCalculatorState
150 // and the (much more complex) evaluator state.
Justin Klaassen741471e2014-06-11 09:43:44 -0700151
Justin Klaassen4b3af052014-05-27 17:53:10 -0700152 private CalculatorState mCurrentState;
Hans Boehm84614952014-11-25 18:46:17 -0800153 private Evaluator mEvaluator;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700154
Justin Klaassen06360f92014-08-28 11:08:44 -0700155 private View mDisplayView;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700156 private CalculatorEditText mFormulaEditText;
Hans Boehm84614952014-11-25 18:46:17 -0800157 private CalculatorResult mResult;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700158 private TextView mDegRadDisplay;
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100159 private ViewPager mPadViewPager;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700160 private View mDeleteButton;
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700161 private View mEqualButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700162 private View mClearButton;
Hans Boehm84614952014-11-25 18:46:17 -0800163 private View mOverflowMenuButton;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700164 private Button mDegRadButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700165
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700166 private View mCurrentButton;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700167 private Animator mCurrentAnimator;
168
169 @Override
170 protected void onCreate(Bundle savedInstanceState) {
171 super.onCreate(savedInstanceState);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700172 setContentView(R.layout.activity_calculator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700173
Justin Klaassen06360f92014-08-28 11:08:44 -0700174 mDisplayView = findViewById(R.id.display);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700175 mFormulaEditText = (CalculatorEditText) findViewById(R.id.formula);
Hans Boehm84614952014-11-25 18:46:17 -0800176 mResult = (CalculatorResult) findViewById(R.id.result);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700177 mDegRadDisplay = (TextView) findViewById(R.id.deg_rad);
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100178 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700179 mDeleteButton = findViewById(R.id.del);
180 mClearButton = findViewById(R.id.clr);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700181 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
182 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
183 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
184 }
Hans Boehm84614952014-11-25 18:46:17 -0800185 mOverflowMenuButton = findViewById(R.id.overflow_menu);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700186 mDegRadButton = (Button)findViewById(R.id.mode_deg_rad);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700187
Hans Boehm84614952014-11-25 18:46:17 -0800188 mEvaluator = new Evaluator(this, mResult);
189 mResult.setEvaluator(mEvaluator);
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700190
Hans Boehm84614952014-11-25 18:46:17 -0800191 if (savedInstanceState != null) {
192 setState(CalculatorState.values()[
193 savedInstanceState.getInt(KEY_DISPLAY_STATE,
194 CalculatorState.INPUT.ordinal())]);
195 byte[] state =
196 savedInstanceState.getByteArray(KEY_EVAL_STATE);
197 if (state != null) {
198 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
199 mEvaluator.restoreInstanceState(in);
200 } catch (Throwable ignored) {
201 // When in doubt, revert to clean state
202 mCurrentState = CalculatorState.INPUT;
203 mEvaluator.clear();
204 }
205 }
206 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700207 mFormulaEditText.addTextChangedListener(mFormulaTextWatcher);
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700208 mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener);
Justin Klaassenfed941a2014-06-09 18:42:40 +0100209 mFormulaEditText.setOnTextSizeChangeListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700210 mDeleteButton.setOnLongClickListener(this);
Hans Boehmbfe8c222015-04-02 16:26:07 -0700211 updateDegreeMode(mEvaluator.getDegreeMode());
Hans Boehm84614952014-11-25 18:46:17 -0800212 if (mCurrentState == CalculatorState.EVALUATE) {
213 // Odd case. Evaluation probably took a long time. Let user ask for it again
214 mCurrentState = CalculatorState.INPUT;
215 // TODO: This can happen if the user rotates the screen.
216 // Is this rotate-to-abort behavior correct? Revisit after experimentation.
217 }
218 if (mCurrentState != CalculatorState.INPUT) {
219 setState(CalculatorState.INIT);
220 mEvaluator.evaluateAndShowResult();
221 mEvaluator.requireResult();
222 } else {
223 redisplayFormula();
224 mEvaluator.evaluateAndShowResult();
225 }
226 // TODO: We're currently not saving and restoring scroll position.
227 // We probably should. Details may require care to deal with:
228 // - new display size
229 // - slow recomputation if we've scrolled far.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700230 }
231
232 @Override
Justin Klaassenf79d6f62014-08-26 12:27:08 -0700233 protected void onSaveInstanceState(@NonNull Bundle outState) {
234 // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
235 if (mCurrentAnimator != null) {
236 mCurrentAnimator.cancel();
237 }
238
Justin Klaassen4b3af052014-05-27 17:53:10 -0700239 super.onSaveInstanceState(outState);
Hans Boehm84614952014-11-25 18:46:17 -0800240 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
241 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
242 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
243 mEvaluator.saveInstanceState(out);
244 } catch (IOException e) {
245 // Impossible; No IO involved.
246 throw new AssertionError("Impossible IO exception", e);
247 }
248 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700249 }
250
251 private void setState(CalculatorState state) {
252 if (mCurrentState != state) {
Hans Boehm84614952014-11-25 18:46:17 -0800253 if (state == CalculatorState.INPUT) {
254 restoreDisplayPositions();
255 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700256 mCurrentState = state;
257
Hans Boehm84614952014-11-25 18:46:17 -0800258 if (mCurrentState == CalculatorState.RESULT
259 || mCurrentState == CalculatorState.ERROR) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700260 mDeleteButton.setVisibility(View.GONE);
261 mClearButton.setVisibility(View.VISIBLE);
262 } else {
263 mDeleteButton.setVisibility(View.VISIBLE);
264 mClearButton.setVisibility(View.GONE);
265 }
266
Hans Boehm84614952014-11-25 18:46:17 -0800267 if (mCurrentState == CalculatorState.ERROR) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700268 final int errorColor = getResources().getColor(R.color.calculator_error_color);
269 mFormulaEditText.setTextColor(errorColor);
Hans Boehm84614952014-11-25 18:46:17 -0800270 mResult.setTextColor(errorColor);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700271 getWindow().setStatusBarColor(errorColor);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700272 } else {
273 mFormulaEditText.setTextColor(
274 getResources().getColor(R.color.display_formula_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800275 mResult.setTextColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700276 getResources().getColor(R.color.display_result_text_color));
Justin Klaassen8fff1442014-06-19 10:43:29 -0700277 getWindow().setStatusBarColor(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700278 getResources().getColor(R.color.calculator_accent_color));
279 }
280 }
281 }
282
283 @Override
Justin Klaassen3b4d13d2014-06-06 18:18:37 +0100284 public void onBackPressed() {
285 if (mPadViewPager == null || mPadViewPager.getCurrentItem() == 0) {
286 // If the user is currently looking at the first pad (or the pad is not paged),
287 // allow the system to handle the Back button.
288 super.onBackPressed();
289 } else {
290 // Otherwise, select the previous pad.
291 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
292 }
293 }
294
295 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700296 public void onUserInteraction() {
297 super.onUserInteraction();
298
299 // If there's an animation in progress, cancel it so the user interaction can be handled
300 // immediately.
301 if (mCurrentAnimator != null) {
302 mCurrentAnimator.cancel();
303 }
304 }
305
Hans Boehmbfe8c222015-04-02 16:26:07 -0700306 // Update the top corner degree/radian display and mode button
307 // to reflect the indicated current degree mode (true = degrees)
308 // TODO: Hide the top corner display until the advanced panel is exposed.
309 private void updateDegreeMode(boolean dm) {
310 Resources res = getResources();
311 String descr;
312 if (dm) {
313 mDegRadDisplay.setText(R.string.mode_deg);
314 mDegRadButton.setText(R.string.mode_rad);
315 mDegRadButton.setContentDescription(res.getString(R.string.desc_mode_rad));
316 } else {
317 mDegRadDisplay.setText(R.string.mode_rad);
318 mDegRadButton.setText(R.string.mode_deg);
319 mDegRadButton.setContentDescription(res.getString(R.string.desc_mode_deg));
320 }
321 }
Hans Boehm84614952014-11-25 18:46:17 -0800322
Justin Klaassen4b3af052014-05-27 17:53:10 -0700323 public void onButtonClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700324 mCurrentButton = view;
Hans Boehm84614952014-11-25 18:46:17 -0800325 int id = view.getId();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700326
Hans Boehm84614952014-11-25 18:46:17 -0800327 // Always cancel in-progress evaluation.
328 // If we were waiting for the result, do nothing else.
329 mEvaluator.cancelAll();
330 if (mCurrentState == CalculatorState.EVALUATE
331 || mCurrentState == CalculatorState.ANIMATE) {
332 onCancelled();
333 return;
334 }
335 switch (id) {
336 case R.id.overflow_menu:
337 PopupMenu menu = constructPopupMenu();
338 if (menu != null) {
339 menu.show();
340 }
341 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700342 case R.id.eq:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700343 onEquals();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700344 break;
345 case R.id.del:
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700346 onDelete();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700347 break;
348 case R.id.clr:
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700349 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700350 break;
Hans Boehmbfe8c222015-04-02 16:26:07 -0700351 case R.id.mode_deg_rad:
352 boolean mode = !mEvaluator.getDegreeMode();
353 updateDegreeMode(mode);
354 if (mCurrentState == CalculatorState.RESULT) {
355 mEvaluator.collapse(); // Capture result evaluated in old mode
356 redisplayFormula();
357 }
358 // In input mode, we reinterpret already entered trig functions.
359 mEvaluator.setDegreeMode(mode);
360 setState(CalculatorState.INPUT);
361 mResult.clear();
362 mEvaluator.evaluateAndShowResult();
363 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700364 default:
Hans Boehm84614952014-11-25 18:46:17 -0800365 if (mCurrentState == CalculatorState.ERROR) {
366 setState(CalculatorState.INPUT);
367 }
368 if (mCurrentState == CalculatorState.RESULT) {
369 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
370 mEvaluator.collapse();
371 } else {
372 mEvaluator.clear();
373 }
374 }
375 if (!mEvaluator.append(id)) {
376 // TODO: Some user visible feedback?
377 }
378 // TODO: Could do this more incrementally.
379 redisplayFormula();
380 setState(CalculatorState.INPUT);
381 mResult.clear();
382 mEvaluator.evaluateAndShowResult();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700383 break;
384 }
385 }
386
Hans Boehm84614952014-11-25 18:46:17 -0800387 void redisplayFormula() {
388 mFormulaEditText.setText(mEvaluator.getExpr().toString(this));
389 }
390
Justin Klaassen4b3af052014-05-27 17:53:10 -0700391 @Override
392 public boolean onLongClick(View view) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700393 mCurrentButton = view;
394
Justin Klaassen4b3af052014-05-27 17:53:10 -0700395 if (view.getId() == R.id.del) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700396 onClear();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700397 return true;
398 }
399 return false;
400 }
401
Hans Boehm84614952014-11-25 18:46:17 -0800402 // Initial evaluation completed successfully. Initiate display.
403 public void onEvaluate(int initDisplayPrec, String truncatedWholeNumber) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700404 if (mCurrentState == CalculatorState.INPUT) {
Hans Boehm84614952014-11-25 18:46:17 -0800405 // Just update small result display.
406 mResult.displayResult(initDisplayPrec, truncatedWholeNumber);
407 } else { // in EVALUATE or INIT state
408 mResult.displayResult(initDisplayPrec, truncatedWholeNumber);
409 onResult(mCurrentState != CalculatorState.INIT);
410 setState(CalculatorState.RESULT);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700411 }
Hans Boehm84614952014-11-25 18:46:17 -0800412 }
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700413
Hans Boehm84614952014-11-25 18:46:17 -0800414 public void onCancelled() {
415 // We should be in EVALUATE state.
416 // Display is still in input state.
417 setState(CalculatorState.INPUT);
418 }
419
420 // Reevaluation completed; ask result to redisplay current value.
421 public void onReevaluate()
422 {
423 mResult.redisplay();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700424 }
425
Justin Klaassenfed941a2014-06-09 18:42:40 +0100426 @Override
427 public void onTextSizeChanged(final TextView textView, float oldSize) {
428 if (mCurrentState != CalculatorState.INPUT) {
429 // Only animate text changes that occur from user input.
430 return;
431 }
432
433 // Calculate the values needed to perform the scale and translation animations,
434 // maintaining the same apparent baseline for the displayed text.
435 final float textScale = oldSize / textView.getTextSize();
436 final float translationX = (1.0f - textScale) *
437 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
438 final float translationY = (1.0f - textScale) *
439 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
440
441 final AnimatorSet animatorSet = new AnimatorSet();
442 animatorSet.playTogether(
443 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
444 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
445 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
446 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
Justin Klaassen94db7202014-06-11 11:22:31 -0700447 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassenfed941a2014-06-09 18:42:40 +0100448 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
449 animatorSet.start();
450 }
451
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700452 private void onEquals() {
453 if (mCurrentState == CalculatorState.INPUT) {
454 setState(CalculatorState.EVALUATE);
Hans Boehm84614952014-11-25 18:46:17 -0800455 mEvaluator.requireResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700456 }
457 }
458
459 private void onDelete() {
460 // Delete works like backspace; remove the last character from the expression.
Hans Boehm84614952014-11-25 18:46:17 -0800461 mEvaluator.cancelAll();
462 mEvaluator.getExpr().delete();
463 redisplayFormula();
464 mResult.clear();
465 mEvaluator.evaluateAndShowResult();
Budi Kusmiantoroad8e88a2014-08-11 17:21:09 -0700466 }
467
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700468 private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
Justin Klaassen06360f92014-08-28 11:08:44 -0700469 final ViewGroupOverlay groupOverlay =
470 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
Justin Klaassen8fff1442014-06-19 10:43:29 -0700471
472 final Rect displayRect = new Rect();
Justin Klaassen06360f92014-08-28 11:08:44 -0700473 mDisplayView.getGlobalVisibleRect(displayRect);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700474
475 // Make reveal cover the display and status bar.
476 final View revealView = new View(this);
Justin Klaassen8fff1442014-06-19 10:43:29 -0700477 revealView.setBottom(displayRect.bottom);
478 revealView.setLeft(displayRect.left);
479 revealView.setRight(displayRect.right);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700480 revealView.setBackgroundColor(getResources().getColor(colorRes));
Justin Klaassen06360f92014-08-28 11:08:44 -0700481 groupOverlay.add(revealView);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700482
Justin Klaassen4b3af052014-05-27 17:53:10 -0700483 final int[] clearLocation = new int[2];
484 sourceView.getLocationInWindow(clearLocation);
485 clearLocation[0] += sourceView.getWidth() / 2;
486 clearLocation[1] += sourceView.getHeight() / 2;
487
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700488 final int revealCenterX = clearLocation[0] - revealView.getLeft();
489 final int revealCenterY = clearLocation[1] - revealView.getTop();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700490
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700491 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
492 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
493 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700494 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
495
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700496 final Animator revealAnimator =
497 ViewAnimationUtils.createCircularReveal(revealView,
ztenghui3d6ecaf2014-06-05 09:56:00 -0700498 revealCenterX, revealCenterY, 0.0f, revealRadius);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700499 revealAnimator.setDuration(
Justin Klaassen4b3af052014-05-27 17:53:10 -0700500 getResources().getInteger(android.R.integer.config_longAnimTime));
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700501 revealAnimator.addListener(listener);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700502
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700503 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700504 alphaAnimator.setDuration(
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700505 getResources().getInteger(android.R.integer.config_mediumAnimTime));
Justin Klaassen4b3af052014-05-27 17:53:10 -0700506
507 final AnimatorSet animatorSet = new AnimatorSet();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700508 animatorSet.play(revealAnimator).before(alphaAnimator);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700509 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
510 animatorSet.addListener(new AnimatorListenerAdapter() {
511 @Override
Justin Klaassen4b3af052014-05-27 17:53:10 -0700512 public void onAnimationEnd(Animator animator) {
Justin Klaassen8fff1442014-06-19 10:43:29 -0700513 groupOverlay.remove(revealView);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700514 mCurrentAnimator = null;
515 }
516 });
517
518 mCurrentAnimator = animatorSet;
519 animatorSet.start();
520 }
521
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700522 private void onClear() {
Hans Boehm84614952014-11-25 18:46:17 -0800523 if (mEvaluator.getExpr().isEmpty()) {
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700524 return;
525 }
Hans Boehm84614952014-11-25 18:46:17 -0800526 mResult.clear();
527 mEvaluator.clear();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700528 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
529 @Override
530 public void onAnimationEnd(Animator animation) {
Hans Boehm84614952014-11-25 18:46:17 -0800531 redisplayFormula();
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700532 }
533 });
534 }
535
Hans Boehm84614952014-11-25 18:46:17 -0800536 // Evaluation encountered en error. Display the error.
537 void onError(final int errorResourceId) {
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700538 if (mCurrentState != CalculatorState.EVALUATE) {
539 // Only animate error on evaluate.
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700540 return;
541 }
542
Hans Boehm84614952014-11-25 18:46:17 -0800543 setState(CalculatorState.ANIMATE);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700544 reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() {
545 @Override
546 public void onAnimationEnd(Animator animation) {
547 setState(CalculatorState.ERROR);
Hans Boehm84614952014-11-25 18:46:17 -0800548 mResult.displayError(errorResourceId);
Justin Klaassen5f2a3342014-06-11 17:40:22 -0700549 }
550 });
551 }
552
Hans Boehm84614952014-11-25 18:46:17 -0800553
554 // Animate movement of result into the top formula slot.
555 // Result window now remains translated in the top slot while the result is displayed.
556 // (We convert it back to formula use only when the user provides new input.)
557 // Historical note: In the Lollipop version, this invisibly and instantaeously moved
558 // formula and result displays back at the end of the animation. We no longer do that,
559 // so that we can continue to properly support scrolling of the result.
560 // We assume the result already contains the text to be expanded.
561 private void onResult(boolean animate) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700562 // Calculate the values needed to perform the scale and translation animations,
563 // accounting for how the scale will affect the final position of the text.
Hans Boehm84614952014-11-25 18:46:17 -0800564 // We want to fix the character size in the display to avoid weird effects
565 // when we scroll.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700566 final float resultScale =
Hans Boehm84614952014-11-25 18:46:17 -0800567 mFormulaEditText.getVariableTextSize(mResult.getText().toString())
568 / mResult.getTextSize() - 0.1f;
569 // FIXME: This doesn't work correctly. The -0.1 is a fudge factor to
570 // improve things slightly. Remove when fixed.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700571 final float resultTranslationX = (1.0f - resultScale) *
Hans Boehm84614952014-11-25 18:46:17 -0800572 (mResult.getWidth() / 2.0f - mResult.getPaddingEnd());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700573 final float resultTranslationY = (1.0f - resultScale) *
Hans Boehm84614952014-11-25 18:46:17 -0800574 (mResult.getHeight() / 2.0f - mResult.getPaddingBottom()) +
575 (mFormulaEditText.getBottom() - mResult.getBottom()) +
576 (mResult.getPaddingBottom() - mFormulaEditText.getPaddingBottom());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700577 final float formulaTranslationY = -mFormulaEditText.getBottom();
578
Hans Boehm84614952014-11-25 18:46:17 -0800579 // TODO: Reintroduce textColorAnimator?
580 // The initial and final colors seemed to be the same in L.
581 // With the new model, the result logically changes back to a formula
582 // only when we switch back to INPUT state, so it's unclear that animating
583 // a color change here makes sense.
584 if (animate) {
585 final AnimatorSet animatorSet = new AnimatorSet();
586 animatorSet.playTogether(
587 ObjectAnimator.ofFloat(mResult, View.SCALE_X, resultScale),
588 ObjectAnimator.ofFloat(mResult, View.SCALE_Y, resultScale),
589 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_X, resultTranslationX),
590 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_Y, resultTranslationY),
591 ObjectAnimator.ofFloat(mFormulaEditText, View.TRANSLATION_Y,
592 formulaTranslationY));
593 animatorSet.setDuration(
594 getResources().getInteger(android.R.integer.config_longAnimTime));
595 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
596 animatorSet.addListener(new AnimatorListenerAdapter() {
597 @Override
598 public void onAnimationStart(Animator animation) {
599 // Result should already be displayed; no need to do anything.
600 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700601
Hans Boehm84614952014-11-25 18:46:17 -0800602 @Override
603 public void onAnimationEnd(Animator animation) {
604 setState(CalculatorState.RESULT);
605 mCurrentAnimator = null;
606 }
607 });
Justin Klaassen4b3af052014-05-27 17:53:10 -0700608
Hans Boehm84614952014-11-25 18:46:17 -0800609 mCurrentAnimator = animatorSet;
610 animatorSet.start();
611 } else /* No animation desired; get there fast, e.g. when restarting */ {
612 mResult.setScaleX(resultScale);
613 mResult.setScaleY(resultScale);
614 mResult.setTranslationX(resultTranslationX);
615 mResult.setTranslationY(resultTranslationY);
616 mFormulaEditText.setTranslationY(formulaTranslationY);
617 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700618 }
Hans Boehm84614952014-11-25 18:46:17 -0800619
620 // Restore positions of the formula and result displays back to their original,
621 // pre-animation state.
622 private void restoreDisplayPositions() {
623 // Clear result.
624 mResult.setText("");
625 // Reset all of the values modified during the animation.
626 mResult.setScaleX(1.0f);
627 mResult.setScaleY(1.0f);
628 mResult.setTranslationX(0.0f);
629 mResult.setTranslationY(0.0f);
630 mFormulaEditText.setTranslationY(0.0f);
631
632 mFormulaEditText.requestFocus();
633 }
634
635 // Overflow menu handling.
636 private PopupMenu constructPopupMenu() {
637 final PopupMenu popupMenu = new PopupMenu(this, mOverflowMenuButton);
638 mOverflowMenuButton.setOnTouchListener(popupMenu.getDragToOpenListener());
639 final Menu menu = popupMenu.getMenu();
640 popupMenu.inflate(R.menu.menu);
641 popupMenu.setOnMenuItemClickListener(this);
642 onPrepareOptionsMenu(menu);
643 return popupMenu;
644 }
645
646 @Override
647 public boolean onMenuItemClick(MenuItem item) {
648 switch (item.getItemId()) {
649 case R.id.menu_help:
650 displayHelpMessage();
651 return true;
652 case R.id.menu_about:
653 displayAboutPage();
654 return true;
655 default:
656 return super.onOptionsItemSelected(item);
657 }
658 }
659
660 private void displayHelpMessage() {
661 AlertDialog.Builder builder = new AlertDialog.Builder(this);
662 if (mPadViewPager != null) {
663 builder.setMessage(getResources().getString(R.string.help_message)
664 + getResources().getString(R.string.help_pager));
665 } else {
666 builder.setMessage(R.string.help_message);
667 }
668 builder.setNegativeButton(R.string.dismiss,
669 new DialogInterface.OnClickListener() {
670 public void onClick(DialogInterface d, int which) { }
671 })
672 .show();
673 }
674
675 private void displayAboutPage() {
676 WebView wv = new WebView(this);
677 wv.loadUrl("file:///android_asset/about.txt");
678 new AlertDialog.Builder(this)
679 .setView(wv)
680 .setNegativeButton(R.string.dismiss,
681 new DialogInterface.OnClickListener() {
682 public void onClick(DialogInterface d, int which) { }
683 })
684 .show();
685 }
686
687 // TODO: Probably delete the following method and all of its callers before release.
688 // Definitely delete most of its callers.
689 private static final String LOG_TAG = "Calculator";
690
691 static void log(String message) {
692 Log.v(LOG_TAG, message);
693 }
694
695 // EVERYTHING BELOW HERE was preserved from the KitKat version of the
696 // calculator, since we expect to need it again once functionality is a bit more
697 // more complete. But it has not yet been wired in correctly, and
698 // IS CURRENTLY UNUSED.
699
700 // Is s a valid constant?
701 // TODO: Possibly generalize to scientific notation, hexadecimal, etc.
702 static boolean isConstant(CharSequence s) {
703 boolean sawDecimal = false;
704 boolean sawDigit = false;
705 final char decimalPt = DecimalFormatSymbols.getInstance().getDecimalSeparator();
706 int len = s.length();
707 int i = 0;
708 while (i < len && Character.isWhitespace(s.charAt(i))) ++i;
709 if (i < len && s.charAt(i) == '-') ++i;
710 for (; i < len; ++i) {
711 char c = s.charAt(i);
712 if (c == '.' || c == decimalPt) {
713 if (sawDecimal) return false;
714 sawDecimal = true;
715 } else if (Character.isDigit(c)) {
716 sawDigit = true;
717 } else {
718 break;
719 }
720 }
721 while (i < len && Character.isWhitespace(s.charAt(i))) ++i;
722 return i == len && sawDigit;
723 }
724
725 // Paste a valid character sequence representing a constant.
726 void paste(CharSequence s) {
727 mEvaluator.cancelAll();
728 if (mCurrentState == CalculatorState.RESULT) {
729 mEvaluator.clear();
730 }
731 int len = s.length();
732 for (int i = 0; i < len; ++i) {
733 char c = s.charAt(i);
734 if (!Character.isWhitespace(c)) {
735 mEvaluator.append(KeyMaps.keyForChar(c));
736 }
737 }
738 }
739
Justin Klaassen4b3af052014-05-27 17:53:10 -0700740}