Calculator.java revision d48b756434bda6a5f66740a8ea603aca1f536544
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.CalculatorEditText.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, CalculatorEditText.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_EVAL_STATE = NAME + "_eval_state"; 172 // Associated value is a byte array holding both mCalculatorState 173 // and the (much more complex) evaluator state. 174 175 private CalculatorState mCurrentState; 176 private Evaluator mEvaluator; 177 178 private View mDisplayView; 179 private TextView mModeView; 180 private CalculatorEditText mFormulaEditText; 181 private CalculatorResult mResult; 182 183 private ViewPager mPadViewPager; 184 private View mDeleteButton; 185 private View mClearButton; 186 private View mEqualButton; 187 private TextView mModeButton; 188 189 private View mCurrentButton; 190 private Animator mCurrentAnimator; 191 192 private String mUnprocessedChars = null; // Characters that were recently entered 193 // at the end of the display that have not yet 194 // been added to the underlying expression. 195 196 @Override 197 protected void onCreate(Bundle savedInstanceState) { 198 super.onCreate(savedInstanceState); 199 setContentView(R.layout.activity_calculator); 200 setActionBar((Toolbar) findViewById(R.id.toolbar)); 201 202 // Hide all default options in the ActionBar. 203 getActionBar().setDisplayOptions(0); 204 205 mDisplayView = findViewById(R.id.display); 206 mModeView = (TextView) findViewById(R.id.deg_rad); 207 mFormulaEditText = (CalculatorEditText) findViewById(R.id.formula); 208 mResult = (CalculatorResult) findViewById(R.id.result); 209 210 mPadViewPager = (ViewPager) findViewById(R.id.pad_pager); 211 mDeleteButton = findViewById(R.id.del); 212 mClearButton = findViewById(R.id.clr); 213 mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq); 214 if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) { 215 mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq); 216 } 217 mModeButton = (TextView) findViewById(R.id.mode_deg_rad); 218 219 mEvaluator = new Evaluator(this, mResult); 220 mResult.setEvaluator(mEvaluator); 221 KeyMaps.setActivity(this); 222 223 if (savedInstanceState != null) { 224 setState(CalculatorState.values()[ 225 savedInstanceState.getInt(KEY_DISPLAY_STATE, 226 CalculatorState.INPUT.ordinal())]); 227 byte[] state = 228 savedInstanceState.getByteArray(KEY_EVAL_STATE); 229 if (state != null) { 230 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) { 231 mEvaluator.restoreInstanceState(in); 232 } catch (Throwable ignored) { 233 // When in doubt, revert to clean state 234 mCurrentState = CalculatorState.INPUT; 235 mEvaluator.clear(); 236 } 237 } 238 } 239 mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener); 240 mFormulaEditText.setOnTextSizeChangeListener(this); 241 mFormulaEditText.setPasteListener(this); 242 mDeleteButton.setOnLongClickListener(this); 243 updateDegreeMode(mEvaluator.getDegreeMode()); 244 if (mCurrentState == CalculatorState.EVALUATE) { 245 // Odd case. Evaluation probably took a long time. Let user ask for it again 246 mCurrentState = CalculatorState.INPUT; 247 // TODO: This can happen if the user rotates the screen. 248 // Is this rotate-to-abort behavior correct? Revisit after experimentation. 249 } 250 if (mCurrentState != CalculatorState.INPUT) { 251 setState(CalculatorState.INIT); 252 mEvaluator.requireResult(); 253 } else { 254 redisplayAfterFormulaChange(); 255 } 256 // TODO: We're currently not saving and restoring scroll position. 257 // We probably should. Details may require care to deal with: 258 // - new display size 259 // - slow recomputation if we've scrolled far. 260 } 261 262 @Override 263 protected void onSaveInstanceState(@NonNull Bundle outState) { 264 // If there's an animation in progress, cancel it first to ensure our state is up-to-date. 265 if (mCurrentAnimator != null) { 266 mCurrentAnimator.cancel(); 267 } 268 269 super.onSaveInstanceState(outState); 270 outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal()); 271 ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); 272 try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) { 273 mEvaluator.saveInstanceState(out); 274 } catch (IOException e) { 275 // Impossible; No IO involved. 276 throw new AssertionError("Impossible IO exception", e); 277 } 278 outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray()); 279 } 280 281 // Set the state, updating delete label and display colors. 282 // This restores display positions on moving to INPUT. 283 // But movement/animation for moving to RESULT has already been done. 284 private void setState(CalculatorState state) { 285 if (mCurrentState != state) { 286 if (state == CalculatorState.INPUT) { 287 restoreDisplayPositions(); 288 } 289 mCurrentState = state; 290 291 if (mCurrentState == CalculatorState.RESULT) { 292 // No longer do this for ERROR; allow mistakes to be corrected. 293 mDeleteButton.setVisibility(View.GONE); 294 mClearButton.setVisibility(View.VISIBLE); 295 } else { 296 mDeleteButton.setVisibility(View.VISIBLE); 297 mClearButton.setVisibility(View.GONE); 298 } 299 300 if (mCurrentState == CalculatorState.ERROR) { 301 final int errorColor = getResources().getColor(R.color.calculator_error_color); 302 mFormulaEditText.setTextColor(errorColor); 303 mResult.setTextColor(errorColor); 304 getWindow().setStatusBarColor(errorColor); 305 } else { 306 mFormulaEditText.setTextColor( 307 getResources().getColor(R.color.display_formula_text_color)); 308 mResult.setTextColor( 309 getResources().getColor(R.color.display_result_text_color)); 310 getWindow().setStatusBarColor( 311 getResources().getColor(R.color.calculator_accent_color)); 312 } 313 314 invalidateOptionsMenu(); 315 } 316 } 317 318 @Override 319 public void onBackPressed() { 320 if (mPadViewPager == null || mPadViewPager.getCurrentItem() == 0) { 321 // If the user is currently looking at the first pad (or the pad is not paged), 322 // allow the system to handle the Back button. 323 super.onBackPressed(); 324 } else { 325 // Otherwise, select the previous pad. 326 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1); 327 } 328 } 329 330 @Override 331 public void onUserInteraction() { 332 super.onUserInteraction(); 333 334 // If there's an animation in progress, cancel it so the user interaction can be handled 335 // immediately. 336 if (mCurrentAnimator != null) { 337 mCurrentAnimator.cancel(); 338 } 339 } 340 341 // Update the top corner degree/radian display and mode button 342 // to reflect the indicated current degree mode (true = degrees) 343 // TODO: Hide the top corner display until the advanced panel is exposed. 344 private void updateDegreeMode(boolean dm) { 345 if (dm) { 346 mModeView.setText(R.string.mode_deg); 347 mModeButton.setText(R.string.mode_rad); 348 mModeButton.setContentDescription(getString(R.string.desc_mode_rad)); 349 } else { 350 mModeView.setText(R.string.mode_rad); 351 mModeButton.setText(R.string.mode_deg); 352 mModeButton.setContentDescription(getString(R.string.desc_mode_deg)); 353 } 354 } 355 356 // Add the given button id to input expression. 357 // If appropriate, clear the expression before doing so. 358 private void addKeyToExpr(int id) { 359 if (mCurrentState == CalculatorState.ERROR) { 360 setState(CalculatorState.INPUT); 361 } else if (mCurrentState == CalculatorState.RESULT) { 362 if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) { 363 mEvaluator.collapse(); 364 } else { 365 mEvaluator.clear(); 366 } 367 setState(CalculatorState.INPUT); 368 } 369 if (!mEvaluator.append(id)) { 370 // TODO: Some user visible feedback? 371 } 372 } 373 374 private void redisplayAfterFormulaChange() { 375 // TODO: Could do this more incrementally. 376 redisplayFormula(); 377 setState(CalculatorState.INPUT); 378 mResult.clear(); 379 mEvaluator.evaluateAndShowResult(); 380 } 381 382 public void onButtonClick(View view) { 383 mCurrentButton = view; 384 385 // Always cancel in-progress evaluation. 386 // If we were waiting for the result, do nothing else. 387 mEvaluator.cancelAll(); 388 389 if (mCurrentState == CalculatorState.EVALUATE 390 || mCurrentState == CalculatorState.ANIMATE) { 391 onCancelled(); 392 return; 393 } 394 395 396 final int id = view.getId(); 397 switch (id) { 398 case R.id.eq: 399 onEquals(); 400 break; 401 case R.id.del: 402 onDelete(); 403 break; 404 case R.id.clr: 405 onClear(); 406 break; 407 case R.id.mode_deg_rad: 408 boolean mode = !mEvaluator.getDegreeMode(); 409 updateDegreeMode(mode); 410 if (mCurrentState == CalculatorState.RESULT) { 411 mEvaluator.collapse(); // Capture result evaluated in old mode 412 redisplayFormula(); 413 } 414 // In input mode, we reinterpret already entered trig functions. 415 mEvaluator.setDegreeMode(mode); 416 setState(CalculatorState.INPUT); 417 mResult.clear(); 418 mEvaluator.evaluateAndShowResult(); 419 break; 420 default: 421 addKeyToExpr(id); 422 redisplayAfterFormulaChange(); 423 break; 424 } 425 } 426 427 void redisplayFormula() { 428 String formula = mEvaluator.getExpr().toString(this); 429 if (mUnprocessedChars != null) { 430 // Add and highlight characters we couldn't process. 431 SpannableString formatted = new SpannableString(formula + mUnprocessedChars); 432 // TODO: should probably match this to the error color. 433 formatted.setSpan(new ForegroundColorSpan(Color.RED), 434 formula.length(), formatted.length(), 435 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 436 mFormulaEditText.setText(formatted); 437 } else { 438 mFormulaEditText.setText(formula); 439 } 440 } 441 442 @Override 443 public boolean onLongClick(View view) { 444 mCurrentButton = view; 445 446 if (view.getId() == R.id.del) { 447 onClear(); 448 return true; 449 } 450 return false; 451 } 452 453 // Initial evaluation completed successfully. Initiate display. 454 public void onEvaluate(int initDisplayPrec, String truncatedWholeNumber) { 455 // Invalidate any options that may depend on the current result. 456 invalidateOptionsMenu(); 457 458 if (mCurrentState == CalculatorState.INPUT) { 459 // Just update small result display. 460 mResult.displayResult(initDisplayPrec, truncatedWholeNumber); 461 } else { // in EVALUATE or INIT state 462 mResult.displayResult(initDisplayPrec, truncatedWholeNumber); 463 onResult(mCurrentState != CalculatorState.INIT); 464 } 465 } 466 467 public void onCancelled() { 468 // We should be in EVALUATE state. 469 // Display is still in input state. 470 setState(CalculatorState.INPUT); 471 } 472 473 // Reevaluation completed; ask result to redisplay current value. 474 public void onReevaluate() 475 { 476 mResult.redisplay(); 477 } 478 479 @Override 480 public void onTextSizeChanged(final TextView textView, float oldSize) { 481 if (mCurrentState != CalculatorState.INPUT) { 482 // Only animate text changes that occur from user input. 483 return; 484 } 485 486 // Calculate the values needed to perform the scale and translation animations, 487 // maintaining the same apparent baseline for the displayed text. 488 final float textScale = oldSize / textView.getTextSize(); 489 final float translationX = (1.0f - textScale) * 490 (textView.getWidth() / 2.0f - textView.getPaddingEnd()); 491 final float translationY = (1.0f - textScale) * 492 (textView.getHeight() / 2.0f - textView.getPaddingBottom()); 493 494 final AnimatorSet animatorSet = new AnimatorSet(); 495 animatorSet.playTogether( 496 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f), 497 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f), 498 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f), 499 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f)); 500 animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime)); 501 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 502 animatorSet.start(); 503 } 504 505 private void onEquals() { 506 if (mCurrentState == CalculatorState.INPUT) { 507 setState(CalculatorState.EVALUATE); 508 mEvaluator.requireResult(); 509 } 510 } 511 512 private void onDelete() { 513 // Delete works like backspace; remove the last character or operator from the expression. 514 // Note that we handle keyboard delete exactly like the delete button. For 515 // example the delete button can be used to delete a character from an incomplete 516 // function name typed on a physical keyboard. 517 mEvaluator.cancelAll(); 518 // This should be impossible in RESULT state. 519 setState(CalculatorState.INPUT); 520 if (mUnprocessedChars != null) { 521 int len = mUnprocessedChars.length(); 522 if (len > 0) { 523 mUnprocessedChars = mUnprocessedChars.substring(0, len-1); 524 } else { 525 mEvaluator.getExpr().delete(); 526 } 527 } else { 528 mEvaluator.getExpr().delete(); 529 } 530 redisplayAfterFormulaChange(); 531 } 532 533 private void reveal(View sourceView, int colorRes, AnimatorListener listener) { 534 final ViewGroupOverlay groupOverlay = 535 (ViewGroupOverlay) getWindow().getDecorView().getOverlay(); 536 537 final Rect displayRect = new Rect(); 538 mDisplayView.getGlobalVisibleRect(displayRect); 539 540 // Make reveal cover the display and status bar. 541 final View revealView = new View(this); 542 revealView.setBottom(displayRect.bottom); 543 revealView.setLeft(displayRect.left); 544 revealView.setRight(displayRect.right); 545 revealView.setBackgroundColor(getResources().getColor(colorRes)); 546 groupOverlay.add(revealView); 547 548 final int[] clearLocation = new int[2]; 549 sourceView.getLocationInWindow(clearLocation); 550 clearLocation[0] += sourceView.getWidth() / 2; 551 clearLocation[1] += sourceView.getHeight() / 2; 552 553 final int revealCenterX = clearLocation[0] - revealView.getLeft(); 554 final int revealCenterY = clearLocation[1] - revealView.getTop(); 555 556 final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2); 557 final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2); 558 final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2); 559 final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2)); 560 561 final Animator revealAnimator = 562 ViewAnimationUtils.createCircularReveal(revealView, 563 revealCenterX, revealCenterY, 0.0f, revealRadius); 564 revealAnimator.setDuration( 565 getResources().getInteger(android.R.integer.config_longAnimTime)); 566 revealAnimator.addListener(listener); 567 568 final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f); 569 alphaAnimator.setDuration( 570 getResources().getInteger(android.R.integer.config_mediumAnimTime)); 571 572 final AnimatorSet animatorSet = new AnimatorSet(); 573 animatorSet.play(revealAnimator).before(alphaAnimator); 574 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 575 animatorSet.addListener(new AnimatorListenerAdapter() { 576 @Override 577 public void onAnimationEnd(Animator animator) { 578 groupOverlay.remove(revealView); 579 mCurrentAnimator = null; 580 } 581 }); 582 583 mCurrentAnimator = animatorSet; 584 animatorSet.start(); 585 } 586 587 private void onClear() { 588 if (mEvaluator.getExpr().isEmpty()) { 589 return; 590 } 591 mUnprocessedChars = null; 592 mResult.clear(); 593 mEvaluator.clear(); 594 reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() { 595 @Override 596 public void onAnimationEnd(Animator animation) { 597 redisplayFormula(); 598 } 599 }); 600 } 601 602 // Evaluation encountered en error. Display the error. 603 void onError(final int errorResourceId) { 604 if (mCurrentState != CalculatorState.EVALUATE) { 605 // Only animate error on evaluate. 606 return; 607 } 608 609 setState(CalculatorState.ANIMATE); 610 reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() { 611 @Override 612 public void onAnimationEnd(Animator animation) { 613 setState(CalculatorState.ERROR); 614 mResult.displayError(errorResourceId); 615 } 616 }); 617 } 618 619 620 // Animate movement of result into the top formula slot. 621 // Result window now remains translated in the top slot while the result is displayed. 622 // (We convert it back to formula use only when the user provides new input.) 623 // Historical note: In the Lollipop version, this invisibly and instantaeously moved 624 // formula and result displays back at the end of the animation. We no longer do that, 625 // so that we can continue to properly support scrolling of the result. 626 // We assume the result already contains the text to be expanded. 627 private void onResult(boolean animate) { 628 // Calculate the values needed to perform the scale and translation animations. 629 // We now fix the character size in the display to avoid weird effects 630 // when we scroll. 631 // Display.xml is designed to ensure exactly a 3/2 ratio between the formula 632 // slot and small result slot. 633 final float resultScale = 1.5f; 634 final float resultTranslationX = -mResult.getWidth() * (resultScale - 1)/2; 635 // mFormulaEditText is aligned with mResult on the right. 636 // When we enlarge it around its center, the right side 637 // moves to the right. This compensates. 638 float resultTranslationY = -mResult.getHeight(); 639 // This is how much we want to move the bottom. 640 // Now compensate for the fact that we're 641 // simultaenously expanding it around its center by half its height 642 resultTranslationY += mResult.getHeight() * (resultScale-1)/2; 643 final float formulaTranslationY = -mFormulaEditText.getBottom(); 644 645 // TODO: Reintroduce textColorAnimator? 646 // The initial and final colors seemed to be the same in L. 647 // With the new model, the result logically changes back to a formula 648 // only when we switch back to INPUT state, so it's unclear that animating 649 // a color change here makes sense. 650 if (animate) { 651 final AnimatorSet animatorSet = new AnimatorSet(); 652 animatorSet.playTogether( 653 ObjectAnimator.ofFloat(mResult, View.SCALE_X, resultScale), 654 ObjectAnimator.ofFloat(mResult, View.SCALE_Y, resultScale), 655 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_X, resultTranslationX), 656 ObjectAnimator.ofFloat(mResult, View.TRANSLATION_Y, resultTranslationY), 657 ObjectAnimator.ofFloat(mFormulaEditText, View.TRANSLATION_Y, 658 formulaTranslationY)); 659 animatorSet.setDuration( 660 getResources().getInteger(android.R.integer.config_longAnimTime)); 661 animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); 662 animatorSet.addListener(new AnimatorListenerAdapter() { 663 @Override 664 public void onAnimationStart(Animator animation) { 665 // Result should already be displayed; no need to do anything. 666 } 667 668 @Override 669 public void onAnimationEnd(Animator animation) { 670 setState(CalculatorState.RESULT); 671 mCurrentAnimator = null; 672 } 673 }); 674 675 mCurrentAnimator = animatorSet; 676 animatorSet.start(); 677 } else /* No animation desired; get there fast, e.g. when restarting */ { 678 mResult.setScaleX(resultScale); 679 mResult.setScaleY(resultScale); 680 mResult.setTranslationX(resultTranslationX); 681 mResult.setTranslationY(resultTranslationY); 682 mFormulaEditText.setTranslationY(formulaTranslationY); 683 setState(CalculatorState.RESULT); 684 } 685 } 686 687 // Restore positions of the formula and result displays back to their original, 688 // pre-animation state. 689 private void restoreDisplayPositions() { 690 // Clear result. 691 mResult.setText(""); 692 // Reset all of the values modified during the animation. 693 mResult.setScaleX(1.0f); 694 mResult.setScaleY(1.0f); 695 mResult.setTranslationX(0.0f); 696 mResult.setTranslationY(0.0f); 697 mFormulaEditText.setTranslationY(0.0f); 698 699 mFormulaEditText.requestFocus(); 700 } 701 702 @Override 703 public boolean onCreateOptionsMenu(Menu menu) { 704 getMenuInflater().inflate(R.menu.overflow, menu); 705 return true; 706 } 707 708 @Override 709 public boolean onPrepareOptionsMenu(Menu menu) { 710 if (mCurrentState != CalculatorState.RESULT) { 711 menu.findItem(R.id.menu_fraction).setEnabled(false); 712 menu.findItem(R.id.menu_leading).setEnabled(false); 713 } else if (mEvaluator.getRational() == null) { 714 menu.findItem(R.id.menu_fraction).setEnabled(false); 715 } 716 return true; 717 } 718 719 @Override 720 public boolean onOptionsItemSelected(MenuItem item) { 721 switch (item.getItemId()) { 722 case R.id.menu_help: 723 displayHelpMessage(); 724 return true; 725 case R.id.menu_about: 726 displayAboutPage(); 727 return true; 728 case R.id.menu_fraction: 729 displayFraction(); 730 return true; 731 case R.id.menu_leading: 732 displayFull(); 733 return true; 734 default: 735 return super.onOptionsItemSelected(item); 736 } 737 } 738 739 private void displayMessage(String s) { 740 AlertDialog.Builder builder = new AlertDialog.Builder(this); 741 builder.setMessage(s) 742 .setNegativeButton(R.string.dismiss, 743 new DialogInterface.OnClickListener() { 744 public void onClick(DialogInterface d, int which) { } 745 }) 746 .show(); 747 } 748 749 private void displayHelpMessage() { 750 Resources res = getResources(); 751 String msg = res.getString(R.string.help_message); 752 if (mPadViewPager != null) { 753 msg += res.getString(R.string.help_pager); 754 } 755 displayMessage(msg); 756 } 757 758 private void displayFraction() { 759 BoundedRational result = mEvaluator.getRational(); 760 displayMessage(KeyMaps.translateResult(result.toNiceString())); 761 } 762 763 // Display full result to currently evaluated precision 764 private void displayFull() { 765 Resources res = getResources(); 766 String msg = mResult.getFullText() + " "; 767 if (mResult.fullTextIsExact()) { 768 msg += res.getString(R.string.exact); 769 } else { 770 msg += res.getString(R.string.approximate); 771 } 772 displayMessage(msg); 773 } 774 775 private void displayAboutPage() { 776 WebView wv = new WebView(this); 777 wv.loadUrl("file:///android_asset/about.txt"); 778 new AlertDialog.Builder(this) 779 .setView(wv) 780 .setNegativeButton(R.string.dismiss, 781 new DialogInterface.OnClickListener() { 782 public void onClick(DialogInterface d, int which) { } 783 }) 784 .show(); 785 } 786 787 // Add input characters to the end of the expression by mapping them to 788 // the appropriate button pushes when possible. Leftover characters 789 // are added to mUnprocessedChars, which is presumed to immediately 790 // precede the newly added characters. 791 private void addChars(String moreChars) { 792 if (mUnprocessedChars != null) { 793 moreChars = mUnprocessedChars + moreChars; 794 } 795 int current = 0; 796 int len = moreChars.length(); 797 while (current < len) { 798 char c = moreChars.charAt(current); 799 int k = KeyMaps.keyForChar(c); 800 if (k != View.NO_ID) { 801 mCurrentButton = findViewById(k); 802 addKeyToExpr(k); 803 if (Character.isSurrogate(c)) { 804 current += 2; 805 } else { 806 ++current; 807 } 808 continue; 809 } 810 int f = KeyMaps.funForString(moreChars, current); 811 if (f != View.NO_ID) { 812 mCurrentButton = findViewById(f); 813 addKeyToExpr(f); 814 if (f == R.id.op_sqrt) { 815 // Square root entered as function; don't lose the parenthesis. 816 addKeyToExpr(R.id.lparen); 817 } 818 current = moreChars.indexOf('(', current) + 1; 819 continue; 820 } 821 // There are characters left, but we can't convert them to button presses. 822 mUnprocessedChars = moreChars.substring(current); 823 redisplayAfterFormulaChange(); 824 return; 825 } 826 mUnprocessedChars = null; 827 redisplayAfterFormulaChange(); 828 return; 829 } 830 831 @Override 832 public boolean paste(Uri uri) { 833 if (mEvaluator.isLastSaved(uri)) { 834 if (mCurrentState == CalculatorState.ERROR 835 || mCurrentState == CalculatorState.RESULT) { 836 setState(CalculatorState.INPUT); 837 mEvaluator.clear(); 838 } 839 mEvaluator.addSaved(); 840 redisplayAfterFormulaChange(); 841 return true; 842 } 843 return false; 844 } 845 846 @Override 847 public void paste(String s) { 848 addChars(s); 849 } 850 851} 852