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