Calculator.java revision 06360f9211fc2c6df4c5749bebb65202e1bb12a8
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
17package com.android.calculator2;
18
19import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ArgbEvaluator;
24import android.animation.ObjectAnimator;
25import android.animation.ValueAnimator;
26import android.animation.ValueAnimator.AnimatorUpdateListener;
27import android.app.Activity;
28import android.graphics.Rect;
29import android.os.Bundle;
30import android.support.annotation.NonNull;
31import android.support.v4.view.ViewPager;
32import android.text.Editable;
33import android.text.TextUtils;
34import android.text.TextWatcher;
35import android.view.KeyEvent;
36import android.view.View;
37import android.view.View.OnKeyListener;
38import android.view.View.OnLongClickListener;
39import android.view.ViewAnimationUtils;
40import android.view.ViewGroupOverlay;
41import android.view.animation.AccelerateDecelerateInterpolator;
42import android.widget.Button;
43import android.widget.TextView;
44
45import com.android.calculator2.CalculatorEditText.OnTextSizeChangeListener;
46import com.android.calculator2.CalculatorExpressionEvaluator.EvaluateCallback;
47
48public class Calculator extends Activity
49        implements OnTextSizeChangeListener, EvaluateCallback, OnLongClickListener {
50
51    private static final String NAME = Calculator.class.getName();
52
53    // instance state keys
54    private static final String KEY_CURRENT_STATE = NAME + "_currentState";
55    private static final String KEY_CURRENT_EXPRESSION = NAME + "_currentExpression";
56
57    /**
58     * Constant for an invalid resource id.
59     */
60    public static final int INVALID_RES_ID = -1;
61
62    private enum CalculatorState {
63        INPUT, EVALUATE, RESULT, ERROR
64    }
65
66    private final TextWatcher mFormulaTextWatcher = new TextWatcher() {
67        @Override
68        public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
69        }
70
71        @Override
72        public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
73        }
74
75        @Override
76        public void afterTextChanged(Editable editable) {
77            setState(CalculatorState.INPUT);
78            mEvaluator.evaluate(editable, Calculator.this);
79        }
80    };
81
82    private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
83        @Override
84        public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
85            switch (keyCode) {
86                case KeyEvent.KEYCODE_NUMPAD_ENTER:
87                case KeyEvent.KEYCODE_ENTER:
88                    if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
89                        mCurrentButton = mEqualButton;
90                        onEquals();
91                    }
92                    // ignore all other actions
93                    return true;
94            }
95            return false;
96        }
97    };
98
99    private final Editable.Factory mFormulaEditableFactory = new Editable.Factory() {
100        @Override
101        public Editable newEditable(CharSequence source) {
102            final boolean isEdited = mCurrentState == CalculatorState.INPUT
103                    || mCurrentState == CalculatorState.ERROR;
104            return new CalculatorExpressionBuilder(source, mTokenizer, isEdited);
105        }
106    };
107
108    private CalculatorState mCurrentState;
109    private CalculatorExpressionTokenizer mTokenizer;
110    private CalculatorExpressionEvaluator mEvaluator;
111
112    private View mDisplayView;
113    private CalculatorEditText mFormulaEditText;
114    private CalculatorEditText mResultEditText;
115    private ViewPager mPadViewPager;
116    private View mDeleteButton;
117    private View mEqualButton;
118    private View mClearButton;
119
120    private View mCurrentButton;
121    private Animator mCurrentAnimator;
122
123    @Override
124    protected void onCreate(Bundle savedInstanceState) {
125        super.onCreate(savedInstanceState);
126        setContentView(R.layout.activity_calculator);
127
128        mDisplayView = findViewById(R.id.display);
129        mFormulaEditText = (CalculatorEditText) findViewById(R.id.formula);
130        mResultEditText = (CalculatorEditText) findViewById(R.id.result);
131        mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
132        mDeleteButton = findViewById(R.id.del);
133        mClearButton = findViewById(R.id.clr);
134
135        mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq);
136        if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) {
137            mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq);
138        }
139
140        mTokenizer = new CalculatorExpressionTokenizer(this);
141        mEvaluator = new CalculatorExpressionEvaluator(mTokenizer);
142
143        savedInstanceState = savedInstanceState == null ? Bundle.EMPTY : savedInstanceState;
144        setState(CalculatorState.values()[
145                savedInstanceState.getInt(KEY_CURRENT_STATE, CalculatorState.INPUT.ordinal())]);
146        mFormulaEditText.setText(mTokenizer.getLocalizedExpression(
147                savedInstanceState.getString(KEY_CURRENT_EXPRESSION, "")));
148        mEvaluator.evaluate(mFormulaEditText.getText(), this);
149
150        mFormulaEditText.setEditableFactory(mFormulaEditableFactory);
151        mFormulaEditText.addTextChangedListener(mFormulaTextWatcher);
152        mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener);
153        mFormulaEditText.setOnTextSizeChangeListener(this);
154        mDeleteButton.setOnLongClickListener(this);
155    }
156
157    @Override
158    protected void onSaveInstanceState(@NonNull Bundle outState) {
159        // If there's an animation in progress, cancel it first to ensure our state is up-to-date.
160        if (mCurrentAnimator != null) {
161            mCurrentAnimator.cancel();
162        }
163
164        super.onSaveInstanceState(outState);
165
166        outState.putInt(KEY_CURRENT_STATE, mCurrentState.ordinal());
167        outState.putString(KEY_CURRENT_EXPRESSION,
168                mTokenizer.getNormalizedExpression(mFormulaEditText.getText().toString()));
169    }
170
171    private void setState(CalculatorState state) {
172        if (mCurrentState != state) {
173            mCurrentState = state;
174
175            if (state == CalculatorState.RESULT || state == CalculatorState.ERROR) {
176                mDeleteButton.setVisibility(View.GONE);
177                mClearButton.setVisibility(View.VISIBLE);
178            } else {
179                mDeleteButton.setVisibility(View.VISIBLE);
180                mClearButton.setVisibility(View.GONE);
181            }
182
183            if (state == CalculatorState.ERROR) {
184                final int errorColor = getResources().getColor(R.color.calculator_error_color);
185                mFormulaEditText.setTextColor(errorColor);
186                mResultEditText.setTextColor(errorColor);
187                getWindow().setStatusBarColor(errorColor);
188            } else {
189                mFormulaEditText.setTextColor(
190                        getResources().getColor(R.color.display_formula_text_color));
191                mResultEditText.setTextColor(
192                        getResources().getColor(R.color.display_result_text_color));
193                getWindow().setStatusBarColor(
194                        getResources().getColor(R.color.calculator_accent_color));
195            }
196        }
197    }
198
199    @Override
200    public void onBackPressed() {
201        if (mPadViewPager == null || mPadViewPager.getCurrentItem() == 0) {
202            // If the user is currently looking at the first pad (or the pad is not paged),
203            // allow the system to handle the Back button.
204            super.onBackPressed();
205        } else {
206            // Otherwise, select the previous pad.
207            mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1);
208        }
209    }
210
211    @Override
212    public void onUserInteraction() {
213        super.onUserInteraction();
214
215        // If there's an animation in progress, cancel it so the user interaction can be handled
216        // immediately.
217        if (mCurrentAnimator != null) {
218            mCurrentAnimator.cancel();
219        }
220    }
221
222    public void onButtonClick(View view) {
223        mCurrentButton = view;
224
225        switch (view.getId()) {
226            case R.id.eq:
227                onEquals();
228                break;
229            case R.id.del:
230                onDelete();
231                break;
232            case R.id.clr:
233                onClear();
234                break;
235            case R.id.fun_cos:
236            case R.id.fun_ln:
237            case R.id.fun_log:
238            case R.id.fun_sin:
239            case R.id.fun_tan:
240                // Add left parenthesis after functions.
241                mFormulaEditText.append(((Button) view).getText() + "(");
242                break;
243            default:
244                mFormulaEditText.append(((Button) view).getText());
245                break;
246        }
247    }
248
249    @Override
250    public boolean onLongClick(View view) {
251        mCurrentButton = view;
252
253        if (view.getId() == R.id.del) {
254            onClear();
255            return true;
256        }
257        return false;
258    }
259
260    @Override
261    public void onEvaluate(String expr, String result, int errorResourceId) {
262        if (mCurrentState == CalculatorState.INPUT) {
263            mResultEditText.setText(result);
264        } else if (errorResourceId != INVALID_RES_ID) {
265            onError(errorResourceId);
266        } else if (!TextUtils.isEmpty(result)) {
267            onResult(result);
268        } else if (mCurrentState == CalculatorState.EVALUATE) {
269            // The current expression cannot be evaluated -> return to the input state.
270            setState(CalculatorState.INPUT);
271        }
272
273        mFormulaEditText.requestFocus();
274    }
275
276    @Override
277    public void onTextSizeChanged(final TextView textView, float oldSize) {
278        if (mCurrentState != CalculatorState.INPUT) {
279            // Only animate text changes that occur from user input.
280            return;
281        }
282
283        // Calculate the values needed to perform the scale and translation animations,
284        // maintaining the same apparent baseline for the displayed text.
285        final float textScale = oldSize / textView.getTextSize();
286        final float translationX = (1.0f - textScale) *
287                (textView.getWidth() / 2.0f - textView.getPaddingEnd());
288        final float translationY = (1.0f - textScale) *
289                (textView.getHeight() / 2.0f - textView.getPaddingBottom());
290
291        final AnimatorSet animatorSet = new AnimatorSet();
292        animatorSet.playTogether(
293                ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f),
294                ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f),
295                ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f),
296                ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f));
297        animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime));
298        animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
299        animatorSet.start();
300    }
301
302    private void onEquals() {
303        if (mCurrentState == CalculatorState.INPUT) {
304            setState(CalculatorState.EVALUATE);
305            mEvaluator.evaluate(mFormulaEditText.getText(), this);
306        }
307    }
308
309    private void onDelete() {
310        // Delete works like backspace; remove the last character from the expression.
311        final Editable formulaText = mFormulaEditText.getEditableText();
312        final int formulaLength = formulaText.length();
313        if (formulaLength > 0) {
314            formulaText.delete(formulaLength - 1, formulaLength);
315        }
316    }
317
318    private void reveal(View sourceView, int colorRes, AnimatorListener listener) {
319        final ViewGroupOverlay groupOverlay =
320                (ViewGroupOverlay) getWindow().getDecorView().getOverlay();
321
322        final Rect displayRect = new Rect();
323        mDisplayView.getGlobalVisibleRect(displayRect);
324
325        // Make reveal cover the display and status bar.
326        final View revealView = new View(this);
327        revealView.setBottom(displayRect.bottom);
328        revealView.setLeft(displayRect.left);
329        revealView.setRight(displayRect.right);
330        revealView.setBackgroundColor(getResources().getColor(colorRes));
331        groupOverlay.add(revealView);
332
333        final int[] clearLocation = new int[2];
334        sourceView.getLocationInWindow(clearLocation);
335        clearLocation[0] += sourceView.getWidth() / 2;
336        clearLocation[1] += sourceView.getHeight() / 2;
337
338        final int revealCenterX = clearLocation[0] - revealView.getLeft();
339        final int revealCenterY = clearLocation[1] - revealView.getTop();
340
341        final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2);
342        final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2);
343        final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2);
344        final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2));
345
346        final Animator revealAnimator =
347                ViewAnimationUtils.createCircularReveal(revealView,
348                        revealCenterX, revealCenterY, 0.0f, revealRadius);
349        revealAnimator.setDuration(
350                getResources().getInteger(android.R.integer.config_longAnimTime));
351        revealAnimator.addListener(listener);
352
353        final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
354        alphaAnimator.setDuration(
355                getResources().getInteger(android.R.integer.config_mediumAnimTime));
356
357        final AnimatorSet animatorSet = new AnimatorSet();
358        animatorSet.play(revealAnimator).before(alphaAnimator);
359        animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
360        animatorSet.addListener(new AnimatorListenerAdapter() {
361            @Override
362            public void onAnimationEnd(Animator animator) {
363                groupOverlay.remove(revealView);
364                mCurrentAnimator = null;
365            }
366        });
367
368        mCurrentAnimator = animatorSet;
369        animatorSet.start();
370    }
371
372    private void onClear() {
373        if (TextUtils.isEmpty(mFormulaEditText.getText())) {
374            return;
375        }
376
377        reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
378            @Override
379            public void onAnimationEnd(Animator animation) {
380                mFormulaEditText.getEditableText().clear();
381            }
382        });
383    }
384
385    private void onError(final int errorResourceId) {
386        if (mCurrentState != CalculatorState.EVALUATE) {
387            // Only animate error on evaluate.
388            mResultEditText.setText(errorResourceId);
389            return;
390        }
391
392        reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() {
393            @Override
394            public void onAnimationEnd(Animator animation) {
395                setState(CalculatorState.ERROR);
396                mResultEditText.setText(errorResourceId);
397            }
398        });
399    }
400
401    private void onResult(final String result) {
402        // Calculate the values needed to perform the scale and translation animations,
403        // accounting for how the scale will affect the final position of the text.
404        final float resultScale =
405                mFormulaEditText.getVariableTextSize(result) / mResultEditText.getTextSize();
406        final float resultTranslationX = (1.0f - resultScale) *
407                (mResultEditText.getWidth() / 2.0f - mResultEditText.getPaddingEnd());
408        final float resultTranslationY = (1.0f - resultScale) *
409                (mResultEditText.getHeight() / 2.0f - mResultEditText.getPaddingBottom()) +
410                (mFormulaEditText.getBottom() - mResultEditText.getBottom()) +
411                (mResultEditText.getPaddingBottom() - mFormulaEditText.getPaddingBottom());
412        final float formulaTranslationY = -mFormulaEditText.getBottom();
413
414        // Use a value animator to fade to the final text color over the course of the animation.
415        final int resultTextColor = mResultEditText.getCurrentTextColor();
416        final int formulaTextColor = mFormulaEditText.getCurrentTextColor();
417        final ValueAnimator textColorAnimator =
418                ValueAnimator.ofObject(new ArgbEvaluator(), resultTextColor, formulaTextColor);
419        textColorAnimator.addUpdateListener(new AnimatorUpdateListener() {
420            @Override
421            public void onAnimationUpdate(ValueAnimator valueAnimator) {
422                mResultEditText.setTextColor((int) valueAnimator.getAnimatedValue());
423            }
424        });
425
426        final AnimatorSet animatorSet = new AnimatorSet();
427        animatorSet.playTogether(
428                textColorAnimator,
429                ObjectAnimator.ofFloat(mResultEditText, View.SCALE_X, resultScale),
430                ObjectAnimator.ofFloat(mResultEditText, View.SCALE_Y, resultScale),
431                ObjectAnimator.ofFloat(mResultEditText, View.TRANSLATION_X, resultTranslationX),
432                ObjectAnimator.ofFloat(mResultEditText, View.TRANSLATION_Y, resultTranslationY),
433                ObjectAnimator.ofFloat(mFormulaEditText, View.TRANSLATION_Y, formulaTranslationY));
434        animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
435        animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
436        animatorSet.addListener(new AnimatorListenerAdapter() {
437            @Override
438            public void onAnimationStart(Animator animation) {
439                mResultEditText.setText(result);
440            }
441
442            @Override
443            public void onAnimationEnd(Animator animation) {
444                // Reset all of the values modified during the animation.
445                mResultEditText.setTextColor(resultTextColor);
446                mResultEditText.setScaleX(1.0f);
447                mResultEditText.setScaleY(1.0f);
448                mResultEditText.setTranslationX(0.0f);
449                mResultEditText.setTranslationY(0.0f);
450                mFormulaEditText.setTranslationY(0.0f);
451
452                // Finally update the formula to use the current result.
453                mFormulaEditText.setText(result);
454                setState(CalculatorState.RESULT);
455
456                mCurrentAnimator = null;
457            }
458        });
459
460        mCurrentAnimator = animatorSet;
461        animatorSet.start();
462    }
463}
464