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