Home | History | Annotate | Download | only in calculator2
      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
     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 
    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 
    176     void setEvaluator(Evaluator evaluator) {
    177         mEvaluator = evaluator;
    178     }
    179 
    180     @Override
    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.
    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      */
    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      */
    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 
    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.getMsdPos, we treat a final 1 as significant.
    339      */
    340     public static int getNaiveMsdIndex(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).  prec is the value that was passed to
    353     // 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     // prec, 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.
    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 : getNaiveMsdIndex(in);  // INVALID_MSD is OK.
    373         final int decIndex = in.indexOf('.');
    374         String result = in;
    375         lastDisplayedOffset[0] = precOffset;
    376         if ((decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
    377                 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) &&  precOffset != -1) {
    378             // No decimal point displayed, and it's not just to the right of the last digit,
    379             // or we should suppress leading zeroes.
    380             // Add an exponent to let the user track which digits are currently displayed.
    381             // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
    382             final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
    383             int exponent = initExponent;
    384             boolean hasPoint = false;
    385             if (!truncated && msdIndex < maxDigs - 1
    386                     && result.length() - msdIndex + 1 + minusSpace
    387                     <= maxDigs + SCI_NOTATION_EXTRA) {
    388                 // Type (1) exponent computation and transformation:
    389                 // Leading digit is in display window. Use standard calculator scientific notation
    390                 // with one digit to the left of the decimal point. Insert decimal point and
    391                 // delete leading zeroes.
    392                 // We try to keep leading digits roughly in position, and never
    393                 // lengthen the result by more than SCI_NOTATION_EXTRA.
    394                 final int resLen = result.length();
    395                 String fraction = result.substring(msdIndex + 1, resLen);
    396                 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
    397                         + "." + fraction;
    398                 // Original exp was correct for decimal point at right of fraction.
    399                 // Adjust by length of fraction.
    400                 exponent = initExponent + resLen - msdIndex - 1;
    401                 hasPoint = true;
    402             }
    403             if (exponent != 0 || truncated) {
    404                 // Actually add the exponent of either type:
    405                 if (!forcePrecision) {
    406                     int dropDigits;  // Digits to drop to make room for exponent.
    407                     if (hasPoint) {
    408                         // Type (1) exponent.
    409                         // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
    410                         dropDigits = expLen(exponent);
    411                         if (dropDigits >= result.length() - 1) {
    412                             // Jumpy is better than no mantissa.  Probably impossible anyway.
    413                             dropDigits = Math.max(result.length() - 2, 0);
    414                         }
    415                     } else {
    416                         // Type (2) exponent.
    417                         // Exponent depends on the number of digits we drop, which depends on
    418                         // exponent ...
    419                         for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
    420                                 ++dropDigits) {}
    421                         exponent = initExponent + dropDigits;
    422                         if (precOffset - dropDigits > mLsdOffset) {
    423                             // This can happen if e.g. result = 10^40 + 10^10
    424                             // It turns out we would otherwise display ...10e9 because it takes
    425                             // the same amount of space as ...1e10 but shows one more digit.
    426                             // But we don't want to display a trailing zero, even if it's free.
    427                             ++dropDigits;
    428                             ++exponent;
    429                         }
    430                     }
    431                     result = result.substring(0, result.length() - dropDigits);
    432                     lastDisplayedOffset[0] -= dropDigits;
    433                 }
    434                 result = result + "E" + Integer.toString(exponent);
    435             } // else don't add zero exponent
    436         }
    437         if (truncated || negative && result.charAt(0) != '-') {
    438             result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
    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      */
    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.
    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 
    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      */
    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      */
    500     public boolean isScrollable() {
    501         return mScrollable;
    502     }
    503 
    504     int getCurrentCharOffset() {
    505         synchronized(mWidthLock) {
    506             return (int) Math.round(mCurrentPos / mCharWidth);
    507         }
    508     }
    509 
    510     void clear() {
    511         mValid = false;
    512         mScrollable = false;
    513         setText("");
    514     }
    515 
    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
    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 
    613     public boolean stopActionMode() {
    614         if (mActionMode != null) {
    615             mActionMode.finish();
    616             return true;
    617         }
    618         return false;
    619     }
    620 
    621     private void setPrimaryClip(ClipData clip) {
    622         ClipboardManager clipboard = (ClipboardManager) getContext().
    623                                                getSystemService(Context.CLIPBOARD_SERVICE);
    624         clipboard.setPrimaryClip(clip);
    625     }
    626 
    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