Home | History | Annotate | Download | only in calculator2
      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
    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