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