1 /* 2 * Copyright (C) 2015 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.content.ClipData; 20 import android.content.ClipDescription; 21 import android.content.ClipboardManager; 22 import android.content.Context; 23 import android.graphics.Rect; 24 import android.text.Layout; 25 import android.text.Spannable; 26 import android.text.SpannableString; 27 import android.text.Spanned; 28 import android.text.TextPaint; 29 import android.text.style.BackgroundColorSpan; 30 import android.text.style.ForegroundColorSpan; 31 import android.util.AttributeSet; 32 import android.view.ActionMode; 33 import android.view.GestureDetector; 34 import android.view.Menu; 35 import android.view.MenuInflater; 36 import android.view.MenuItem; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.widget.OverScroller; 40 import android.widget.Toast; 41 42 // A text widget that is "infinitely" scrollable to the right, 43 // and obtains the text to display via a callback to Logic. 44 public class CalculatorResult extends AlignedTextView { 45 static final int MAX_RIGHT_SCROLL = 10000000; 46 static final int INVALID = MAX_RIGHT_SCROLL + 10000; 47 // A larger value is unlikely to avoid running out of space 48 final OverScroller mScroller; 49 final GestureDetector mGestureDetector; 50 class MyTouchListener implements View.OnTouchListener { 51 @Override onTouch(View v, MotionEvent event)52 public boolean onTouch(View v, MotionEvent event) { 53 return mGestureDetector.onTouchEvent(event); 54 } 55 } 56 final MyTouchListener mTouchListener = new MyTouchListener(); 57 private Evaluator mEvaluator; 58 private boolean mScrollable = false; 59 // A scrollable result is currently displayed. 60 private boolean mValid = false; 61 // The result holds something valid; either a a number or an error 62 // message. 63 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position 64 // in which the decimal point is just barely visible on the right of the display. 65 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels. 66 // Large positive values mean the decimal point is scrolled off the 67 // left of the display. Zero means decimal point is barely displayed 68 // on the right. 69 private int mLastPos; // Position already reflected in display. Pixels. 70 private int mMinPos; // Minimum position before all digits disappear off the right. Pixels. 71 private int mMaxPos; // Maximum position before we start displaying the infinite 72 // sequence of trailing zeroes on the right. Pixels. 73 // In the following, we use a suffix of Offset to denote a character position in a numeric 74 // string relative to the decimal point. Positive is to the right and negative is to 75 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used 76 // for the offset of the last digit in an a nonterminating decimal expansion. 77 // We use the suffix "Index" to denote a zero-based index into a string representing a 78 // result. 79 // TODO: Apply the same convention to other classes. 80 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit 81 // that should be displayed. Essentially the same as 82 private int mLsdOffset; // Position of least-significant digit in result 83 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding 84 // exponent. 85 private final Object mWidthLock = new Object(); 86 // Protects the next two fields. 87 private int mWidthConstraint = -1; 88 // Our total width in pixels minus space for ellipsis. 89 private float mCharWidth = 1; 90 // Maximum character width. For now we pretend that all characters 91 // have this width. 92 // TODO: We're not really using a fixed width font. But it appears 93 // to be close enough for the characters we use that the difference 94 // is not noticeable. 95 private static final int MAX_WIDTH = 100; 96 // Maximum number of digits displayed 97 public static final int MAX_LEADING_ZEROES = 6; 98 // Maximum number of leading zeroes after decimal point before we 99 // switch to scientific notation with negative exponent. 100 public static final int MAX_TRAILING_ZEROES = 6; 101 // Maximum number of trailing zeroes before the decimal point before 102 // we switch to scientific notation with positive exponent. 103 private static final int SCI_NOTATION_EXTRA = 1; 104 // Extra digits for standard scientific notation. In this case we 105 // have a decimal point and no ellipsis. 106 // We assume that we do not drop digits to make room for the decimal 107 // point in ordinary scientific notation. Thus >= 1. 108 private ActionMode mActionMode; 109 private final ForegroundColorSpan mExponentColorSpan; 110 CalculatorResult(Context context, AttributeSet attrs)111 public CalculatorResult(Context context, AttributeSet attrs) { 112 super(context, attrs); 113 mScroller = new OverScroller(context); 114 mGestureDetector = new GestureDetector(context, 115 new GestureDetector.SimpleOnGestureListener() { 116 @Override 117 public boolean onDown(MotionEvent e) { 118 return true; 119 } 120 @Override 121 public boolean onFling(MotionEvent e1, MotionEvent e2, 122 float velocityX, float velocityY) { 123 if (!mScroller.isFinished()) { 124 mCurrentPos = mScroller.getFinalX(); 125 } 126 mScroller.forceFinished(true); 127 stopActionMode(); 128 CalculatorResult.this.cancelLongPress(); 129 // Ignore scrolls of error string, etc. 130 if (!mScrollable) return true; 131 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */, 132 mMinPos, mMaxPos, 0, 0); 133 postInvalidateOnAnimation(); 134 return true; 135 } 136 @Override 137 public boolean onScroll(MotionEvent e1, MotionEvent e2, 138 float distanceX, float distanceY) { 139 int distance = (int)distanceX; 140 if (!mScroller.isFinished()) { 141 mCurrentPos = mScroller.getFinalX(); 142 } 143 mScroller.forceFinished(true); 144 stopActionMode(); 145 CalculatorResult.this.cancelLongPress(); 146 if (!mScrollable) return true; 147 if (mCurrentPos + distance < mMinPos) { 148 distance = mMinPos - mCurrentPos; 149 } else if (mCurrentPos + distance > mMaxPos) { 150 distance = mMaxPos - mCurrentPos; 151 } 152 int duration = (int)(e2.getEventTime() - e1.getEventTime()); 153 if (duration < 1 || duration > 100) duration = 10; 154 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration); 155 postInvalidateOnAnimation(); 156 return true; 157 } 158 @Override 159 public void onLongPress(MotionEvent e) { 160 if (mValid) { 161 mActionMode = startActionMode(mCopyActionModeCallback, 162 ActionMode.TYPE_FLOATING); 163 } 164 } 165 }); 166 setOnTouchListener(mTouchListener); 167 setHorizontallyScrolling(false); // do it ourselves 168 setCursorVisible(false); 169 mExponentColorSpan = new ForegroundColorSpan( 170 context.getColor(R.color.display_result_exponent_text_color)); 171 172 // Copy ActionMode is triggered explicitly, not through 173 // setCustomSelectionActionModeCallback. 174 } 175 setEvaluator(Evaluator evaluator)176 void setEvaluator(Evaluator evaluator) { 177 mEvaluator = evaluator; 178 } 179 180 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)181 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 182 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 183 184 final TextPaint paint = getPaint(); 185 final Context context = getContext(); 186 final float newCharWidth = Layout.getDesiredWidth("\u2007", paint); 187 // Digits are presumed to have no more than newCharWidth. 188 // We sometimes replace a character by an ellipsis or, due to SCI_NOTATION_EXTRA, add 189 // an extra decimal separator beyond the maximum number of characters we normally allow. 190 // Empirically, our minus sign is also slightly wider than a digit, so we have to 191 // account for that. We never have both an ellipsis and two minus signs, and 192 // we assume an ellipsis is no narrower than a minus sign. 193 final float decimalSeparatorWidth = Layout.getDesiredWidth( 194 context.getString(R.string.dec_point), paint); 195 final float minusExtraWidth = Layout.getDesiredWidth( 196 context.getString(R.string.op_sub), paint) - newCharWidth; 197 final float ellipsisExtraWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint) 198 - newCharWidth; 199 final int extraWidth = (int) (Math.ceil(Math.max(decimalSeparatorWidth + minusExtraWidth, 200 ellipsisExtraWidth)) + Math.max(minusExtraWidth, 0.0f)); 201 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec) 202 - (getPaddingLeft() + getPaddingRight()) - extraWidth; 203 synchronized(mWidthLock) { 204 mWidthConstraint = newWidthConstraint; 205 mCharWidth = newCharWidth; 206 } 207 } 208 209 // Return the length of the exponent representation for the given exponent, in 210 // characters. expLen(int exp)211 private final int expLen(int exp) { 212 if (exp == 0) return 0; 213 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp)) 214 + 0.0000000001d /* Round whole numbers to next integer */); 215 return abs_exp_digits + (exp >= 0 ? 1 : 2); 216 } 217 218 /** 219 * Initiate display of a new result. 220 * The parameters specify various properties of the result. 221 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit) 222 * @param msd Position of most significant digit. Offset from left of string. 223 Evaluator.INVALID_MSD if unknown. 224 * @param leastDigPos Position of least significant digit (1 = tenths digit) 225 * or Integer.MAX_VALUE. 226 * @param truncatedWholePart Result up to but not including decimal point. 227 Currently we only use the length. 228 */ displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart)229 void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) { 230 initPositions(initPrec, msd, leastDigPos, truncatedWholePart); 231 redisplay(); 232 } 233 234 /** 235 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is 236 * scrollable, based on the supplied information about the result. 237 * This is unfortunately complicated because we need to predict whether trailing digits 238 * will eventually be replaced by an exponent. 239 * Just appending the exponent during formatting would be simpler, but would produce 240 * jumpier results during transitions. 241 */ initPositions(int initPrecOffset, int msdIndex, int lsdOffset, String truncatedWholePart)242 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset, 243 String truncatedWholePart) { 244 float charWidth; 245 int maxChars = getMaxChars(); 246 mLastPos = INVALID; 247 mLsdOffset = lsdOffset; 248 synchronized(mWidthLock) { 249 charWidth = mCharWidth; 250 } 251 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * charWidth); 252 // Prevent scrolling past initial position, which is calculated to show leading digits. 253 if (msdIndex == Evaluator.INVALID_MSD) { 254 // Possible zero value 255 if (lsdOffset == Integer.MIN_VALUE) { 256 // Definite zero value. 257 mMaxPos = mMinPos; 258 mMaxCharOffset = (int) Math.round(mMaxPos/charWidth); 259 mScrollable = false; 260 } else { 261 // May be very small nonzero value. Allow user to find out. 262 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL; 263 mMinPos -= charWidth; // Allow for future minus sign. 264 mScrollable = true; 265 } 266 return; 267 } 268 int wholeLen = truncatedWholePart.length(); 269 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0; 270 if (msdIndex > wholeLen && msdIndex <= wholeLen + 3) { 271 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point. 272 msdIndex = wholeLen - 1; 273 } 274 int minCharOffset = msdIndex - wholeLen; 275 // Position of leftmost significant digit relative to dec. point. 276 // Usually negative. 277 mMaxCharOffset = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right? 278 // If msd is left of decimal point should logically be 279 // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but 280 // we eventually translate to a character position by dividing by mCharWidth. 281 // To avoid rounding issues, we use the analogous computation here. 282 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) { 283 // Small number of leading zeroes, avoid scientific notation. 284 minCharOffset = -1; 285 } 286 if (lsdOffset < MAX_RIGHT_SCROLL) { 287 mMaxCharOffset = lsdOffset; 288 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) { 289 mMaxCharOffset = -1; 290 } 291 // lsdOffset is positive or negative, never 0. 292 int currentExpLen = 0; // Length of required standard scientific notation exponent. 293 if (mMaxCharOffset < -1) { 294 currentExpLen = expLen(-minCharOffset - 1); 295 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) { 296 // Number either entirely to the right of decimal point, or decimal point not 297 // visible when scrolled to the right. 298 currentExpLen = expLen(-minCharOffset); 299 } 300 mScrollable = (mMaxCharOffset + currentExpLen - minCharOffset + negative >= maxChars); 301 int newMaxCharOffset; 302 if (currentExpLen > 0) { 303 if (mScrollable) { 304 // We'll use exponent corresponding to leastDigPos when scrolled to right. 305 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset); 306 } else { 307 newMaxCharOffset = mMaxCharOffset + currentExpLen; 308 } 309 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) { 310 // Very unlikely; just drop exponent. 311 mMaxCharOffset = -1; 312 } else { 313 mMaxCharOffset = newMaxCharOffset; 314 } 315 } 316 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * charWidth), MAX_RIGHT_SCROLL); 317 if (!mScrollable) { 318 // Position the number consistently with our assumptions to make sure it 319 // actually fits. 320 mCurrentPos = mMaxPos; 321 } 322 } else { 323 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL; 324 mScrollable = true; 325 } 326 } 327 displayError(int resourceId)328 void displayError(int resourceId) { 329 mValid = true; 330 mScrollable = false; 331 setText(resourceId); 332 } 333 334 private final int MAX_COPY_SIZE = 1000000; 335 336 /* 337 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD. 338 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant. 339 */ getNaiveMsdIndexOf(String s)340 public static int getNaiveMsdIndexOf(String s) { 341 int len = s.length(); 342 for (int i = 0; i < len; ++i) { 343 char c = s.charAt(i); 344 if (c != '-' && c != '.' && c != '0') { 345 return i; 346 } 347 } 348 return Evaluator.INVALID_MSD; 349 } 350 351 // Format a result returned by Evaluator.getString() into a single line containing ellipses 352 // (if appropriate) and an exponent (if appropriate). precOffset is the value that was passed 353 // to getString and thus identifies the significance of the rightmost digit. 354 // A value of 1 means the rightmost digits corresponds to tenths. 355 // maxDigs is the maximum number of characters in the result. 356 // We set lastDisplayedOffset[0] to the offset of the last digit actually appearing in 357 // the display. 358 // If forcePrecision is true, we make sure that the last displayed digit corresponds to 359 // precOffset, and allow maxDigs to be exceeded in assing the exponent. 360 // We add two distinct kinds of exponents: 361 // (1) If the final result contains the leading digit we use standard scientific notation. 362 // (2) If not, we add an exponent corresponding to an interpretation of the final result as 363 // an integer. 364 // We add an ellipsis on the left if the result was truncated. 365 // We add ellipses and exponents in a way that leaves most digits in the position they 366 // would have been in had we not done so. 367 // This minimizes jumps as a result of scrolling. Result is NOT internationalized, 368 // uses "E" for exponent. formatResult(String in, int precOffset, int maxDigs, boolean truncated, boolean negative, int lastDisplayedOffset[], boolean forcePrecision)369 public String formatResult(String in, int precOffset, int maxDigs, boolean truncated, 370 boolean negative, int lastDisplayedOffset[], boolean forcePrecision) { 371 final int minusSpace = negative ? 1 : 0; 372 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK. 373 String result = in; 374 if (truncated || (negative && result.charAt(0) != '-')) { 375 result = KeyMaps.ELLIPSIS + result.substring(1, result.length()); 376 // Ellipsis may be removed again in the type(1) scientific notation case. 377 } 378 final int decIndex = result.indexOf('.'); 379 lastDisplayedOffset[0] = precOffset; 380 if ((decIndex == -1 || msdIndex != Evaluator.INVALID_MSD 381 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) { 382 // No decimal point displayed, and it's not just to the right of the last digit, 383 // or we should suppress leading zeroes. 384 // Add an exponent to let the user track which digits are currently displayed. 385 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point. 386 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1; 387 int exponent = initExponent; 388 boolean hasPoint = false; 389 if (!truncated && msdIndex < maxDigs - 1 390 && result.length() - msdIndex + 1 + minusSpace 391 <= maxDigs + SCI_NOTATION_EXTRA) { 392 // Type (1) exponent computation and transformation: 393 // Leading digit is in display window. Use standard calculator scientific notation 394 // with one digit to the left of the decimal point. Insert decimal point and 395 // delete leading zeroes. 396 // We try to keep leading digits roughly in position, and never 397 // lengthen the result by more than SCI_NOTATION_EXTRA. 398 final int resLen = result.length(); 399 String fraction = result.substring(msdIndex + 1, resLen); 400 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1) 401 + "." + fraction; 402 // Original exp was correct for decimal point at right of fraction. 403 // Adjust by length of fraction. 404 exponent = initExponent + resLen - msdIndex - 1; 405 hasPoint = true; 406 } 407 // Exponent can't be zero. 408 // Actually add the exponent of either type: 409 if (!forcePrecision) { 410 int dropDigits; // Digits to drop to make room for exponent. 411 if (hasPoint) { 412 // Type (1) exponent. 413 // Drop digits even if there is room. Otherwise the scrolling gets jumpy. 414 dropDigits = expLen(exponent); 415 if (dropDigits >= result.length() - 1) { 416 // Jumpy is better than no mantissa. Probably impossible anyway. 417 dropDigits = Math.max(result.length() - 2, 0); 418 } 419 } else { 420 // Type (2) exponent. 421 // Exponent depends on the number of digits we drop, which depends on 422 // exponent ... 423 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits; 424 ++dropDigits) {} 425 exponent = initExponent + dropDigits; 426 if (precOffset - dropDigits > mLsdOffset) { 427 // This can happen if e.g. result = 10^40 + 10^10 428 // It turns out we would otherwise display ...10e9 because it takes 429 // the same amount of space as ...1e10 but shows one more digit. 430 // But we don't want to display a trailing zero, even if it's free. 431 ++dropDigits; 432 ++exponent; 433 } 434 } 435 result = result.substring(0, result.length() - dropDigits); 436 lastDisplayedOffset[0] -= dropDigits; 437 } 438 result = result + "E" + Integer.toString(exponent); 439 } 440 return result; 441 } 442 443 /** 444 * Get formatted, but not internationalized, result from mEvaluator. 445 * @param precOffset requested position (1 = tenths) of last included digit. 446 * @param maxSize Maximum number of characters (more or less) in result. 447 * @param lastDisplayedOffset Zeroth entry is set to actual offset of last included digit, 448 * after adjusting for exponent, etc. 449 * @param forcePrecision Ensure that last included digit is at pos, at the expense 450 * of treating maxSize as a soft limit. 451 */ getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[], boolean forcePrecision)452 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[], 453 boolean forcePrecision) { 454 final boolean truncated[] = new boolean[1]; 455 final boolean negative[] = new boolean[1]; 456 final int requestedPrecOffset[] = {precOffset}; 457 final String rawResult = mEvaluator.getString(requestedPrecOffset, mMaxCharOffset, 458 maxSize, truncated, negative); 459 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0], 460 lastDisplayedOffset, forcePrecision); 461 } 462 463 // Return entire result (within reason) up to current displayed precision. getFullText()464 public String getFullText() { 465 if (!mValid) return ""; 466 if (!mScrollable) return getText().toString(); 467 int currentCharOffset = getCurrentCharOffset(); 468 int unused[] = new int[1]; 469 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE, 470 unused, true)); 471 } 472 fullTextIsExact()473 public boolean fullTextIsExact() { 474 return !mScrollable 475 || mMaxCharOffset == getCurrentCharOffset() && mMaxCharOffset != MAX_RIGHT_SCROLL; 476 } 477 478 /** 479 * Return the maximum number of characters that will fit in the result display. 480 * May be called asynchronously from non-UI thread. 481 */ getMaxChars()482 int getMaxChars() { 483 int result; 484 synchronized(mWidthLock) { 485 result = (int) Math.floor(mWidthConstraint / mCharWidth); 486 // We can apparently finish evaluating before onMeasure in CalculatorText has been 487 // called, in which case we get 0 or -1 as the width constraint. 488 } 489 if (result <= 0) { 490 // Return something conservatively big, to force sufficient evaluation. 491 return MAX_WIDTH; 492 } else { 493 return result; 494 } 495 } 496 497 /** 498 * @return {@code true} if the currently displayed result is scrollable 499 */ isScrollable()500 public boolean isScrollable() { 501 return mScrollable; 502 } 503 getCurrentCharOffset()504 int getCurrentCharOffset() { 505 synchronized(mWidthLock) { 506 return (int) Math.round(mCurrentPos / mCharWidth); 507 } 508 } 509 clear()510 void clear() { 511 mValid = false; 512 mScrollable = false; 513 setText(""); 514 } 515 redisplay()516 void redisplay() { 517 int currentCharOffset = getCurrentCharOffset(); 518 int maxChars = getMaxChars(); 519 int lastDisplayedOffset[] = new int[1]; 520 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset, false); 521 int expIndex = result.indexOf('E'); 522 result = KeyMaps.translateResult(result); 523 if (expIndex > 0 && result.indexOf('.') == -1) { 524 // Gray out exponent if used as position indicator 525 SpannableString formattedResult = new SpannableString(result); 526 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(), 527 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 528 setText(formattedResult); 529 } else { 530 setText(result); 531 } 532 mLastDisplayedOffset = lastDisplayedOffset[0]; 533 mValid = true; 534 } 535 536 @Override computeScroll()537 public void computeScroll() { 538 if (!mScrollable) return; 539 if (mScroller.computeScrollOffset()) { 540 mCurrentPos = mScroller.getCurrX(); 541 if (mCurrentPos != mLastPos) { 542 mLastPos = mCurrentPos; 543 redisplay(); 544 } 545 if (!mScroller.isFinished()) { 546 postInvalidateOnAnimation(); 547 } 548 } 549 } 550 551 // Copy support: 552 553 private ActionMode.Callback2 mCopyActionModeCallback = new ActionMode.Callback2() { 554 555 private BackgroundColorSpan mHighlightSpan; 556 557 private void highlightResult() { 558 final Spannable text = (Spannable) getText(); 559 mHighlightSpan = new BackgroundColorSpan(getHighlightColor()); 560 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 561 } 562 563 private void unhighlightResult() { 564 final Spannable text = (Spannable) getText(); 565 text.removeSpan(mHighlightSpan); 566 } 567 568 @Override 569 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 570 MenuInflater inflater = mode.getMenuInflater(); 571 inflater.inflate(R.menu.copy, menu); 572 highlightResult(); 573 return true; 574 } 575 576 @Override 577 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 578 return false; // Return false if nothing is done 579 } 580 581 @Override 582 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 583 switch (item.getItemId()) { 584 case R.id.menu_copy: 585 copyContent(); 586 mode.finish(); 587 return true; 588 default: 589 return false; 590 } 591 } 592 593 @Override 594 public void onDestroyActionMode(ActionMode mode) { 595 unhighlightResult(); 596 mActionMode = null; 597 } 598 599 @Override 600 public void onGetContentRect(ActionMode mode, View view, Rect outRect) { 601 super.onGetContentRect(mode, view, outRect); 602 outRect.left += getPaddingLeft(); 603 outRect.top += getPaddingTop(); 604 outRect.right -= getPaddingRight(); 605 outRect.bottom -= getPaddingBottom(); 606 final int width = (int) Layout.getDesiredWidth(getText(), getPaint()); 607 if (width < outRect.width()) { 608 outRect.left = outRect.right - width; 609 } 610 } 611 }; 612 stopActionMode()613 public boolean stopActionMode() { 614 if (mActionMode != null) { 615 mActionMode.finish(); 616 return true; 617 } 618 return false; 619 } 620 setPrimaryClip(ClipData clip)621 private void setPrimaryClip(ClipData clip) { 622 ClipboardManager clipboard = (ClipboardManager) getContext(). 623 getSystemService(Context.CLIPBOARD_SERVICE); 624 clipboard.setPrimaryClip(clip); 625 } 626 copyContent()627 private void copyContent() { 628 final CharSequence text = getFullText(); 629 ClipboardManager clipboard = 630 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); 631 // We include a tag URI, to allow us to recognize our own results and handle them 632 // specially. 633 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture()); 634 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN}; 635 ClipData cd = new ClipData("calculator result", mimeTypes, newItem); 636 clipboard.setPrimaryClip(cd); 637 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show(); 638 } 639 640 } 641