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