Home | History | Annotate | Download | only in calculator2
      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 // TODO: Copy & more general paste in formula?  Note that this requires
     18 //       great care: Currently the text version of a displayed formula
     19 //       is not directly useful for re-evaluating the formula later, since
     20 //       it contains ellipses representing subexpressions evaluated with
     21 //       a different degree mode.  Rather than supporting copy from the
     22 //       formula window, we may eventually want to support generation of a
     23 //       more useful text version in a separate window.  It's not clear
     24 //       this is worth the added (code and user) complexity.
     25 
     26 package com.android.calculator2;
     27 
     28 import android.animation.Animator;
     29 import android.animation.Animator.AnimatorListener;
     30 import android.animation.AnimatorListenerAdapter;
     31 import android.animation.AnimatorSet;
     32 import android.animation.ObjectAnimator;
     33 import android.animation.PropertyValuesHolder;
     34 import android.app.Activity;
     35 import android.content.ClipData;
     36 import android.content.Intent;
     37 import android.content.res.Resources;
     38 import android.graphics.Color;
     39 import android.graphics.Rect;
     40 import android.net.Uri;
     41 import android.os.Bundle;
     42 import android.support.annotation.NonNull;
     43 import android.support.v4.view.ViewPager;
     44 import android.text.SpannableString;
     45 import android.text.SpannableStringBuilder;
     46 import android.text.Spanned;
     47 import android.text.style.ForegroundColorSpan;
     48 import android.text.TextUtils;
     49 import android.util.Property;
     50 import android.view.KeyCharacterMap;
     51 import android.view.KeyEvent;
     52 import android.view.Menu;
     53 import android.view.MenuItem;
     54 import android.view.View;
     55 import android.view.View.OnKeyListener;
     56 import android.view.View.OnLongClickListener;
     57 import android.view.ViewAnimationUtils;
     58 import android.view.ViewGroupOverlay;
     59 import android.view.animation.AccelerateDecelerateInterpolator;
     60 import android.widget.TextView;
     61 import android.widget.Toolbar;
     62 
     63 import com.android.calculator2.CalculatorText.OnTextSizeChangeListener;
     64 
     65 import java.io.ByteArrayInputStream;
     66 import java.io.ByteArrayOutputStream;
     67 import java.io.IOException;
     68 import java.io.ObjectInput;
     69 import java.io.ObjectInputStream;
     70 import java.io.ObjectOutput;
     71 import java.io.ObjectOutputStream;
     72 
     73 public class Calculator extends Activity
     74         implements OnTextSizeChangeListener, OnLongClickListener, CalculatorText.OnPasteListener {
     75 
     76     /**
     77      * Constant for an invalid resource id.
     78      */
     79     public static final int INVALID_RES_ID = -1;
     80 
     81     private enum CalculatorState {
     82         INPUT,          // Result and formula both visible, no evaluation requested,
     83                         // Though result may be visible on bottom line.
     84         EVALUATE,       // Both visible, evaluation requested, evaluation/animation incomplete.
     85                         // Not used for instant result evaluation.
     86         INIT,           // Very temporary state used as alternative to EVALUATE
     87                         // during reinitialization.  Do not animate on completion.
     88         ANIMATE,        // Result computed, animation to enlarge result window in progress.
     89         RESULT,         // Result displayed, formula invisible.
     90                         // If we are in RESULT state, the formula was evaluated without
     91                         // error to initial precision.
     92         ERROR           // Error displayed: Formula visible, result shows error message.
     93                         // Display similar to INPUT state.
     94     }
     95     // Normal transition sequence is
     96     // INPUT -> EVALUATE -> ANIMATE -> RESULT (or ERROR) -> INPUT
     97     // A RESULT -> ERROR transition is possible in rare corner cases, in which
     98     // a higher precision evaluation exposes an error.  This is possible, since we
     99     // initially evaluate assuming we were given a well-defined problem.  If we
    100     // were actually asked to compute sqrt(<extremely tiny negative number>) we produce 0
    101     // unless we are asked for enough precision that we can distinguish the argument from zero.
    102     // TODO: Consider further heuristics to reduce the chance of observing this?
    103     //       It already seems to be observable only in contrived cases.
    104     // ANIMATE, ERROR, and RESULT are translated to an INIT state if the application
    105     // is restarted in that state.  This leads us to recompute and redisplay the result
    106     // ASAP.
    107     // TODO: Possibly save a bit more information, e.g. its initial display string
    108     // or most significant digit position, to speed up restart.
    109 
    110     private final Property<TextView, Integer> TEXT_COLOR =
    111             new Property<TextView, Integer>(Integer.class, "textColor") {
    112         @Override
    113         public Integer get(TextView textView) {
    114             return textView.getCurrentTextColor();
    115         }
    116 
    117         @Override
    118         public void set(TextView textView, Integer textColor) {
    119             textView.setTextColor(textColor);
    120         }
    121     };
    122 
    123     // We currently assume that the formula does not change out from under us in
    124     // any way. We explicitly handle all input to the formula here.
    125     private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
    126         @Override
    127         public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
    128             stopActionMode();
    129             // Never consume DPAD key events.
    130             switch (keyCode) {
    131                 case KeyEvent.KEYCODE_DPAD_UP:
    132                 case KeyEvent.KEYCODE_DPAD_DOWN:
    133                 case KeyEvent.KEYCODE_DPAD_LEFT:
    134                 case KeyEvent.KEYCODE_DPAD_RIGHT:
    135                     return false;
    136             }
    137             // Always cancel unrequested in-progress evaluation, so that we don't have
    138             // to worry about subsequent asynchronous completion.
    139             // Requested in-progress evaluations are handled below.
    140             if (mCurrentState != CalculatorState.EVALUATE) {
    141                 mEvaluator.cancelAll(true);
    142             }
    143             // In other cases we go ahead and process the input normally after cancelling:
    144             if (keyEvent.getAction() != KeyEvent.ACTION_UP) {
    145                 return true;
    146             }
    147             switch (keyCode) {
    148                 case KeyEvent.KEYCODE_NUMPAD_ENTER:
    149                 case KeyEvent.KEYCODE_ENTER:
    150                 case KeyEvent.KEYCODE_DPAD_CENTER:
    151                     mCurrentButton = mEqualButton;
    152                     onEquals();
    153                     return true;
    154                 case KeyEvent.KEYCODE_DEL:
    155                     mCurrentButton = mDeleteButton;
    156                     onDelete();
    157                     return true;
    158                 default:
    159                     cancelIfEvaluating(false);
    160                     final int raw = keyEvent.getKeyCharacterMap()
    161                             .get(keyCode, keyEvent.getMetaState());
    162                     if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
    163                         return true; // discard
    164                     }
    165                     // Try to discard non-printing characters and the like.
    166                     // The user will have to explicitly delete other junk that gets past us.
    167                     if (Character.isIdentifierIgnorable(raw)
    168                             || Character.isWhitespace(raw)) {
    169                         return true;
    170                     }
    171                     char c = (char) raw;
    172                     if (c == '=') {
    173                         mCurrentButton = mEqualButton;
    174                         onEquals();
    175                     } else {
    176                         addChars(String.valueOf(c), true);
    177                         redisplayAfterFormulaChange();
    178                     }
    179             }
    180             return false;
    181         }
    182     };
    183 
    184     private static final String NAME = Calculator.class.getName();
    185     private static final String KEY_DISPLAY_STATE = NAME + "_display_state";
    186     private static final String KEY_UNPROCESSED_CHARS = NAME + "_unprocessed_chars";
    187     private static final String KEY_EVAL_STATE = NAME + "_eval_state";
    188                 // Associated value is a byte array holding both mCalculatorState
    189                 // and the (much more complex) evaluator state.
    190 
    191     private CalculatorState mCurrentState;
    192     private Evaluator mEvaluator;
    193 
    194     private View mDisplayView;
    195     private TextView mModeView;
    196     private CalculatorText mFormulaText;
    197     private CalculatorResult mResultText;
    198 
    199     private ViewPager mPadViewPager;
    200     private View mDeleteButton;
    201     private View mClearButton;
    202     private View mEqualButton;
    203 
    204     private TextView mInverseToggle;
    205     private TextView mModeToggle;
    206 
    207     private View[] mInvertibleButtons;
    208     private View[] mInverseButtons;
    209 
    210     private View mCurrentButton;
    211     private Animator mCurrentAnimator;
    212 
    213     // Characters that were recently entered at the end of the display that have not yet
    214     // been added to the underlying expression.
    215     private String mUnprocessedChars = null;
    216 
    217     // Color to highlight unprocessed characters from physical keyboard.
    218     // TODO: should probably match this to the error color?
    219     private ForegroundColorSpan mUnprocessedColorSpan = new ForegroundColorSpan(Color.RED);
    220 
    221     @Override
    222     protected void onCreate(Bundle savedInstanceState) {
    223         super.onCreate(savedInstanceState);
    224         setContentView(R.layout.activity_calculator);
    225         setActionBar((Toolbar) findViewById(R.id.toolbar));
    226 
    227         // Hide all default options in the ActionBar.
    228         getActionBar().setDisplayOptions(0);
    229 
    230         mDisplayView = findViewById(R.id.display);
    231         mModeView = (TextView) findViewById(R.id.mode);
    232         mFormulaText = (CalculatorText) findViewById(R.id.formula);
    233         mResultText = (CalculatorResult) findViewById(R.id.result);
    234 
    235         mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
    236         mDeleteButton = findViewById(R.id.del);
    237         mClearButton = findViewById(R.id.clr);
    238         mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
    239         if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
    240             mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
    241         }
    242 
    243         mInverseToggle = (TextView) findViewById(R.id.toggle_inv);
    244         mModeToggle = (TextView) findViewById(R.id.toggle_mode);
    245 
    246         mInvertibleButtons = new View[] {
    247                 findViewById(R.id.fun_sin),
    248                 findViewById(R.id.fun_cos),
    249                 findViewById(R.id.fun_tan),
    250                 findViewById(R.id.fun_ln),
    251                 findViewById(R.id.fun_log),
    252                 findViewById(R.id.op_sqrt)
    253         };
    254         mInverseButtons = new View[] {
    255                 findViewById(R.id.fun_arcsin),
    256                 findViewById(R.id.fun_arccos),
    257                 findViewById(R.id.fun_arctan),
    258                 findViewById(R.id.fun_exp),
    259                 findViewById(R.id.fun_10pow),
    260                 findViewById(R.id.op_sqr)
    261         };
    262 
    263         mEvaluator = new Evaluator(this, mResultText);
    264         mResultText.setEvaluator(mEvaluator);
    265         KeyMaps.setActivity(this);
    266 
    267         if (savedInstanceState != null) {
    268             setState(CalculatorState.values()[
    269                 savedInstanceState.getInt(KEY_DISPLAY_STATE,
    270                                           CalculatorState.INPUT.ordinal())]);
    271             CharSequence unprocessed = savedInstanceState.getCharSequence(KEY_UNPROCESSED_CHARS);
    272             if (unprocessed != null) {
    273                 mUnprocessedChars = unprocessed.toString();
    274             }
    275             byte[] state = savedInstanceState.getByteArray(KEY_EVAL_STATE);
    276             if (state != null) {
    277                 try (ObjectInput in = new ObjectInputStream(new ByteArrayInputStream(state))) {
    278                     mEvaluator.restoreInstanceState(in);
    279                 } catch (Throwable ignored) {
    280                     // When in doubt, revert to clean state
    281                     mCurrentState = CalculatorState.INPUT;
    282                     mEvaluator.clear();
    283                 }
    284             }
    285         } else {
    286             mCurrentState = CalculatorState.INPUT;
    287             mEvaluator.clear();
    288         }
    289 
    290         mFormulaText.setOnKeyListener(mFormulaOnKeyListener);
    291         mFormulaText.setOnTextSizeChangeListener(this);
    292         mFormulaText.setOnPasteListener(this);
    293         mDeleteButton.setOnLongClickListener(this);
    294 
    295         onInverseToggled(mInverseToggle.isSelected());
    296         onModeChanged(mEvaluator.getDegreeMode());
    297 
    298         if (mCurrentState != CalculatorState.INPUT) {
    299             // Just reevaluate.
    300             redisplayFormula();
    301             setState(CalculatorState.INIT);
    302             mEvaluator.requireResult();
    303         } else {
    304             redisplayAfterFormulaChange();
    305         }
    306         // TODO: We're currently not saving and restoring scroll position.
    307         //       We probably should.  Details may require care to deal with:
    308         //         - new display size
    309         //         - slow recomputation if we've scrolled far.
    310     }
    311 
    312     @Override
    313     protected void onSaveInstanceState(@NonNull Bundle outState) {
    314         // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
    315         if (mCurrentAnimator != null) {
    316             mCurrentAnimator.cancel();
    317         }
    318 
    319         super.onSaveInstanceState(outState);
    320         outState.putInt(KEY_DISPLAY_STATE, mCurrentState.ordinal());
    321         outState.putCharSequence(KEY_UNPROCESSED_CHARS, mUnprocessedChars);
    322         ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
    323         try (ObjectOutput out = new ObjectOutputStream(byteArrayStream)) {
    324             mEvaluator.saveInstanceState(out);
    325         } catch (IOException e) {
    326             // Impossible; No IO involved.
    327             throw new AssertionError("Impossible IO exception", e);
    328         }
    329         outState.putByteArray(KEY_EVAL_STATE, byteArrayStream.toByteArray());
    330     }
    331 
    332     // Set the state, updating delete label and display colors.
    333     // This restores display positions on moving to INPUT.
    334     // But movement/animation for moving to RESULT has already been done.
    335     private void setState(CalculatorState state) {
    336         if (mCurrentState != state) {
    337             if (state == CalculatorState.INPUT) {
    338                 restoreDisplayPositions();
    339             }
    340             mCurrentState = state;
    341 
    342             if (mCurrentState == CalculatorState.RESULT) {
    343                 // No longer do this for ERROR; allow mistakes to be corrected.
    344                 mDeleteButton.setVisibility(View.GONE);
    345                 mClearButton.setVisibility(View.VISIBLE);
    346             } else {
    347                 mDeleteButton.setVisibility(View.VISIBLE);
    348                 mClearButton.setVisibility(View.GONE);
    349             }
    350 
    351             if (mCurrentState == CalculatorState.ERROR) {
    352                 final int errorColor = getColor(R.color.calculator_error_color);
    353                 mFormulaText.setTextColor(errorColor);
    354                 mResultText.setTextColor(errorColor);
    355                 getWindow().setStatusBarColor(errorColor);
    356             } else if (mCurrentState != CalculatorState.RESULT) {
    357                 mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
    358                 mResultText.setTextColor(getColor(R.color.display_result_text_color));
    359                 getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
    360             }
    361 
    362             invalidateOptionsMenu();
    363         }
    364     }
    365 
    366     // Stop any active ActionMode.  Return true if there was one.
    367     private boolean stopActionMode() {
    368         if (mResultText.stopActionMode()) {
    369             return true;
    370         }
    371         if (mFormulaText.stopActionMode()) {
    372             return true;
    373         }
    374         return false;
    375     }
    376 
    377     @Override
    378     public void onBackPressed() {
    379         if (!stopActionMode()) {
    380             if (mPadViewPager != null && mPadViewPager.getCurrentItem() != 0) {
    381                 // Select the previous pad.
    382                 mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
    383             } else {
    384                 // If the user is currently looking at the first pad (or the pad is not paged),
    385                 // allow the system to handle the Back button.
    386                 super.onBackPressed();
    387             }
    388         }
    389     }
    390 
    391     @Override
    392     public void onUserInteraction() {
    393         super.onUserInteraction();
    394 
    395         // If there's an animation in progress, end it immediately, so the user interaction can
    396         // be handled.
    397         if (mCurrentAnimator != null) {
    398             mCurrentAnimator.end();
    399         }
    400     }
    401 
    402     /**
    403      * Invoked whenever the inverse button is toggled to update the UI.
    404      *
    405      * @param showInverse {@code true} if inverse functions should be shown
    406      */
    407     private void onInverseToggled(boolean showInverse) {
    408         if (showInverse) {
    409             mInverseToggle.setContentDescription(getString(R.string.desc_inv_on));
    410             for (View invertibleButton : mInvertibleButtons) {
    411                 invertibleButton.setVisibility(View.GONE);
    412             }
    413             for (View inverseButton : mInverseButtons) {
    414                 inverseButton.setVisibility(View.VISIBLE);
    415             }
    416         } else {
    417             mInverseToggle.setContentDescription(getString(R.string.desc_inv_off));
    418             for (View invertibleButton : mInvertibleButtons) {
    419                 invertibleButton.setVisibility(View.VISIBLE);
    420             }
    421             for (View inverseButton : mInverseButtons) {
    422                 inverseButton.setVisibility(View.GONE);
    423             }
    424         }
    425     }
    426 
    427     /**
    428      * Invoked whenever the deg/rad mode may have changed to update the UI.
    429      *
    430      * @param degreeMode {@code true} if in degree mode
    431      */
    432     private void onModeChanged(boolean degreeMode) {
    433         if (degreeMode) {
    434             mModeView.setText(R.string.mode_deg);
    435             mModeView.setContentDescription(getString(R.string.desc_mode_deg));
    436 
    437             mModeToggle.setText(R.string.mode_rad);
    438             mModeToggle.setContentDescription(getString(R.string.desc_switch_rad));
    439         } else {
    440             mModeView.setText(R.string.mode_rad);
    441             mModeView.setContentDescription(getString(R.string.desc_mode_rad));
    442 
    443             mModeToggle.setText(R.string.mode_deg);
    444             mModeToggle.setContentDescription(getString(R.string.desc_switch_deg));
    445         }
    446     }
    447 
    448     // Add the given button id to input expression.
    449     // If appropriate, clear the expression before doing so.
    450     private void addKeyToExpr(int id) {
    451         if (mCurrentState == CalculatorState.ERROR) {
    452             setState(CalculatorState.INPUT);
    453         } else if (mCurrentState == CalculatorState.RESULT) {
    454             if (KeyMaps.isBinary(id) || KeyMaps.isSuffix(id)) {
    455                 mEvaluator.collapse();
    456             } else {
    457                 announceClearForAccessibility();
    458                 mEvaluator.clear();
    459             }
    460             setState(CalculatorState.INPUT);
    461         }
    462         if (!mEvaluator.append(id)) {
    463             // TODO: Some user visible feedback?
    464         }
    465     }
    466 
    467     /**
    468      * Add the given button id to input expression, assuming it was explicitly
    469      * typed/touched.
    470      * We perform slightly more aggressive correction than in pasted expressions.
    471      */
    472     private void addExplicitKeyToExpr(int id) {
    473         if (mCurrentState == CalculatorState.INPUT && id == R.id.op_sub) {
    474             mEvaluator.getExpr().removeTrailingAdditiveOperators();
    475         }
    476         addKeyToExpr(id);
    477     }
    478 
    479     private void redisplayAfterFormulaChange() {
    480         // TODO: Could do this more incrementally.
    481         redisplayFormula();
    482         setState(CalculatorState.INPUT);
    483         if (mEvaluator.getExpr().hasInterestingOps()) {
    484             mEvaluator.evaluateAndShowResult();
    485         } else {
    486             mResultText.clear();
    487         }
    488     }
    489 
    490     public void onButtonClick(View view) {
    491         // Any animation is ended before we get here.
    492         mCurrentButton = view;
    493         stopActionMode();
    494         // See onKey above for the rationale behind some of the behavior below:
    495         if (mCurrentState != CalculatorState.EVALUATE) {
    496             // Cancel evaluations that were not specifically requested.
    497             mEvaluator.cancelAll(true);
    498         }
    499         final int id = view.getId();
    500         switch (id) {
    501             case R.id.eq:
    502                 onEquals();
    503                 break;
    504             case R.id.del:
    505                 onDelete();
    506                 break;
    507             case R.id.clr:
    508                 onClear();
    509                 break;
    510             case R.id.toggle_inv:
    511                 final boolean selected = !mInverseToggle.isSelected();
    512                 mInverseToggle.setSelected(selected);
    513                 onInverseToggled(selected);
    514                 if (mCurrentState == CalculatorState.RESULT) {
    515                     mResultText.redisplay();   // In case we cancelled reevaluation.
    516                 }
    517                 break;
    518             case R.id.toggle_mode:
    519                 cancelIfEvaluating(false);
    520                 final boolean mode = !mEvaluator.getDegreeMode();
    521                 if (mCurrentState == CalculatorState.RESULT) {
    522                     mEvaluator.collapse();  // Capture result evaluated in old mode
    523                     redisplayFormula();
    524                 }
    525                 // In input mode, we reinterpret already entered trig functions.
    526                 mEvaluator.setDegreeMode(mode);
    527                 onModeChanged(mode);
    528                 setState(CalculatorState.INPUT);
    529                 mResultText.clear();
    530                 if (mEvaluator.getExpr().hasInterestingOps()) {
    531                     mEvaluator.evaluateAndShowResult();
    532                 }
    533                 break;
    534             default:
    535                 cancelIfEvaluating(false);
    536                 addExplicitKeyToExpr(id);
    537                 redisplayAfterFormulaChange();
    538                 break;
    539         }
    540     }
    541 
    542     void redisplayFormula() {
    543         SpannableStringBuilder formula = mEvaluator.getExpr().toSpannableStringBuilder(this);
    544         if (mUnprocessedChars != null) {
    545             // Add and highlight characters we couldn't process.
    546             formula.append(mUnprocessedChars, mUnprocessedColorSpan,
    547                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    548         }
    549         mFormulaText.changeTextTo(formula);
    550     }
    551 
    552     @Override
    553     public boolean onLongClick(View view) {
    554         mCurrentButton = view;
    555 
    556         if (view.getId() == R.id.del) {
    557             onClear();
    558             return true;
    559         }
    560         return false;
    561     }
    562 
    563     // Initial evaluation completed successfully.  Initiate display.
    564     public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
    565             String truncatedWholeNumber) {
    566         // Invalidate any options that may depend on the current result.
    567         invalidateOptionsMenu();
    568 
    569         mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
    570         if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
    571             onResult(mCurrentState != CalculatorState.INIT);
    572         }
    573     }
    574 
    575     // Reset state to reflect evaluator cancellation.  Invoked by evaluator.
    576     public void onCancelled() {
    577         // We should be in EVALUATE state.
    578         setState(CalculatorState.INPUT);
    579         mResultText.clear();
    580     }
    581 
    582     // Reevaluation completed; ask result to redisplay current value.
    583     public void onReevaluate()
    584     {
    585         mResultText.redisplay();
    586     }
    587 
    588     @Override
    589     public void onTextSizeChanged(final TextView textView, float oldSize) {
    590         if (mCurrentState != CalculatorState.INPUT) {
    591             // Only animate text changes that occur from user input.
    592             return;
    593         }
    594 
    595         // Calculate the values needed to perform the scale and translation animations,
    596         // maintaining the same apparent baseline for the displayed text.
    597         final float textScale = oldSize / textView.getTextSize();
    598         final float translationX = (1.0f - textScale) *
    599                 (textView.getWidth() / 2.0f - textView.getPaddingEnd());
    600         final float translationY = (1.0f - textScale) *
    601                 (textView.getHeight() / 2.0f - textView.getPaddingBottom());
    602 
    603         final AnimatorSet animatorSet = new AnimatorSet();
    604         animatorSet.playTogether(
    605                 ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
    606                 ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
    607                 ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
    608                 ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
    609         animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
    610         animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
    611         animatorSet.start();
    612     }
    613 
    614     /**
    615      * Cancel any in-progress explicitly requested evaluations.
    616      * @param quiet suppress pop-up message.  Explicit evaluation can change the expression
    617                     value, and certainly changes the display, so it seems reasonable to warn.
    618      * @return      true if there was such an evaluation
    619      */
    620     private boolean cancelIfEvaluating(boolean quiet) {
    621         if (mCurrentState == CalculatorState.EVALUATE) {
    622             mEvaluator.cancelAll(quiet);
    623             return true;
    624         } else {
    625             return false;
    626         }
    627     }
    628 
    629     private void onEquals() {
    630         // In non-INPUT state assume this was redundant and ignore it.
    631         if (mCurrentState == CalculatorState.INPUT && !mEvaluator.getExpr().isEmpty()) {
    632             setState(CalculatorState.EVALUATE);
    633             mEvaluator.requireResult();
    634         }
    635     }
    636 
    637     private void onDelete() {
    638         // Delete works like backspace; remove the last character or operator from the expression.
    639         // Note that we handle keyboard delete exactly like the delete button.  For
    640         // example the delete button can be used to delete a character from an incomplete
    641         // function name typed on a physical keyboard.
    642         // This should be impossible in RESULT state.
    643         // If there is an in-progress explicit evaluation, just cancel it and return.
    644         if (cancelIfEvaluating(false)) return;
    645         setState(CalculatorState.INPUT);
    646         if (mUnprocessedChars != null) {
    647             int len = mUnprocessedChars.length();
    648             if (len > 0) {
    649                 mUnprocessedChars = mUnprocessedChars.substring(0, len-1);
    650             } else {
    651                 mEvaluator.delete();
    652             }
    653         } else {
    654             mEvaluator.delete();
    655         }
    656         redisplayAfterFormulaChange();
    657     }
    658 
    659     private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
    660         final ViewGroupOverlay groupOverlay =
    661                 (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
    662 
    663         final Rect displayRect = new Rect();
    664         mDisplayView.getGlobalVisibleRect(displayRect);
    665 
    666         // Make reveal cover the display and status bar.
    667         final View revealView = new View(this);
    668         revealView.setBottom(displayRect.bottom);
    669         revealView.setLeft(displayRect.left);
    670         revealView.setRight(displayRect.right);
    671         revealView.setBackgroundColor(getResources().getColor(colorRes));
    672         groupOverlay.add(revealView);
    673 
    674         final int[] clearLocation = new int[2];
    675         sourceView.getLocationInWindow(clearLocation);
    676         clearLocation[0] += sourceView.getWidth() / 2;
    677         clearLocation[1] += sourceView.getHeight() / 2;
    678 
    679         final int revealCenterX = clearLocation[0] - revealView.getLeft();
    680         final int revealCenterY = clearLocation[1] - revealView.getTop();
    681 
    682         final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
    683         final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
    684         final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
    685         final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
    686 
    687         final Animator revealAnimator =
    688                 ViewAnimationUtils.createCircularReveal(revealView,
    689                         revealCenterX, revealCenterY, 0.0f, revealRadius);
    690         revealAnimator.setDuration(
    691                 getResources().getInteger(android.R.integer.config_longAnimTime));
    692         revealAnimator.addListener(listener);
    693 
    694         final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
    695         alphaAnimator.setDuration(
    696                 getResources().getInteger(android.R.integer.config_mediumAnimTime));
    697 
    698         final AnimatorSet animatorSet = new AnimatorSet();
    699         animatorSet.play(revealAnimator).before(alphaAnimator);
    700         animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
    701         animatorSet.addListener(new AnimatorListenerAdapter() {
    702             @Override
    703             public void onAnimationEnd(Animator animator) {
    704                 groupOverlay.remove(revealView);
    705                 mCurrentAnimator = null;
    706             }
    707         });
    708 
    709         mCurrentAnimator = animatorSet;
    710         animatorSet.start();
    711     }
    712 
    713     private void announceClearForAccessibility() {
    714         mResultText.announceForAccessibility(getResources().getString(R.string.desc_clr));
    715     }
    716 
    717     private void onClear() {
    718         if (mEvaluator.getExpr().isEmpty()) {
    719             return;
    720         }
    721         cancelIfEvaluating(true);
    722         announceClearForAccessibility();
    723         reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
    724             @Override
    725             public void onAnimationEnd(Animator animation) {
    726                 mUnprocessedChars = null;
    727                 mResultText.clear();
    728                 mEvaluator.clear();
    729                 setState(CalculatorState.INPUT);
    730                 redisplayFormula();
    731             }
    732         });
    733     }
    734 
    735     // Evaluation encountered en error.  Display the error.
    736     void onError(final int errorResourceId) {
    737         if (mCurrentState == CalculatorState.EVALUATE) {
    738             setState(CalculatorState.ANIMATE);
    739             mResultText.announceForAccessibility(getResources().getString(errorResourceId));
    740             reveal(mCurrentButton, R.color.calculator_error_color,
    741                     new AnimatorListenerAdapter() {
    742                         @Override
    743                         public void onAnimationEnd(Animator animation) {
    744                            setState(CalculatorState.ERROR);
    745                            mResultText.displayError(errorResourceId);
    746                         }
    747                     });
    748         } else if (mCurrentState == CalculatorState.INIT) {
    749             setState(CalculatorState.ERROR);
    750             mResultText.displayError(errorResourceId);
    751         } else {
    752             mResultText.clear();
    753         }
    754     }
    755 
    756 
    757     // Animate movement of result into the top formula slot.
    758     // Result window now remains translated in the top slot while the result is displayed.
    759     // (We convert it back to formula use only when the user provides new input.)
    760     // Historical note: In the Lollipop version, this invisibly and instantaneously moved
    761     // formula and result displays back at the end of the animation.  We no longer do that,
    762     // so that we can continue to properly support scrolling of the result.
    763     // We assume the result already contains the text to be expanded.
    764     private void onResult(boolean animate) {
    765         // Calculate the textSize that would be used to display the result in the formula.
    766         // For scrollable results just use the minimum textSize to maximize the number of digits
    767         // that are visible on screen.
    768         float textSize = mFormulaText.getMinimumTextSize();
    769         if (!mResultText.isScrollable()) {
    770             textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
    771         }
    772 
    773         // Scale the result to match the calculated textSize, minimizing the jump-cut transition
    774         // when a result is reused in a subsequent expression.
    775         final float resultScale = textSize / mResultText.getTextSize();
    776 
    777         // Set the result's pivot to match its gravity.
    778         mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
    779         mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
    780 
    781         // Calculate the necessary translations so the result takes the place of the formula and
    782         // the formula moves off the top of the screen.
    783         final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
    784                 - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
    785         final float formulaTranslationY = -mFormulaText.getBottom();
    786 
    787         // Change the result's textColor to match the formula.
    788         final int formulaTextColor = mFormulaText.getCurrentTextColor();
    789 
    790         if (animate) {
    791             mResultText.announceForAccessibility(getResources().getString(R.string.desc_eq));
    792             mResultText.announceForAccessibility(mResultText.getText());
    793             setState(CalculatorState.ANIMATE);
    794             final AnimatorSet animatorSet = new AnimatorSet();
    795             animatorSet.playTogether(
    796                     ObjectAnimator.ofPropertyValuesHolder(mResultText,
    797                             PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
    798                             PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
    799                             PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
    800                     ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
    801                     ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
    802             animatorSet.setDuration(getResources().getInteger(
    803                     android.R.integer.config_longAnimTime));
    804             animatorSet.addListener(new AnimatorListenerAdapter() {
    805                 @Override
    806                 public void onAnimationEnd(Animator animation) {
    807                     setState(CalculatorState.RESULT);
    808                     mCurrentAnimator = null;
    809                 }
    810             });
    811 
    812             mCurrentAnimator = animatorSet;
    813             animatorSet.start();
    814         } else /* No animation desired; get there fast, e.g. when restarting */ {
    815             mResultText.setScaleX(resultScale);
    816             mResultText.setScaleY(resultScale);
    817             mResultText.setTranslationY(resultTranslationY);
    818             mResultText.setTextColor(formulaTextColor);
    819             mFormulaText.setTranslationY(formulaTranslationY);
    820             setState(CalculatorState.RESULT);
    821         }
    822     }
    823 
    824     // Restore positions of the formula and result displays back to their original,
    825     // pre-animation state.
    826     private void restoreDisplayPositions() {
    827         // Clear result.
    828         mResultText.setText("");
    829         // Reset all of the values modified during the animation.
    830         mResultText.setScaleX(1.0f);
    831         mResultText.setScaleY(1.0f);
    832         mResultText.setTranslationX(0.0f);
    833         mResultText.setTranslationY(0.0f);
    834         mFormulaText.setTranslationY(0.0f);
    835 
    836         mFormulaText.requestFocus();
    837      }
    838 
    839     @Override
    840     public boolean onCreateOptionsMenu(Menu menu) {
    841         super.onCreateOptionsMenu(menu);
    842 
    843         getMenuInflater().inflate(R.menu.activity_calculator, menu);
    844         return true;
    845     }
    846 
    847     @Override
    848     public boolean onPrepareOptionsMenu(Menu menu) {
    849         super.onPrepareOptionsMenu(menu);
    850 
    851         // Show the leading option when displaying a result.
    852         menu.findItem(R.id.menu_leading).setVisible(mCurrentState == CalculatorState.RESULT);
    853 
    854         // Show the fraction option when displaying a rational result.
    855         menu.findItem(R.id.menu_fraction).setVisible(mCurrentState == CalculatorState.RESULT
    856                 && mEvaluator.getRational() != null);
    857 
    858         return true;
    859     }
    860 
    861     @Override
    862     public boolean onOptionsItemSelected(MenuItem item) {
    863         switch (item.getItemId()) {
    864             case R.id.menu_leading:
    865                 displayFull();
    866                 return true;
    867             case R.id.menu_fraction:
    868                 displayFraction();
    869                 return true;
    870             case R.id.menu_licenses:
    871                 startActivity(new Intent(this, Licenses.class));
    872                 return true;
    873             default:
    874                 return super.onOptionsItemSelected(item);
    875         }
    876     }
    877 
    878     private void displayMessage(String s) {
    879         AlertDialogFragment.showMessageDialog(this, s);
    880     }
    881 
    882     private void displayFraction() {
    883         BoundedRational result = mEvaluator.getRational();
    884         displayMessage(KeyMaps.translateResult(result.toNiceString()));
    885     }
    886 
    887     // Display full result to currently evaluated precision
    888     private void displayFull() {
    889         Resources res = getResources();
    890         String msg = mResultText.getFullText() + " ";
    891         if (mResultText.fullTextIsExact()) {
    892             msg += res.getString(R.string.exact);
    893         } else {
    894             msg += res.getString(R.string.approximate);
    895         }
    896         displayMessage(msg);
    897     }
    898 
    899     /**
    900      * Add input characters to the end of the expression.
    901      * Map them to the appropriate button pushes when possible.  Leftover characters
    902      * are added to mUnprocessedChars, which is presumed to immediately precede the newly
    903      * added characters.
    904      * @param moreChars Characters to be added.
    905      * @param explicit These characters were explicitly typed by the user, not pasted.
    906      */
    907     private void addChars(String moreChars, boolean explicit) {
    908         if (mUnprocessedChars != null) {
    909             moreChars = mUnprocessedChars + moreChars;
    910         }
    911         int current = 0;
    912         int len = moreChars.length();
    913         boolean lastWasDigit = false;
    914         while (current < len) {
    915             char c = moreChars.charAt(current);
    916             int k = KeyMaps.keyForChar(c);
    917             if (!explicit) {
    918                 int expEnd;
    919                 if (lastWasDigit && current !=
    920                         (expEnd = Evaluator.exponentEnd(moreChars, current))) {
    921                     // Process scientific notation with 'E' when pasting, in spite of ambiguity
    922                     // with base of natural log.
    923                     // Otherwise the 10^x key is the user's friend.
    924                     mEvaluator.addExponent(moreChars, current, expEnd);
    925                     current = expEnd;
    926                     lastWasDigit = false;
    927                     continue;
    928                 } else {
    929                     boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
    930                     if (current == 0 && (isDigit || k == R.id.dec_point)
    931                             && mEvaluator.getExpr().hasTrailingConstant()) {
    932                         // Refuse to concatenate pasted content to trailing constant.
    933                         // This makes pasting of calculator results more consistent, whether or
    934                         // not the old calculator instance is still around.
    935                         addKeyToExpr(R.id.op_mul);
    936                     }
    937                     lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
    938                 }
    939             }
    940             if (k != View.NO_ID) {
    941                 mCurrentButton = findViewById(k);
    942                 if (explicit) {
    943                     addExplicitKeyToExpr(k);
    944                 } else {
    945                     addKeyToExpr(k);
    946                 }
    947                 if (Character.isSurrogate(c)) {
    948                     current += 2;
    949                 } else {
    950                     ++current;
    951                 }
    952                 continue;
    953             }
    954             int f = KeyMaps.funForString(moreChars, current);
    955             if (f != View.NO_ID) {
    956                 mCurrentButton = findViewById(f);
    957                 if (explicit) {
    958                     addExplicitKeyToExpr(f);
    959                 } else {
    960                     addKeyToExpr(f);
    961                 }
    962                 if (f == R.id.op_sqrt) {
    963                     // Square root entered as function; don't lose the parenthesis.
    964                     addKeyToExpr(R.id.lparen);
    965                 }
    966                 current = moreChars.indexOf('(', current) + 1;
    967                 continue;
    968             }
    969             // There are characters left, but we can't convert them to button presses.
    970             mUnprocessedChars = moreChars.substring(current);
    971             redisplayAfterFormulaChange();
    972             return;
    973         }
    974         mUnprocessedChars = null;
    975         redisplayAfterFormulaChange();
    976     }
    977 
    978     @Override
    979     public boolean onPaste(ClipData clip) {
    980         final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
    981         if (item == null) {
    982             // nothing to paste, bail early...
    983             return false;
    984         }
    985 
    986         // Check if the item is a previously copied result, otherwise paste as raw text.
    987         final Uri uri = item.getUri();
    988         if (uri != null && mEvaluator.isLastSaved(uri)) {
    989             if (mCurrentState == CalculatorState.ERROR
    990                     || mCurrentState == CalculatorState.RESULT) {
    991                 setState(CalculatorState.INPUT);
    992                 mEvaluator.clear();
    993             }
    994             mEvaluator.addSaved();
    995             redisplayAfterFormulaChange();
    996         } else {
    997             addChars(item.coerceToText(this).toString(), false);
    998         }
    999         return true;
   1000     }
   1001 }
   1002