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