• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.calculator2;
18 
19 import android.animation.Animator;
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ArgbEvaluator;
24 import android.animation.ObjectAnimator;
25 import android.animation.ValueAnimator;
26 import android.animation.ValueAnimator.AnimatorUpdateListener;
27 import android.app.Activity;
28 import android.graphics.Rect;
29 import android.os.Bundle;
30 import android.support.annotation.NonNull;
31 import android.support.v4.view.ViewPager;
32 import android.text.Editable;
33 import android.text.TextUtils;
34 import android.text.TextWatcher;
35 import android.view.KeyEvent;
36 import android.view.View;
37 import android.view.View.OnKeyListener;
38 import android.view.View.OnLongClickListener;
39 import android.view.ViewAnimationUtils;
40 import android.view.ViewGroupOverlay;
41 import android.view.animation.AccelerateDecelerateInterpolator;
42 import android.widget.Button;
43 import android.widget.TextView;
44 
45 import com.android.calculator2.CalculatorEditText.OnTextSizeChangeListener;
46 import com.android.calculator2.CalculatorExpressionEvaluator.EvaluateCallback;
47 
48 public 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
onCreate(Bundle savedInstanceState)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
onSaveInstanceState(@onNull Bundle outState)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 
setState(CalculatorState state)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
onBackPressed()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
onUserInteraction()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 
onButtonClick(View view)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
onLongClick(View view)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
onEvaluate(String expr, String result, int errorResourceId)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
onTextSizeChanged(final TextView textView, float oldSize)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 
onEquals()297     private void onEquals() {
298         if (mCurrentState == CalculatorState.INPUT) {
299             setState(CalculatorState.EVALUATE);
300             mEvaluator.evaluate(mFormulaEditText.getText(), this);
301         }
302     }
303 
onDelete()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 
reveal(View sourceView, int colorRes, AnimatorListener listener)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 
onClear()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 
onError(final int errorResourceId)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 
onResult(final String result)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