blob: a2bcad13b502c74d8bc665346792f412c600d17e [file] [log] [blame]
/*
* 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) {
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 mClearButton;
private View mEqualButton;
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, end it immediately to ensure the state is
// up-to-date before it is serialized.
if (mCurrentAnimator != null) {
mCurrentAnimator.end();
}
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, end it immediately to ensure the state is
// up-to-date before the pending user interaction is handled.
if (mCurrentAnimator != null) {
mCurrentAnimator.end();
}
}
public void onButtonClick(View 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) {
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));
final Animator alphaAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
alphaAnimator.setDuration(
getResources().getInteger(android.R.integer.config_mediumAnimTime));
alphaAnimator.addListener(listener);
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;
}
final View sourceView = mClearButton.getVisibility() == View.VISIBLE
? mClearButton : mDeleteButton;
reveal(sourceView, R.color.calculator_accent_color, new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(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(mEqualButton, R.color.calculator_error_color, new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(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();
}
}