Calculator.java revision 06c4944949254d1d34d6b6dde44f289c332156da
1/* 2 * Copyright (C) 2015 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 17// 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. 20// TODO: Better indication of when the result is known to be exact. 21// TODO: Check and possibly fix accessability issues. 22// 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. 30 31package com.android.calculator2; 32 33import android.animation.Animator; 34import android.animation.Animator.AnimatorListener; 35import android.animation.AnimatorListenerAdapter; 36import android.animation.AnimatorSet; 37import android.animation.ObjectAnimator; 38import android.animation.PropertyValuesHolder; 39import android.app.Activity; 40import android.app.AlertDialog; 41import android.content.DialogInterface; 42import android.content.Intent; 43import android.content.res.Resources; 44import android.graphics.Color; 45import android.graphics.Rect; 46import android.net.Uri; 47import android.os.Bundle; 48import android.support.annotation.NonNull; 49import android.support.v4.view.ViewPager; 50import android.text.SpannableString; 51import android.text.Spanned; 52import android.text.style.ForegroundColorSpan; 53import android.util.Property; 54import android.view.KeyCharacterMap; 55import android.view.KeyEvent; 56import android.view.Menu; 57import android.view.MenuItem; 58import android.view.View; 59import android.view.View.OnKeyListener; 60import android.view.View.OnLongClickListener; 61import android.view.ViewAnimationUtils; 62import android.view.ViewGroupOverlay; 63import android.view.animation.AccelerateDecelerateInterpolator; 64import android.widget.TextView; 65import android.widget.Toolbar; 66 67import com.android.calculator2.CalculatorText.OnTextSizeChangeListener; 68 69import java.io.ByteArrayInputStream; 70import java.io.ByteArrayOutputStream; 71import java.io.IOException; 72import java.io.ObjectInput; 73import java.io.ObjectInputStream; 74import java.io.ObjectOutput; 75import java.io.ObjectOutputStream; 76 77public class Calculator extends Activity 78 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.PasteListener { 79 80 /** 81 * Constant for an invalid resource id. 82 */ 83 public static final int INVALID_RES_ID = -1; 84 85 private enum CalculatorState { 86 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. 97 } 98 // 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. 112 113 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 126 // 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. 128 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() { 129 @Override 130 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 131 stopActionMode(); 132 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 } 145 switch (keyCode) { 146 case KeyEvent.KEYCODE_NUMPAD_ENTER: 147 case KeyEvent.KEYCODE_ENTER: 148 case KeyEvent.KEYCODE_DPAD_CENTER: 149 mCurrentButton = mEqualButton; 150 onEquals(); 151 return true; 152 case KeyEvent.KEYCODE_DEL: 153 mCurrentButton = mDeleteButton; 154 onDelete(); 155 return true; 156 default: 157 final int raw = keyEvent.getKeyCharacterMap() 158 .get(keyCode, keyEvent.getMetaState()); 159 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) 165 || Character.isWhitespace(raw)) { 166 return true; 167 } 168 char c = (char) raw; 169 if (c == '=') { 170 mCurrentButton = mEqualButton; 171 onEquals(); 172 } else { 173 addChars(String.valueOf(c)); 174 redisplayAfterFormulaChange(); 175 } 176 } 177 return false; 178 } 179 }; 180 181 private static final String NAME = Calculator.class.getName(); 182 private static final String KEY_DISPLAY_STATE = NAME + "_display_state"; 183 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars"; 184 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. 187 188 private CalculatorState mCurrentState; 189 private Evaluator mEvaluator; 190 191 private View mDisplayView; 192 private TextView mModeView; 193 private CalculatorText mFormulaText; 194 private CalculatorResult mResultText; 195 196 private ViewPager mPadViewPager; 197 private View mDeleteButton; 198 private View mClearButton; 199 private View mEqualButton; 200 201 private TextView mInverseToggle; 202 private TextView mModeToggle; 203 204 private View[] mInvertibleButtons; 205 private View[] mInverseButtons; 206 207 private View mCurrentButton; 208 private Animator mCurrentAnimator; 209 210 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 214 @Override 215 protected void onCreate(Bundle savedInstanceState) { 216 super.onCreate(savedInstanceState); 217 setContentView(R.layout.activity_calculator); 218 setActionBar((Toolbar) findViewById(R.id.toolbar)); 219 220 // Hide all default options in the ActionBar. 221 getActionBar().setDisplayOptions(0); 222 223 mDisplayView = findViewById(R.id.display); 224 mModeView = (TextView) findViewById(R.id.mode); 225 mFormulaText = (CalculatorText) findViewById(R.id.formula); 226 mResultText = (CalculatorResult) findViewById(R.id.result); 227 228 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager); 229 mDeleteButton = findViewById(R.id.del); 230 mClearButton = findViewById(R.id.clr); 231 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 } 235 236 mInverseToggle = (TextView) findViewById(R.id.toggle_inv); 237 mModeToggle = (TextView) findViewById(R.id.toggle_mode); 238 239 mInvertibleButtons = new View[] { 240 findViewById(R.id.fun_sin), 241 findViewById(R.id.fun_cos), 242 findViewById(R.id.fun_tan), 243 findViewById(R.id.fun_ln), 244 findViewById(R.id.fun_log), 245 findViewById(R.id.op_sqrt) 246 }; 247 mInverseButtons = new View[] { 248 findViewById(R.id.fun_arcsin), 249 findViewById(R.id.fun_arccos), 250 findViewById(R.id.fun_arctan), 251 findViewById(R.id.fun_exp), 252 findViewById(R.id.fun_10pow), 253 findViewById(R.id.op_sqr) 254 }; 255 256 mEvaluator = new Evaluator(this, mResultText); 257 mResultText.setEvaluator(mEvaluator); 258 KeyMaps.setActivity(this); 259 260 if (savedInstanceState != null) { 261 setState(CalculatorState.values()[ 262 savedInstanceState.getInt(KEY_DISPLAY_STATE, 263 CalculatorState.INPUT.ordinal())]); 264 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS); 265 if (unprocessed != null) { 266 mUnprocessedChars = unprocessed.toString(); 267 } 268 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE); 269 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 } 278 } else { 279 mCurrentState = CalculatorState.INPUT; 280 mEvaluator.clear(); 281 } 282 283 mFormulaText.setOnKeyListener(mFormulaOnKeyListener); 284 mFormulaText.setOnTextSizeChangeListener(this); 285 mFormulaText.setPasteListener(this); 286 mDeleteButton.setOnLongClickListener(this); 287 288 onInverseToggled(mInverseToggle.isSelected()); 289 onModeChanged(mEvaluator.getDegreeMode()); 290 291 if (mCurrentState != CalculatorState.INPUT) { 292 // Just reevaluate. 293 redisplayFormula(); 294 setState(CalculatorState.INIT); 295 mEvaluator.requireResult(); 296 } else { 297 redisplayAfterFormulaChange(); 298 } 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. 303 } 304 305 @Override 306 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 312 super.onSaveInstanceState(outState); 313 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal()); 314 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars); 315 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()); 323 } 324 325 // Set the state, updating delete label and display colors. 326 // This restores display positions on moving to INPUT. 327 // But movement/animation for moving to RESULT has already been done. 328 private void setState(CalculatorState state) { 329 if (mCurrentState != state) { 330 if (state == CalculatorState.INPUT) { 331 restoreDisplayPositions(); 332 } 333 mCurrentState = state; 334 335 if (mCurrentState == CalculatorState.RESULT) { 336 // No longer do this for ERROR; allow mistakes to be corrected. 337 mDeleteButton.setVisibility(View.GONE); 338 mClearButton.setVisibility(View.VISIBLE); 339 } else { 340 mDeleteButton.setVisibility(View.VISIBLE); 341 mClearButton.setVisibility(View.GONE); 342 } 343 344 if (mCurrentState == CalculatorState.ERROR) { 345 final int errorColor = getColor(R.color.calculator_error_color); 346 mFormulaText.setTextColor(errorColor); 347 mResultText.setTextColor(errorColor); 348 getWindow().setStatusBarColor(errorColor); 349 } 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)); 353 } 354 355 invalidateOptionsMenu(); 356 } 357 } 358 359 // Stop any active ActionMode. Return true if there was one. 360 private boolean stopActionMode() { 361 if (mResultText.stopActionMode()) { 362 return true; 363 } 364 if (mFormulaText.stopActionMode()) { 365 return true; 366 } 367 return false; 368 } 369 370 @Override 371 public void onBackPressed() { 372 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 } 381 } 382 } 383 384 @Override 385 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 395 /** 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)); 403 for (View invertibleButton : mInvertibleButtons) { 404 invertibleButton.setVisibility(View.GONE); 405 } 406 for (View inverseButton : mInverseButtons) { 407 inverseButton.setVisibility(View.VISIBLE); 408 } 409 } else { 410 mInverseToggle.setContentDescription(getString(R.string.desc_inv_off)); 411 for (View invertibleButton : mInvertibleButtons) { 412 invertibleButton.setVisibility(View.VISIBLE); 413 } 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) { 427 mModeView.setText(R.string.mode_deg); 428 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)); 432 } else { 433 mModeView.setText(R.string.mode_rad); 434 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)); 438 } 439 } 440 441 // 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); 463 if (mEvaluator.getExpr().hasInterestingOps()) { 464 mEvaluator.evaluateAndShowResult(); 465 } else { 466 mResultText.clear(); 467 } 468 } 469 470 public void onButtonClick(View view) { 471 mCurrentButton = view; 472 stopActionMode(); 473 474 // Always cancel in-progress evaluation. 475 // If we were waiting for the result, do nothing else. 476 mEvaluator.cancelAll(); 477 478 if (mCurrentState == CalculatorState.EVALUATE 479 || mCurrentState == CalculatorState.ANIMATE) { 480 onCancelled(); 481 return; 482 } 483 484 final int id = view.getId(); 485 switch (id) { 486 case R.id.eq: 487 onEquals(); 488 break; 489 case R.id.del: 490 onDelete(); 491 break; 492 case R.id.clr: 493 onClear(); 494 break; 495 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(); 502 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); 508 onModeChanged(mode); 509 510 setState(CalculatorState.INPUT); 511 mResultText.clear(); 512 if (mEvaluator.getExpr().hasInterestingOps()) { 513 mEvaluator.evaluateAndShowResult(); 514 } 515 break; 516 default: 517 addKeyToExpr(id); 518 redisplayAfterFormulaChange(); 519 break; 520 } 521 } 522 523 void redisplayFormula() { 524 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); 532 mFormulaText.setText(formatted); 533 } else { 534 mFormulaText.setText(formula); 535 } 536 } 537 538 @Override 539 public boolean onLongClick(View view) { 540 mCurrentButton = view; 541 542 if (view.getId() == R.id.del) { 543 onClear(); 544 return true; 545 } 546 return false; 547 } 548 549 // Initial evaluation completed successfully. Initiate display. 550 public void onEvaluate(int initDisplayPrec, int leastDigPos, String truncatedWholeNumber) { 551 // Invalidate any options that may depend on the current result. 552 invalidateOptionsMenu(); 553 554 mResultText.displayResult(initDisplayPrec, leastDigPos, truncatedWholeNumber); 555 if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state 556 onResult(mCurrentState != CalculatorState.INIT); 557 } 558 } 559 560 public void onCancelled() { 561 // We should be in EVALUATE state. 562 // Display is still in input state. 563 setState(CalculatorState.INPUT); 564 mResultText.clear(); 565 } 566 567 // Reevaluation completed; ask result to redisplay current value. 568 public void onReevaluate() 569 { 570 mResultText.redisplay(); 571 } 572 573 @Override 574 public void onTextSizeChanged(final TextView textView, float oldSize) { 575 if (mCurrentState != CalculatorState.INPUT) { 576 // Only animate text changes that occur from user input. 577 return; 578 } 579 580 // Calculate the values needed to perform the scale and translation animations, 581 // maintaining the same apparent baseline for the displayed text. 582 final float textScale = oldSize / textView.getTextSize(); 583 final float translationX = (1.0f - textScale) * 584 (textView.getWidth() / 2.0f - textView.getPaddingEnd()); 585 final float translationY = (1.0f - textScale) * 586 (textView.getHeight() / 2.0f - textView.getPaddingBottom()); 587 588 final AnimatorSet animatorSet = new AnimatorSet(); 589 animatorSet.playTogether( 590 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f), 591 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f), 592 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f), 593 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f)); 594 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime)); 595 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 596 animatorSet.start(); 597 } 598 599 private void onEquals() { 600 if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) { 601 setState(CalculatorState.EVALUATE); 602 mEvaluator.requireResult(); 603 } 604 } 605 606 private void onDelete() { 607 // Delete works like backspace; remove the last character or operator from the expression. 608 // Note that we handle keyboard delete exactly like the delete button. For 609 // example the delete button can be used to delete a character from an incomplete 610 // function name typed on a physical keyboard. 611 mEvaluator.cancelAll(); 612 // This should be impossible in RESULT state. 613 setState(CalculatorState.INPUT); 614 if (mUnprocessedChars != null) { 615 int len = mUnprocessedChars.length(); 616 if (len > 0) { 617 mUnprocessedChars = mUnprocessedChars.substring(0, len-1); 618 } else { 619 mEvaluator.delete(); 620 } 621 } else { 622 mEvaluator.delete(); 623 } 624 redisplayAfterFormulaChange(); 625 } 626 627 private void reveal(View sourceView, int colorRes, AnimatorListener listener) { 628 final ViewGroupOverlay groupOverlay = 629 (ViewGroupOverlay) getWindow().getDecorView().getOverlay(); 630 631 final Rect displayRect = new Rect(); 632 mDisplayView.getGlobalVisibleRect(displayRect); 633 634 // Make reveal cover the display and status bar. 635 final View revealView = new View(this); 636 revealView.setBottom(displayRect.bottom); 637 revealView.setLeft(displayRect.left); 638 revealView.setRight(displayRect.right); 639 revealView.setBackgroundColor(getResources().getColor(colorRes)); 640 groupOverlay.add(revealView); 641 642 final int[] clearLocation = new int[2]; 643 sourceView.getLocationInWindow(clearLocation); 644 clearLocation[0] += sourceView.getWidth() / 2; 645 clearLocation[1] += sourceView.getHeight() / 2; 646 647 final int revealCenterX = clearLocation[0] - revealView.getLeft(); 648 final int revealCenterY = clearLocation[1] - revealView.getTop(); 649 650 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2); 651 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2); 652 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2); 653 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2)); 654 655 final Animator revealAnimator = 656 ViewAnimationUtils.createCircularReveal(revealView, 657 revealCenterX, revealCenterY, 0.0f, revealRadius); 658 revealAnimator.setDuration( 659 getResources().getInteger(android.R.integer.config_longAnimTime)); 660 revealAnimator.addListener(listener); 661 662 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f); 663 alphaAnimator.setDuration( 664 getResources().getInteger(android.R.integer.config_mediumAnimTime)); 665 666 final AnimatorSet animatorSet = new AnimatorSet(); 667 animatorSet.play(revealAnimator).before(alphaAnimator); 668 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 669 animatorSet.addListener(new AnimatorListenerAdapter() { 670 @Override 671 public void onAnimationEnd(Animator animator) { 672 groupOverlay.remove(revealView); 673 mCurrentAnimator = null; 674 } 675 }); 676 677 mCurrentAnimator = animatorSet; 678 animatorSet.start(); 679 } 680 681 private void onClear() { 682 if (mEvaluator.getExpr().isEmpty()) { 683 return; 684 } 685 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() { 686 @Override 687 public void onAnimationEnd(Animator animation) { 688 mUnprocessedChars = null; 689 mResultText.clear(); 690 mEvaluator.clear(); 691 setState(CalculatorState.INPUT); 692 redisplayFormula(); 693 } 694 }); 695 } 696 697 // Evaluation encountered en error. Display the error. 698 void onError(final int errorResourceId) { 699 if (mCurrentState == CalculatorState.EVALUATE) { 700 setState(CalculatorState.ANIMATE); 701 reveal(mCurrentButton, R.color.calculator_error_color, 702 new AnimatorListenerAdapter() { 703 @Override 704 public void onAnimationEnd(Animator animation) { 705 setState(CalculatorState.ERROR); 706 mResultText.displayError(errorResourceId); 707 } 708 }); 709 } else if (mCurrentState == CalculatorState.INIT) { 710 setState(CalculatorState.ERROR); 711 mResultText.displayError(errorResourceId); 712 } else { 713 mResultText.clear(); 714 } 715 } 716 717 718 // Animate movement of result into the top formula slot. 719 // Result window now remains translated in the top slot while the result is displayed. 720 // (We convert it back to formula use only when the user provides new input.) 721 // Historical note: In the Lollipop version, this invisibly and instantaneously moved 722 // formula and result displays back at the end of the animation. We no longer do that, 723 // so that we can continue to properly support scrolling of the result. 724 // We assume the result already contains the text to be expanded. 725 private void onResult(boolean animate) { 726 // Calculate the textSize that would be used to display the result in the formula. 727 // For scrollable results just use the minimum textSize to maximize the number of digits 728 // that are visible on screen. 729 float textSize = mFormulaText.getMinimumTextSize(); 730 if (!mResultText.isScrollable()) { 731 textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString()); 732 } 733 734 // Scale the result to match the calculated textSize, minimizing the jump-cut transition 735 // when a result is reused in a subsequent expression. 736 final float resultScale = textSize / mResultText.getTextSize(); 737 738 // Set the result's pivot to match its gravity. 739 mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight()); 740 mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom()); 741 742 // Calculate the necessary translations so the result takes the place of the formula and 743 // the formula moves off the top of the screen. 744 final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom()) 745 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom()); 746 final float formulaTranslationY = -mFormulaText.getBottom(); 747 748 // Change the result's textColor to match the formula. 749 final int formulaTextColor = mFormulaText.getCurrentTextColor(); 750 751 if (animate) { 752 final AnimatorSet animatorSet = new AnimatorSet(); 753 animatorSet.playTogether( 754 ObjectAnimator.ofPropertyValuesHolder(mResultText, 755 PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale), 756 PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale), 757 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)), 758 ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor), 759 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY)); 760 animatorSet.setDuration(getResources().getInteger( 761 android.R.integer.config_longAnimTime)); 762 animatorSet.addListener(new AnimatorListenerAdapter() { 763 @Override 764 public void onAnimationEnd(Animator animation) { 765 setState(CalculatorState.RESULT); 766 mCurrentAnimator = null; 767 } 768 }); 769 770 mCurrentAnimator = animatorSet; 771 animatorSet.start(); 772 } else /* No animation desired; get there fast, e.g. when restarting */ { 773 mResultText.setScaleX(resultScale); 774 mResultText.setScaleY(resultScale); 775 mResultText.setTranslationY(resultTranslationY); 776 mResultText.setTextColor(formulaTextColor); 777 mFormulaText.setTranslationY(formulaTranslationY); 778 setState(CalculatorState.RESULT); 779 } 780 } 781 782 // Restore positions of the formula and result displays back to their original, 783 // pre-animation state. 784 private void restoreDisplayPositions() { 785 // Clear result. 786 mResultText.setText(""); 787 // Reset all of the values modified during the animation. 788 mResultText.setScaleX(1.0f); 789 mResultText.setScaleY(1.0f); 790 mResultText.setTranslationX(0.0f); 791 mResultText.setTranslationY(0.0f); 792 mFormulaText.setTranslationY(0.0f); 793 794 mFormulaText.requestFocus(); 795 } 796 797 @Override 798 public boolean onCreateOptionsMenu(Menu menu) { 799 super.onCreateOptionsMenu(menu); 800 801 getMenuInflater().inflate(R.menu.activity_calculator, menu); 802 return true; 803 } 804 805 @Override 806 public boolean onPrepareOptionsMenu(Menu menu) { 807 super.onPrepareOptionsMenu(menu); 808 809 // Show the leading option when displaying a result. 810 menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT); 811 812 // Show the fraction option when displaying a rational result. 813 menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT 814 && mEvaluator.getRational() != null); 815 816 return true; 817 } 818 819 @Override 820 public boolean onOptionsItemSelected(MenuItem item) { 821 switch (item.getItemId()) { 822 case R.id.menu_leading: 823 displayFull(); 824 return true; 825 case R.id.menu_fraction: 826 displayFraction(); 827 return true; 828 case R.id.menu_licenses: 829 startActivity(new Intent(this, Licenses.class)); 830 return true; 831 default: 832 return super.onOptionsItemSelected(item); 833 } 834 } 835 836 private void displayMessage(String s) { 837 new AlertDialog.Builder(this) 838 .setMessage(s) 839 .setNegativeButton(R.string.dismiss, null /* listener */) 840 .show(); 841 } 842 843 private void displayFraction() { 844 BoundedRational result = mEvaluator.getRational(); 845 displayMessage(KeyMaps.translateResult(result.toNiceString())); 846 } 847 848 // Display full result to currently evaluated precision 849 private void displayFull() { 850 Resources res = getResources(); 851 String msg = mResultText.getFullText() + " "; 852 if (mResultText.fullTextIsExact()) { 853 msg += res.getString(R.string.exact); 854 } else { 855 msg += res.getString(R.string.approximate); 856 } 857 displayMessage(msg); 858 } 859 860 // Add input characters to the end of the expression by mapping them to 861 // the appropriate button pushes when possible. Leftover characters 862 // are added to mUnprocessedChars, which is presumed to immediately 863 // precede the newly added characters. 864 private void addChars(String moreChars) { 865 if (mUnprocessedChars != null) { 866 moreChars = mUnprocessedChars + moreChars; 867 } 868 int current = 0; 869 int len = moreChars.length(); 870 while (current < len) { 871 char c = moreChars.charAt(current); 872 int k = KeyMaps.keyForChar(c); 873 if (k != View.NO_ID) { 874 mCurrentButton = findViewById(k); 875 addKeyToExpr(k); 876 if (Character.isSurrogate(c)) { 877 current += 2; 878 } else { 879 ++current; 880 } 881 continue; 882 } 883 int f = KeyMaps.funForString(moreChars, current); 884 if (f != View.NO_ID) { 885 mCurrentButton = findViewById(f); 886 addKeyToExpr(f); 887 if (f == R.id.op_sqrt) { 888 // Square root entered as function; don't lose the parenthesis. 889 addKeyToExpr(R.id.lparen); 890 } 891 current = moreChars.indexOf('(', current) + 1; 892 continue; 893 } 894 // There are characters left, but we can't convert them to button presses. 895 mUnprocessedChars = moreChars.substring(current); 896 redisplayAfterFormulaChange(); 897 return; 898 } 899 mUnprocessedChars = null; 900 redisplayAfterFormulaChange(); 901 return; 902 } 903 904 @Override 905 public boolean paste(Uri uri) { 906 if (mEvaluator.isLastSaved(uri)) { 907 if (mCurrentState == CalculatorState.ERROR 908 || mCurrentState == CalculatorState.RESULT) { 909 setState(CalculatorState.INPUT); 910 mEvaluator.clear(); 911 } 912 mEvaluator.addSaved(); 913 redisplayAfterFormulaChange(); 914 return true; 915 } 916 return false; 917 } 918 919 @Override 920 public void paste(String s) { 921 addChars(s); 922 } 923 924} 925