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