/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calculator2; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.app.Activity; import android.graphics.Rect; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.view.ViewPager; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.View; import android.view.View.OnKeyListener; import android.view.View.OnLongClickListener; import android.view.ViewAnimationUtils; import android.view.ViewGroupOverlay; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.Button; import android.widget.TextView; import com.android.calculator2.CalculatorEditText.OnTextSizeChangeListener; import com.android.calculator2.CalculatorExpressionEvaluator.EvaluateCallback; public class Calculator extends Activity implements OnTextSizeChangeListener, EvaluateCallback, OnLongClickListener { private static final String NAME = Calculator.class.getName(); // instance state keys private static final String KEY_CURRENT_STATE = NAME + "_currentState"; private static final String KEY_CURRENT_EXPRESSION = NAME + "_currentExpression"; /** * Constant for an invalid resource id. */ public static final int INVALID_RES_ID = -1; private enum CalculatorState { INPUT, EVALUATE, RESULT, ERROR } private final TextWatcher mFormulaTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { } @Override public void onTextChanged(CharSequence charSequence, int start, int count, int after) { } @Override public void afterTextChanged(Editable editable) { setState(CalculatorState.INPUT); mEvaluator.evaluate(editable, Calculator.this); } }; private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() { @Override public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { switch (keyCode) { case KeyEvent.KEYCODE_NUMPAD_ENTER: case KeyEvent.KEYCODE_ENTER: if (keyEvent.getAction() == KeyEvent.ACTION_UP) { mCurrentButton = mEqualButton; onEquals(); } // ignore all other actions return true; } return false; } }; private final Editable.Factory mFormulaEditableFactory = new Editable.Factory() { @Override public Editable newEditable(CharSequence source) { final boolean isEdited = mCurrentState == CalculatorState.INPUT || mCurrentState == CalculatorState.ERROR; return new CalculatorExpressionBuilder(source, mTokenizer, isEdited); } }; private CalculatorState mCurrentState; private CalculatorExpressionTokenizer mTokenizer; private CalculatorExpressionEvaluator mEvaluator; private View mDisplayView; private CalculatorEditText mFormulaEditText; private CalculatorEditText mResultEditText; private ViewPager mPadViewPager; private View mDeleteButton; private View mEqualButton; private View mClearButton; private View mCurrentButton; private Animator mCurrentAnimator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_calculator); mDisplayView = findViewById(R.id.display); mFormulaEditText = (CalculatorEditText) findViewById(R.id.formula); mResultEditText = (CalculatorEditText) findViewById(R.id.result); mPadViewPager = (ViewPager) findViewById(R.id.pad_pager); mDeleteButton = findViewById(R.id.del); mClearButton = findViewById(R.id.clr); mEqualButton = findViewById(R.id.pad_numeric).findViewById(R.id.eq); if (mEqualButton == null || mEqualButton.getVisibility() != View.VISIBLE) { mEqualButton = findViewById(R.id.pad_operator).findViewById(R.id.eq); } mTokenizer = new CalculatorExpressionTokenizer(this); mEvaluator = new CalculatorExpressionEvaluator(mTokenizer); savedInstanceState = savedInstanceState == null ? Bundle.EMPTY : savedInstanceState; setState(CalculatorState.values()[ savedInstanceState.getInt(KEY_CURRENT_STATE, CalculatorState.INPUT.ordinal())]); mFormulaEditText.setText(mTokenizer.getLocalizedExpression( savedInstanceState.getString(KEY_CURRENT_EXPRESSION, ""))); mEvaluator.evaluate(mFormulaEditText.getText(), this); mFormulaEditText.setEditableFactory(mFormulaEditableFactory); mFormulaEditText.addTextChangedListener(mFormulaTextWatcher); mFormulaEditText.setOnKeyListener(mFormulaOnKeyListener); mFormulaEditText.setOnTextSizeChangeListener(this); mDeleteButton.setOnLongClickListener(this); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { // If there's an animation in progress, cancel it first to ensure our state is up-to-date. if (mCurrentAnimator != null) { mCurrentAnimator.cancel(); } super.onSaveInstanceState(outState); outState.putInt(KEY_CURRENT_STATE, mCurrentState.ordinal()); outState.putString(KEY_CURRENT_EXPRESSION, mTokenizer.getNormalizedExpression(mFormulaEditText.getText().toString())); } private void setState(CalculatorState state) { if (mCurrentState != state) { mCurrentState = state; if (state == CalculatorState.RESULT || state == CalculatorState.ERROR) { mDeleteButton.setVisibility(View.GONE); mClearButton.setVisibility(View.VISIBLE); } else { mDeleteButton.setVisibility(View.VISIBLE); mClearButton.setVisibility(View.GONE); } if (state == CalculatorState.ERROR) { final int errorColor = getResources().getColor(R.color.calculator_error_color); mFormulaEditText.setTextColor(errorColor); mResultEditText.setTextColor(errorColor); getWindow().setStatusBarColor(errorColor); } else { mFormulaEditText.setTextColor( getResources().getColor(R.color.display_formula_text_color)); mResultEditText.setTextColor( getResources().getColor(R.color.display_result_text_color)); getWindow().setStatusBarColor( getResources().getColor(R.color.calculator_accent_color)); } } } @Override public void onBackPressed() { if (mPadViewPager == null || mPadViewPager.getCurrentItem() == 0) { // If the user is currently looking at the first pad (or the pad is not paged), // allow the system to handle the Back button. super.onBackPressed(); } else { // Otherwise, select the previous pad. mPadViewPager.setCurrentItem(mPadViewPager.getCurrentItem() - 1); } } @Override public void onUserInteraction() { super.onUserInteraction(); // If there's an animation in progress, cancel it so the user interaction can be handled // immediately. if (mCurrentAnimator != null) { mCurrentAnimator.cancel(); } } public void onButtonClick(View view) { mCurrentButton = view; switch (view.getId()) { case R.id.eq: onEquals(); break; case R.id.del: onDelete(); break; case R.id.clr: onClear(); break; case R.id.fun_cos: case R.id.fun_ln: case R.id.fun_log: case R.id.fun_sin: case R.id.fun_tan: // Add left parenthesis after functions. mFormulaEditText.append(((Button) view).getText() + "("); break; default: mFormulaEditText.append(((Button) view).getText()); break; } } @Override public boolean onLongClick(View view) { mCurrentButton = view; if (view.getId() == R.id.del) { onClear(); return true; } return false; } @Override public void onEvaluate(String expr, String result, int errorResourceId) { if (mCurrentState == CalculatorState.INPUT) { mResultEditText.setText(result); } else if (errorResourceId != INVALID_RES_ID) { onError(errorResourceId); } else if (!TextUtils.isEmpty(result)) { onResult(result); } else if (mCurrentState == CalculatorState.EVALUATE) { // The current expression cannot be evaluated -> return to the input state. setState(CalculatorState.INPUT); } mFormulaEditText.requestFocus(); } @Override public void onTextSizeChanged(final TextView textView, float oldSize) { if (mCurrentState != CalculatorState.INPUT) { // Only animate text changes that occur from user input. return; } // Calculate the values needed to perform the scale and translation animations, // maintaining the same apparent baseline for the displayed text. final float textScale = oldSize / textView.getTextSize(); final float translationX = (1.0f - textScale) * (textView.getWidth() / 2.0f - textView.getPaddingEnd()); final float translationY = (1.0f - textScale) * (textView.getHeight() / 2.0f - textView.getPaddingBottom()); final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( ObjectAnimator.ofFloat(textView, View.SCALE_X, textScale, 1.0f), ObjectAnimator.ofFloat(textView, View.SCALE_Y, textScale, 1.0f), ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, translationX, 0.0f), ObjectAnimator.ofFloat(textView, View.TRANSLATION_Y, translationY, 0.0f)); animatorSet.setDuration(getResources().getInteger(android.R.integer.config_mediumAnimTime)); animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); animatorSet.start(); } private void onEquals() { if (mCurrentState == CalculatorState.INPUT) { setState(CalculatorState.EVALUATE); mEvaluator.evaluate(mFormulaEditText.getText(), this); } } private void onDelete() { // Delete works like backspace; remove the last character from the expression. final Editable formulaText = mFormulaEditText.getEditableText(); final int formulaLength = formulaText.length(); if (formulaLength > 0) { formulaText.delete(formulaLength - 1, formulaLength); } } private void reveal(View sourceView, int colorRes, AnimatorListener listener) { final ViewGroupOverlay groupOverlay = (ViewGroupOverlay) getWindow().getDecorView().getOverlay(); final Rect displayRect = new Rect(); mDisplayView.getGlobalVisibleRect(displayRect); // Make reveal cover the display and status bar. final View revealView = new View(this); revealView.setBottom(displayRect.bottom); revealView.setLeft(displayRect.left); revealView.setRight(displayRect.right); revealView.setBackgroundColor(getResources().getColor(colorRes)); groupOverlay.add(revealView); final int[] clearLocation = new int[2]; sourceView.getLocationInWindow(clearLocation); clearLocation[0] += sourceView.getWidth() / 2; clearLocation[1] += sourceView.getHeight() / 2; final int revealCenterX = clearLocation[0] - revealView.getLeft(); final int revealCenterY = clearLocation[1] - revealView.getTop(); final double x1_2 = Math.pow(revealView.getLeft() - revealCenterX, 2); final double x2_2 = Math.pow(revealView.getRight() - revealCenterX, 2); final double y_2 = Math.pow(revealView.getTop() - revealCenterY, 2); final float revealRadius = (float) Math.max(Math.sqrt(x1_2 + y_2), Math.sqrt(x2_2 + y_2)); final Animator revealAnimator = ViewAnimationUtils.createCircularReveal(revealView, revealCenterX, revealCenterY, 0.0f, revealRadius); revealAnimator.setDuration( getResources().getInteger(android.R.integer.config_longAnimTime)); revealAnimator.addListener(listener); final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f); alphaAnimator.setDuration( getResources().getInteger(android.R.integer.config_mediumAnimTime)); final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(revealAnimator).before(alphaAnimator); animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { groupOverlay.remove(revealView); mCurrentAnimator = null; } }); mCurrentAnimator = animatorSet; animatorSet.start(); } private void onClear() { if (TextUtils.isEmpty(mFormulaEditText.getText())) { return; } reveal(mCurrentButton, R.color.calculator_accent_color, new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mFormulaEditText.getEditableText().clear(); } }); } private void onError(final int errorResourceId) { if (mCurrentState != CalculatorState.EVALUATE) { // Only animate error on evaluate. mResultEditText.setText(errorResourceId); return; } reveal(mCurrentButton, R.color.calculator_error_color, new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { setState(CalculatorState.ERROR); mResultEditText.setText(errorResourceId); } }); } private void onResult(final String result) { // Calculate the values needed to perform the scale and translation animations, // accounting for how the scale will affect the final position of the text. final float resultScale = mFormulaEditText.getVariableTextSize(result) / mResultEditText.getTextSize(); final float resultTranslationX = (1.0f - resultScale) * (mResultEditText.getWidth() / 2.0f - mResultEditText.getPaddingEnd()); final float resultTranslationY = (1.0f - resultScale) * (mResultEditText.getHeight() / 2.0f - mResultEditText.getPaddingBottom()) + (mFormulaEditText.getBottom() - mResultEditText.getBottom()) + (mResultEditText.getPaddingBottom() - mFormulaEditText.getPaddingBottom()); final float formulaTranslationY = -mFormulaEditText.getBottom(); // Use a value animator to fade to the final text color over the course of the animation. final int resultTextColor = mResultEditText.getCurrentTextColor(); final int formulaTextColor = mFormulaEditText.getCurrentTextColor(); final ValueAnimator textColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), resultTextColor, formulaTextColor); textColorAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mResultEditText.setTextColor((int) valueAnimator.getAnimatedValue()); } }); final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( textColorAnimator, ObjectAnimator.ofFloat(mResultEditText, View.SCALE_X, resultScale), ObjectAnimator.ofFloat(mResultEditText, View.SCALE_Y, resultScale), ObjectAnimator.ofFloat(mResultEditText, View.TRANSLATION_X, resultTranslationX), ObjectAnimator.ofFloat(mResultEditText, View.TRANSLATION_Y, resultTranslationY), ObjectAnimator.ofFloat(mFormulaEditText, View.TRANSLATION_Y, formulaTranslationY)); animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime)); animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mResultEditText.setText(result); } @Override public void onAnimationEnd(Animator animation) { // Reset all of the values modified during the animation. mResultEditText.setTextColor(resultTextColor); mResultEditText.setScaleX(1.0f); mResultEditText.setScaleY(1.0f); mResultEditText.setTranslationX(0.0f); mResultEditText.setTranslationY(0.0f); mFormulaEditText.setTranslationY(0.0f); // Finally update the formula to use the current result. mFormulaEditText.setText(result); setState(CalculatorState.RESULT); mCurrentAnimator = null; } }); mCurrentAnimator = animatorSet; animatorSet.start(); } }