Calculator.java revision 08e8f322b0d93e06aaa2a15acc869dfd70791461
1/* 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 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: Revisit handling of "Help" menu, so that it's more consistent 21// with our conventions. 22// TODO: See if we can make scrolling look better, especially on small 23// displays. Fix evaluation interface so the evaluator returns entire 24// result, and formatting of exponent etc. is done separately. 25// TODO: Better indication of when the result is known to be exact. 26// TODO: Fix placement of inverse trig buttons. 27// TODO: Check and possibly fix accessability issues. 28// TODO: Copy & more general paste in formula? Note that this requires 29// great care: Currently the text version of a displayed formula 30// is not directly useful for re-evaluating the formula later, since 31// it contains ellipses representing subexpressions evaluated with 32// a different degree mode. Rather than supporting copy from the 33// formula window, we may eventually want to support generation of a 34// more useful text version in a separate window. It's not clear 35// this is worth the added (code and user) complexity. 36 37package com.android.calculator2; 38 39import android.animation.Animator; 40import android.animation.Animator.AnimatorListener; 41import android.animation.AnimatorListenerAdapter; 42import android.animation.AnimatorSet; 43import android.animation.ArgbEvaluator; 44import android.animation.ObjectAnimator; 45import android.animation.ValueAnimator; 46import android.animation.ValueAnimator.AnimatorUpdateListener; 47import android.app.Activity; 48import android.app.AlertDialog; 49import android.content.Context; 50import android.content.DialogInterface; 51import android.content.res.Resources; 52import android.graphics.Color; 53import android.graphics.Rect; 54import android.net.Uri; 55import android.os.Bundle; 56import android.support.annotation.NonNull; 57import android.support.v4.view.ViewPager; 58import android.text.Editable; 59import android.text.SpannableString; 60import android.text.Spanned; 61import android.text.TextUtils; 62import android.text.TextWatcher; 63import android.text.style.ForegroundColorSpan; 64import android.util.Log; 65import android.view.KeyCharacterMap; 66import android.view.KeyEvent; 67import android.view.Menu; 68import android.view.MenuItem; 69import android.view.View; 70import android.view.View.OnKeyListener; 71import android.view.View.OnLongClickListener; 72import android.view.ViewAnimationUtils; 73import android.view.ViewGroupOverlay; 74import android.view.animation.AccelerateDecelerateInterpolator; 75import android.webkit.WebView; 76import android.widget.TextView; 77import android.widget.Toolbar; 78 79import com.android.calculator2.CalculatorText.OnTextSizeChangeListener; 80 81import java.io.ByteArrayInputStream; 82import java.io.ObjectInputStream; 83import java.io.ByteArrayOutputStream; 84import java.io.ObjectOutputStream; 85import java.io.ObjectInput; 86import java.io.ObjectOutput; 87import java.io.IOException; 88import java.text.DecimalFormatSymbols; // TODO: May eventually not need this here. 89 90public class Calculator extends Activity 91 implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.PasteListener { 92 93 /** 94 * Constant for an invalid resource id. 95 */ 96 public static final int INVALID_RES_ID = -1; 97 98 private enum CalculatorState { 99 INPUT, // Result and formula both visible, no evaluation requested, 100 // Though result may be visible on bottom line. 101 EVALUATE, // Both visible, evaluation requested, evaluation/animation incomplete. 102 INIT, // Very temporary state used as alternative to EVALUATE 103 // during reinitialization. Do not animate on completion. 104 ANIMATE, // Result computed, animation to enlarge result window in progress. 105 RESULT, // Result displayed, formula invisible. 106 // If we are in RESULT state, the formula was evaluated without 107 // error to initial precision. 108 ERROR // Error displayed: Formula visible, result shows error message. 109 // Display similar to INPUT state. 110 } 111 // Normal transition sequence is 112 // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT 113 // A RESULT -> ERROR transition is possible in rare corner cases, in which 114 // a higher precision evaluation exposes an error. This is possible, since we 115 // initially evaluate assuming we were given a well-defined problem. If we 116 // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0 117 // unless we are asked for enough precision that we can distinguish the argument from zero. 118 // TODO: Consider further heuristics to reduce the chance of observing this? 119 // It already seems to be observable only in contrived cases. 120 // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application 121 // is restarted in that state. This leads us to recompute and redisplay the result 122 // ASAP. 123 // TODO: Possibly save a bit more information, e.g. its initial display string 124 // or most significant digit position, to speed up restart. 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 // TODO: Perhaps the formula should not be editable at all? 129 130 private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() { 131 @Override 132 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 133 if (keyEvent.getAction() != KeyEvent.ACTION_UP) return true; 134 switch (keyCode) { 135 case KeyEvent.KEYCODE_NUMPAD_ENTER: 136 case KeyEvent.KEYCODE_ENTER: 137 case KeyEvent.KEYCODE_DPAD_CENTER: 138 mCurrentButton = mEqualButton; 139 onEquals(); 140 return true; 141 case KeyEvent.KEYCODE_DEL: 142 mCurrentButton = mDeleteButton; 143 onDelete(); 144 return true; 145 default: 146 final int raw = keyEvent.getKeyCharacterMap() 147 .get(keyCode, keyEvent.getMetaState()); 148 if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) { 149 return true; // discard 150 } 151 // Try to discard non-printing characters and the like. 152 // The user will have to explicitly delete other junk that gets past us. 153 if (Character.isIdentifierIgnorable(raw) 154 || Character.isWhitespace(raw)) { 155 return true; 156 } 157 char c = (char)raw; 158 if (c == '=') { 159 onEquals(); 160 } else { 161 addChars(String.valueOf(c)); 162 redisplayAfterFormulaChange(); 163 } 164 } 165 return false; 166 } 167 }; 168 169 private static final String NAME = Calculator.class.getName(); 170 private static final String KEY_DISPLAY_STATE = NAME + "_display_state"; 171 private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars"; 172 private static final String KEY_EVAL_STATE = NAME + "_eval_state"; 173 // Associated value is a byte array holding both mCalculatorState 174 // and the (much more complex) evaluator state. 175 176 private CalculatorState mCurrentState; 177 private Evaluator mEvaluator; 178 179 private View mDisplayView; 180 private TextView mModeView; 181 private CalculatorText mFormulaText; 182 private CalculatorResult mResult; 183 184 private ViewPager mPadViewPager; 185 private View mDeleteButton; 186 private View mClearButton; 187 private View mEqualButton; 188 private TextView mModeButton; 189 190 private View mCurrentButton; 191 private Animator mCurrentAnimator; 192 193 private String mUnprocessedChars = null; // Characters that were recently entered 194 // at the end of the display that have not yet 195 // been added to the underlying expression. 196 197 @Override 198 protected void onCreate(Bundle savedInstanceState) { 199 super.onCreate(savedInstanceState); 200 setContentView(R.layout.activity_calculator); 201 setActionBar((Toolbar) findViewById(R.id.toolbar)); 202 203 // Hide all default options in the ActionBar. 204 getActionBar().setDisplayOptions(0); 205 206 mDisplayView = findViewById(R.id.display); 207 mModeView = (TextView) findViewById(R.id.deg_rad); 208 mFormulaText = (CalculatorText) findViewById(R.id.formula); 209 mResult = (CalculatorResult) findViewById(R.id.result); 210 211 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager); 212 mDeleteButton = findViewById(R.id.del); 213 mClearButton = findViewById(R.id.clr); 214 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq); 215 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) { 216 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq); 217 } 218 mModeButton = (TextView) findViewById(R.id.mode_deg_rad); 219 220 mEvaluator = new Evaluator(this, mResult); 221 mResult.setEvaluator(mEvaluator); 222 KeyMaps.setActivity(this); 223 224 if (savedInstanceState != null) { 225 setState(CalculatorState.values()[ 226 savedInstanceState.getInt(KEY_DISPLAY_STATE, 227 CalculatorState.INPUT.ordinal())]); 228 CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS); 229 if (unprocessed != null) { 230 mUnprocessedChars = unprocessed.toString(); 231 } 232 byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE); 233 if (state != null) { 234 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) { 235 mEvaluator.restoreInstanceState(in); 236 } catch (Throwable ignored) { 237 // When in doubt, revert to clean state 238 mCurrentState = CalculatorState.INPUT; 239 mEvaluator.clear(); 240 } 241 } 242 } 243 mFormulaText.setOnKeyListener(mFormulaOnKeyListener); 244 mFormulaText.setOnTextSizeChangeListener(this); 245 mFormulaText.setPasteListener(this); 246 mDeleteButton.setOnLongClickListener(this); 247 updateDegreeMode(mEvaluator.getDegreeMode()); 248 if (mCurrentState == CalculatorState.EVALUATE) { 249 // Odd case. Evaluation probably took a long time. Let user ask for it again 250 mCurrentState = CalculatorState.INPUT; 251 // TODO: This can happen if the user rotates the screen. 252 // Is this rotate-to-abort behavior correct? Revisit after experimentation. 253 } 254 if (mCurrentState != CalculatorState.INPUT) { 255 setState(CalculatorState.INIT); 256 mEvaluator.requireResult(); 257 } else { 258 redisplayAfterFormulaChange(); 259 } 260 // TODO: We're currently not saving and restoring scroll position. 261 // We probably should. Details may require care to deal with: 262 // - new display size 263 // - slow recomputation if we've scrolled far. 264 } 265 266 @Override 267 protected void onSaveInstanceState(@NonNull Bundle outState) { 268 // If there's an animation in progress, cancel it first to ensure our state is up-to-date. 269 if (mCurrentAnimator != null) { 270 mCurrentAnimator.cancel(); 271 } 272 273 super.onSaveInstanceState(outState); 274 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal()); 275 outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars); 276 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); 277 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) { 278 mEvaluator.saveInstanceState(out); 279 } catch (IOException e) { 280 // Impossible; No IO involved. 281 throw new AssertionError("Impossible IO exception", e); 282 } 283 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray()); 284 } 285 286 // Set the state, updating delete label and display colors. 287 // This restores display positions on moving to INPUT. 288 // But movement/animation for moving to RESULT has already been done. 289 private void setState(CalculatorState state) { 290 if (mCurrentState != state) { 291 if (state == CalculatorState.INPUT) { 292 restoreDisplayPositions(); 293 } 294 mCurrentState = state; 295 296 if (mCurrentState == CalculatorState.RESULT) { 297 // No longer do this for ERROR; allow mistakes to be corrected. 298 mDeleteButton.setVisibility(View.GONE); 299 mClearButton.setVisibility(View.VISIBLE); 300 } else { 301 mDeleteButton.setVisibility(View.VISIBLE); 302 mClearButton.setVisibility(View.GONE); 303 } 304 305 if (mCurrentState == CalculatorState.ERROR) { 306 final int errorColor = getResources().getColor(R.color.calculator_error_color); 307 mFormulaText.setTextColor(errorColor); 308 mResult.setTextColor(errorColor); 309 getWindow().setStatusBarColor(errorColor); 310 } else { 311 mFormulaText.setTextColor( 312 getResources().getColor(R.color.display_formula_text_color)); 313 mResult.setTextColor( 314 getResources().getColor(R.color.display_result_text_color)); 315 getWindow().setStatusBarColor( 316 getResources().getColor(R.color.calculator_accent_color)); 317 } 318 319 invalidateOptionsMenu(); 320 } 321 } 322 323 @Override 324 public void onBackPressed() { 325 if (mPadViewPager == null || mPadViewPager.getCurrentItem() == 0) { 326 // If the user is currently looking at the first pad (or the pad is not paged), 327 // allow the system to handle the Back button. 328 super.onBackPressed(); 329 } else { 330 // Otherwise, select the previous pad. 331 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1); 332 } 333 } 334 335 @Override 336 public void onUserInteraction() { 337 super.onUserInteraction(); 338 339 // If there's an animation in progress, cancel it so the user interaction can be handled 340 // immediately. 341 if (mCurrentAnimator != null) { 342 mCurrentAnimator.cancel(); 343 } 344 } 345 346 // Update the top corner degree/radian display and mode button 347 // to reflect the indicated current degree mode (true = degrees) 348 // TODO: Hide the top corner display until the advanced panel is exposed. 349 private void updateDegreeMode(boolean dm) { 350 if (dm) { 351 mModeView.setText(R.string.mode_deg); 352 mModeButton.setText(R.string.mode_rad); 353 mModeButton.setContentDescription(getString(R.string.desc_mode_rad)); 354 } else { 355 mModeView.setText(R.string.mode_rad); 356 mModeButton.setText(R.string.mode_deg); 357 mModeButton.setContentDescription(getString(R.string.desc_mode_deg)); 358 } 359 } 360 361 // Add the given button id to input expression. 362 // If appropriate, clear the expression before doing so. 363 private void addKeyToExpr(int id) { 364 if (mCurrentState == CalculatorState.ERROR) { 365 setState(CalculatorState.INPUT); 366 } else if (mCurrentState == CalculatorState.RESULT) { 367 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) { 368 mEvaluator.collapse(); 369 } else { 370 mEvaluator.clear(); 371 } 372 setState(CalculatorState.INPUT); 373 } 374 if (!mEvaluator.append(id)) { 375 // TODO: Some user visible feedback? 376 } 377 } 378 379 private void redisplayAfterFormulaChange() { 380 // TODO: Could do this more incrementally. 381 redisplayFormula(); 382 setState(CalculatorState.INPUT); 383 mResult.clear(); 384 mEvaluator.evaluateAndShowResult(); 385 } 386 387 public void onButtonClick(View view) { 388 mCurrentButton = view; 389 390 // Always cancel in-progress evaluation. 391 // If we were waiting for the result, do nothing else. 392 mEvaluator.cancelAll(); 393 394 if (mCurrentState == CalculatorState.EVALUATE 395 || mCurrentState == CalculatorState.ANIMATE) { 396 onCancelled(); 397 return; 398 } 399 400 401 final int id = view.getId(); 402 switch (id) { 403 case R.id.eq: 404 onEquals(); 405 break; 406 case R.id.del: 407 onDelete(); 408 break; 409 case R.id.clr: 410 onClear(); 411 break; 412 case R.id.mode_deg_rad: 413 boolean mode = !mEvaluator.getDegreeMode(); 414 updateDegreeMode(mode); 415 if (mCurrentState == CalculatorState.RESULT) { 416 mEvaluator.collapse(); // Capture result evaluated in old mode 417 redisplayFormula(); 418 } 419 // In input mode, we reinterpret already entered trig functions. 420 mEvaluator.setDegreeMode(mode); 421 setState(CalculatorState.INPUT); 422 mResult.clear(); 423 mEvaluator.evaluateAndShowResult(); 424 break; 425 default: 426 addKeyToExpr(id); 427 redisplayAfterFormulaChange(); 428 break; 429 } 430 } 431 432 void redisplayFormula() { 433 String formula = mEvaluator.getExpr().toString(this); 434 if (mUnprocessedChars != null) { 435 // Add and highlight characters we couldn't process. 436 SpannableString formatted = new SpannableString(formula + mUnprocessedChars); 437 // TODO: should probably match this to the error color. 438 formatted.setSpan(new ForegroundColorSpan(Color.RED), 439 formula.length(), formatted.length(), 440 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 441 mFormulaText.setText(formatted); 442 } else { 443 mFormulaText.setText(formula); 444 } 445 } 446 447 @Override 448 public boolean onLongClick(View view) { 449 mCurrentButton = view; 450 451 if (view.getId() == R.id.del) { 452 onClear(); 453 return true; 454 } 455 return false; 456 } 457 458 // Initial evaluation completed successfully. Initiate display. 459 public void onEvaluate(int initDisplayPrec, String truncatedWholeNumber) { 460 // Invalidate any options that may depend on the current result. 461 invalidateOptionsMenu(); 462 463 if (mCurrentState == CalculatorState.INPUT) { 464 // Just update small result display. 465 mResult.displayResult(initDisplayPrec, truncatedWholeNumber); 466 } else { // in EVALUATE or INIT state 467 mResult.displayResult(initDisplayPrec, truncatedWholeNumber); 468 onResult(mCurrentState != CalculatorState.INIT); 469 } 470 } 471 472 public void onCancelled() { 473 // We should be in EVALUATE state. 474 // Display is still in input state. 475 setState(CalculatorState.INPUT); 476 } 477 478 // Reevaluation completed; ask result to redisplay current value. 479 public void onReevaluate() 480 { 481 mResult.redisplay(); 482 } 483 484 @Override 485 public void onTextSizeChanged(final TextView textView, float oldSize) { 486 if (mCurrentState != CalculatorState.INPUT) { 487 // Only animate text changes that occur from user input. 488 return; 489 } 490 491 // Calculate the values needed to perform the scale and translation animations, 492 // maintaining the same apparent baseline for the displayed text. 493 final float textScale = oldSize / textView.getTextSize(); 494 final float translationX = (1.0f - textScale) * 495 (textView.getWidth() / 2.0f - textView.getPaddingEnd()); 496 final float translationY = (1.0f - textScale) * 497 (textView.getHeight() / 2.0f - textView.getPaddingBottom()); 498 499 final AnimatorSet animatorSet = new AnimatorSet(); 500 animatorSet.playTogether( 501 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f), 502 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f), 503 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f), 504 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f)); 505 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime)); 506 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 507 animatorSet.start(); 508 } 509 510 private void onEquals() { 511 if (mCurrentState == CalculatorState.INPUT) { 512 setState(CalculatorState.EVALUATE); 513 mEvaluator.requireResult(); 514 } 515 } 516 517 private void onDelete() { 518 // Delete works like backspace; remove the last character or operator from the expression. 519 // Note that we handle keyboard delete exactly like the delete button. For 520 // example the delete button can be used to delete a character from an incomplete 521 // function name typed on a physical keyboard. 522 mEvaluator.cancelAll(); 523 // This should be impossible in RESULT state. 524 setState(CalculatorState.INPUT); 525 if (mUnprocessedChars != null) { 526 int len = mUnprocessedChars.length(); 527 if (len > 0) { 528 mUnprocessedChars = mUnprocessedChars.substring(0, len-1); 529 } else { 530 mEvaluator.getExpr().delete(); 531 } 532 } else { 533 mEvaluator.getExpr().delete(); 534 } 535 redisplayAfterFormulaChange(); 536 } 537 538 private void reveal(View sourceView, int colorRes, AnimatorListener listener) { 539 final ViewGroupOverlay groupOverlay = 540 (ViewGroupOverlay) getWindow().getDecorView().getOverlay(); 541 542 final Rect displayRect = new Rect(); 543 mDisplayView.getGlobalVisibleRect(displayRect); 544 545 // Make reveal cover the display and status bar. 546 final View revealView = new View(this); 547 revealView.setBottom(displayRect.bottom); 548 revealView.setLeft(displayRect.left); 549 revealView.setRight(displayRect.right); 550 revealView.setBackgroundColor(getResources().getColor(colorRes)); 551 groupOverlay.add(revealView); 552 553 final int[] clearLocation = new int[2]; 554 sourceView.getLocationInWindow(clearLocation); 555 clearLocation[0] += sourceView.getWidth() / 2; 556 clearLocation[1] += sourceView.getHeight() / 2; 557 558 final int revealCenterX = clearLocation[0] - revealView.getLeft(); 559 final int revealCenterY = clearLocation[1] - revealView.getTop(); 560 561 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2); 562 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2); 563 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2); 564 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2)); 565 566 final Animator revealAnimator = 567 ViewAnimationUtils.createCircularReveal(revealView, 568 revealCenterX, revealCenterY, 0.0f, revealRadius); 569 revealAnimator.setDuration( 570 getResources().getInteger(android.R.integer.config_longAnimTime)); 571 revealAnimator.addListener(listener); 572 573 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f); 574 alphaAnimator.setDuration( 575 getResources().getInteger(android.R.integer.config_mediumAnimTime)); 576 577 final AnimatorSet animatorSet = new AnimatorSet(); 578 animatorSet.play(revealAnimator).before(alphaAnimator); 579 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 580 animatorSet.addListener(new AnimatorListenerAdapter() { 581 @Override 582 public void onAnimationEnd(Animator animator) { 583 groupOverlay.remove(revealView); 584 mCurrentAnimator = null; 585 } 586 }); 587 588 mCurrentAnimator = animatorSet; 589 animatorSet.start(); 590 } 591 592 private void onClear() { 593 if (mEvaluator.getExpr().isEmpty()) { 594 return; 595 } 596 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() { 597 @Override 598 public void onAnimationEnd(Animator animation) { 599 mUnprocessedChars = null; 600 mResult.clear(); 601 mEvaluator.clear(); 602 setState(CalculatorState.INPUT); 603 redisplayFormula(); 604 } 605 }); 606 } 607 608 // Evaluation encountered en error. Display the error. 609 void onError(final int errorResourceId) { 610 if (mCurrentState != CalculatorState.EVALUATE) { 611 // Only animate error on evaluate. 612 return; 613 } 614 615 setState(CalculatorState.ANIMATE); 616 reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() { 617 @Override 618 public void onAnimationEnd(Animator animation) { 619 setState(CalculatorState.ERROR); 620 mResult.displayError(errorResourceId); 621 } 622 }); 623 } 624 625 626 // Animate movement of result into the top formula slot. 627 // Result window now remains translated in the top slot while the result is displayed. 628 // (We convert it back to formula use only when the user provides new input.) 629 // Historical note: In the Lollipop version, this invisibly and instantaeously moved 630 // formula and result displays back at the end of the animation. We no longer do that, 631 // so that we can continue to properly support scrolling of the result. 632 // We assume the result already contains the text to be expanded. 633 private void onResult(boolean animate) { 634 // Calculate the values needed to perform the scale and translation animations. 635 // We now fix the character size in the display to avoid weird effects 636 // when we scroll. 637 // Display.xml is designed to ensure exactly a 3/2 ratio between the formula 638 // slot and small result slot. 639 final float resultScale = 1.5f; 640 final float resultTranslationX = -mResult.getWidth() * (resultScale - 1)/2; 641 // mFormulaText is aligned with mResult on the right. 642 // When we enlarge it around its center, the right side 643 // moves to the right. This compensates. 644 float resultTranslationY = -mResult.getHeight(); 645 // This is how much we want to move the bottom. 646 // Now compensate for the fact that we're 647 // simultaenously expanding it around its center by half its height 648 resultTranslationY += mResult.getHeight() * (resultScale - 1)/2; 649 final float formulaTranslationY = -mFormulaText.getBottom(); 650 651 // TODO: Reintroduce textColorAnimator? 652 // The initial and final colors seemed to be the same in L. 653 // With the new model, the result logically changes back to a formula 654 // only when we switch back to INPUT state, so it's unclear that animating 655 // a color change here makes sense. 656 if (animate) { 657 final AnimatorSet animatorSet = new AnimatorSet(); 658 animatorSet.playTogether( 659 ObjectAnimator.ofFloat(mResult, View.SCALE_X, resultScale), 660 ObjectAnimator.ofFloat(mResult, View.SCALE_Y, resultScale), 661 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_X, resultTranslationX), 662 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_Y, resultTranslationY), 663 ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, 664 formulaTranslationY)); 665 animatorSet.setDuration( 666 getResources().getInteger(android.R.integer.config_longAnimTime)); 667 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 668 animatorSet.addListener(new AnimatorListenerAdapter() { 669 @Override 670 public void onAnimationStart(Animator animation) { 671 // Result should already be displayed; no need to do anything. 672 } 673 674 @Override 675 public void onAnimationEnd(Animator animation) { 676 setState(CalculatorState.RESULT); 677 mCurrentAnimator = null; 678 } 679 }); 680 681 mCurrentAnimator = animatorSet; 682 animatorSet.start(); 683 } else /* No animation desired; get there fast, e.g. when restarting */ { 684 mResult.setScaleX(resultScale); 685 mResult.setScaleY(resultScale); 686 mResult.setTranslationX(resultTranslationX); 687 mResult.setTranslationY(resultTranslationY); 688 mFormulaText.setTranslationY(formulaTranslationY); 689 setState(CalculatorState.RESULT); 690 } 691 } 692 693 // Restore positions of the formula and result displays back to their original, 694 // pre-animation state. 695 private void restoreDisplayPositions() { 696 // Clear result. 697 mResult.setText(""); 698 // Reset all of the values modified during the animation. 699 mResult.setScaleX(1.0f); 700 mResult.setScaleY(1.0f); 701 mResult.setTranslationX(0.0f); 702 mResult.setTranslationY(0.0f); 703 mFormulaText.setTranslationY(0.0f); 704 705 mFormulaText.requestFocus(); 706 } 707 708 @Override 709 public boolean onCreateOptionsMenu(Menu menu) { 710 getMenuInflater().inflate(R.menu.overflow, menu); 711 return true; 712 } 713 714 @Override 715 public boolean onPrepareOptionsMenu(Menu menu) { 716 if (mCurrentState != CalculatorState.RESULT) { 717 menu.findItem(R.id.menu_fraction).setEnabled(false); 718 menu.findItem(R.id.menu_leading).setEnabled(false); 719 } else if (mEvaluator.getRational() == null) { 720 menu.findItem(R.id.menu_fraction).setEnabled(false); 721 } 722 return true; 723 } 724 725 @Override 726 public boolean onOptionsItemSelected(MenuItem item) { 727 switch (item.getItemId()) { 728 case R.id.menu_help: 729 displayHelpMessage(); 730 return true; 731 case R.id.menu_about: 732 displayAboutPage(); 733 return true; 734 case R.id.menu_fraction: 735 displayFraction(); 736 return true; 737 case R.id.menu_leading: 738 displayFull(); 739 return true; 740 default: 741 return super.onOptionsItemSelected(item); 742 } 743 } 744 745 private void displayMessage(String s) { 746 AlertDialog.Builder builder = new AlertDialog.Builder(this); 747 builder.setMessage(s) 748 .setNegativeButton(R.string.dismiss, 749 new DialogInterface.OnClickListener() { 750 public void onClick(DialogInterface d, int which) { } 751 }) 752 .show(); 753 } 754 755 private void displayHelpMessage() { 756 Resources res = getResources(); 757 String msg = res.getString(R.string.help_message); 758 if (mPadViewPager != null) { 759 msg += res.getString(R.string.help_pager); 760 } 761 displayMessage(msg); 762 } 763 764 private void displayFraction() { 765 BoundedRational result = mEvaluator.getRational(); 766 displayMessage(KeyMaps.translateResult(result.toNiceString())); 767 } 768 769 // Display full result to currently evaluated precision 770 private void displayFull() { 771 Resources res = getResources(); 772 String msg = mResult.getFullText() + " "; 773 if (mResult.fullTextIsExact()) { 774 msg += res.getString(R.string.exact); 775 } else { 776 msg += res.getString(R.string.approximate); 777 } 778 displayMessage(msg); 779 } 780 781 private void displayAboutPage() { 782 WebView wv = new WebView(this); 783 wv.loadUrl("file:///android_asset/about.txt"); 784 new AlertDialog.Builder(this) 785 .setView(wv) 786 .setNegativeButton(R.string.dismiss, 787 new DialogInterface.OnClickListener() { 788 public void onClick(DialogInterface d, int which) { } 789 }) 790 .show(); 791 } 792 793 // Add input characters to the end of the expression by mapping them to 794 // the appropriate button pushes when possible. Leftover characters 795 // are added to mUnprocessedChars, which is presumed to immediately 796 // precede the newly added characters. 797 private void addChars(String moreChars) { 798 if (mUnprocessedChars != null) { 799 moreChars = mUnprocessedChars + moreChars; 800 } 801 int current = 0; 802 int len = moreChars.length(); 803 while (current < len) { 804 char c = moreChars.charAt(current); 805 int k = KeyMaps.keyForChar(c); 806 if (k != View.NO_ID) { 807 mCurrentButton = findViewById(k); 808 addKeyToExpr(k); 809 if (Character.isSurrogate(c)) { 810 current += 2; 811 } else { 812 ++current; 813 } 814 continue; 815 } 816 int f = KeyMaps.funForString(moreChars, current); 817 if (f != View.NO_ID) { 818 mCurrentButton = findViewById(f); 819 addKeyToExpr(f); 820 if (f == R.id.op_sqrt) { 821 // Square root entered as function; don't lose the parenthesis. 822 addKeyToExpr(R.id.lparen); 823 } 824 current = moreChars.indexOf('(', current) + 1; 825 continue; 826 } 827 // There are characters left, but we can't convert them to button presses. 828 mUnprocessedChars = moreChars.substring(current); 829 redisplayAfterFormulaChange(); 830 return; 831 } 832 mUnprocessedChars = null; 833 redisplayAfterFormulaChange(); 834 return; 835 } 836 837 @Override 838 public boolean paste(Uri uri) { 839 if (mEvaluator.isLastSaved(uri)) { 840 if (mCurrentState == CalculatorState.ERROR 841 || mCurrentState == CalculatorState.RESULT) { 842 setState(CalculatorState.INPUT); 843 mEvaluator.clear(); 844 } 845 mEvaluator.addSaved(); 846 redisplayAfterFormulaChange(); 847 return true; 848 } 849 return false; 850 } 851 852 @Override 853 public void paste(String s) { 854 addChars(s); 855 } 856 857} 858