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