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