Polish display and evaluate animation

Bug: 20915670
Bug: 21489377

- Adjust font metrics across all supported device configurations to
  support font scaling and min touch size requirements.
- Support proper font scaling for non-scrollable results when performing
  the evaluate animation.
- Remove restriction for only using 4/5 of the width of the result
  display (NOTE: the result's textSize must match the formula's
  minTextSize).
- Add AlignedTextView base class to ensure formula/result padding is
  based on the displayed text's ascent/baseline.

Change-Id: Id53e9bdc6e699fb05fdf331a6a472ecc170edf38
diff --git a/src/com/android/calculator2/AlignedTextView.java b/src/com/android/calculator2/AlignedTextView.java
new file mode 100644
index 0000000..1c4b78f
--- /dev/null
+++ b/src/com/android/calculator2/AlignedTextView.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calculator2;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+/**
+ * Extended {@link TextView} that supports ascent/baseline alignment.
+ */
+public class AlignedTextView extends TextView {
+
+    private static final String LATIN_CAPITAL_LETTER = "H";
+    private static final CharsetEncoder LATIN_CHARSET_ENCODER =
+            Charset.forName("ISO-8859-1").newEncoder();
+
+    // temporary rect for use during layout
+    private final Rect mTempRect = new Rect();
+
+    private int mTopPaddingOffset;
+    private int mBottomPaddingOffset;
+
+    public AlignedTextView(Context context) {
+        this(context, null /* attrs */);
+    }
+
+    public AlignedTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, android.R.attr.textViewStyle);
+    }
+
+    public AlignedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        // Disable any included font padding by default.
+        setIncludeFontPadding(false);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        CharSequence text = getText();
+        if (TextUtils.isEmpty(text) || LATIN_CHARSET_ENCODER.canEncode(text)) {
+            // For latin text align to the default capital letter height.
+            text = LATIN_CAPITAL_LETTER;
+        }
+        getPaint().getTextBounds(text.toString(), 0, text.length(), mTempRect);
+
+        final Paint textPaint = getPaint();
+        mTopPaddingOffset = Math.min(getPaddingTop(),
+                (int) Math.floor(mTempRect.top - textPaint.ascent()));
+        mBottomPaddingOffset = Math.min(getPaddingBottom(),
+                (int) Math.floor(textPaint.descent()));
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    public int getCompoundPaddingTop() {
+        return super.getCompoundPaddingTop() - mTopPaddingOffset;
+    }
+
+    @Override
+    public int getCompoundPaddingBottom() {
+        return super.getCompoundPaddingBottom() - mBottomPaddingOffset;
+    }
+}
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 3a4ac6d..0970174 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,8 +17,6 @@
 // FIXME: Menu handling, particularly for cut/paste, is very ugly
 //        and not the way it was intended.
 //        Other menus are not handled brilliantly either.
-// TODO: Revisit handling of "Help" menu, so that it's more consistent
-//       with our conventions.
 // TODO: Better indication of when the result is known to be exact.
 // TODO: Check and possibly fix accessability issues.
 // TODO: Copy & more general paste in formula?  Note that this requires
@@ -37,6 +35,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
@@ -51,6 +50,7 @@
 import android.text.SpannableString;
 import android.text.Spanned;
 import android.text.style.ForegroundColorSpan;
+import android.util.Property;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.Menu;
@@ -110,10 +110,21 @@
     // TODO: Possibly save a bit more information, e.g. its initial display string
     // or most significant digit position, to speed up restart.
 
+    private final Property<TextView, Integer> TEXT_COLOR =
+            new Property<TextView, Integer>(Integer.class, "textColor") {
+        @Override
+        public Integer get(TextView textView) {
+            return textView.getCurrentTextColor();
+        }
+
+        @Override
+        public void set(TextView textView, Integer textColor) {
+            textView.setTextColor(textColor);
+        }
+    };
+
     // We currently assume that the formula does not change out from under us in
     // any way. We explicitly handle all input to the formula here.
-    // TODO: Perhaps the formula should not be editable at all?
-
     private final OnKeyListener mFormulaOnKeyListener = new OnKeyListener() {
         @Override
         public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
@@ -132,17 +143,17 @@
                     return true;
                 default:
                     final int raw = keyEvent.getKeyCharacterMap()
-                          .get(keyCode, keyEvent.getMetaState());
+                            .get(keyCode, keyEvent.getMetaState());
                     if ((raw & KeyCharacterMap.COMBINING_ACCENT) != 0) {
                         return true; // discard
                     }
                     // Try to discard non-printing characters and the like.
                     // The user will have to explicitly delete other junk that gets past us.
                     if (Character.isIdentifierIgnorable(raw)
-                        || Character.isWhitespace(raw)) {
+                            || Character.isWhitespace(raw)) {
                         return true;
                     }
-                    char c = (char)raw;
+                    char c = (char) raw;
                     if (c == '=') {
                         mCurrentButton = mEqualButton;
                         onEquals();
@@ -168,7 +179,7 @@
     private View mDisplayView;
     private TextView mModeView;
     private CalculatorText mFormulaText;
-    private CalculatorResult mResult;
+    private CalculatorResult mResultText;
 
     private ViewPager mPadViewPager;
     private View mDeleteButton;
@@ -200,7 +211,7 @@
         mDisplayView = findViewById(R.id.display);
         mModeView = (TextView) findViewById(R.id.mode);
         mFormulaText = (CalculatorText) findViewById(R.id.formula);
-        mResult = (CalculatorResult) findViewById(R.id.result);
+        mResultText = (CalculatorResult) findViewById(R.id.result);
 
         mPadViewPager = (ViewPager) findViewById(R.id.pad_pager);
         mDeleteButton = findViewById(R.id.del);
@@ -224,8 +235,8 @@
                 findViewById(R.id.fun_arctan)
         };
 
-        mEvaluator = new Evaluator(this, mResult);
-        mResult.setEvaluator(mEvaluator);
+        mEvaluator = new Evaluator(this, mResultText);
+        mResultText.setEvaluator(mEvaluator);
         KeyMaps.setActivity(this);
 
         if (savedInstanceState != null) {
@@ -313,17 +324,14 @@
             }
 
             if (mCurrentState == CalculatorState.ERROR) {
-                final int errorColor = getResources().getColor(R.color.calculator_error_color);
+                final int errorColor = getColor(R.color.calculator_error_color);
                 mFormulaText.setTextColor(errorColor);
-                mResult.setTextColor(errorColor);
+                mResultText.setTextColor(errorColor);
                 getWindow().setStatusBarColor(errorColor);
-            } else {
-                mFormulaText.setTextColor(
-                        getResources().getColor(R.color.display_formula_text_color));
-                mResult.setTextColor(
-                        getResources().getColor(R.color.display_result_text_color));
-                getWindow().setStatusBarColor(
-                        getResources().getColor(R.color.calculator_accent_color));
+            } else if (mCurrentState != CalculatorState.RESULT) {
+                mFormulaText.setTextColor(getColor(R.color.display_formula_text_color));
+                mResultText.setTextColor(getColor(R.color.display_result_text_color));
+                getWindow().setStatusBarColor(getColor(R.color.calculator_accent_color));
             }
 
             invalidateOptionsMenu();
@@ -332,7 +340,7 @@
 
     // Stop any active ActionMode.  Return true if there was one.
     private boolean stopActionMode() {
-        if (mResult.stopActionMode()) {
+        if (mResultText.stopActionMode()) {
             return true;
         }
         if (mFormulaText.stopActionMode()) {
@@ -437,7 +445,7 @@
         if (mEvaluator.getExpr().hasInterestingOps()) {
             mEvaluator.evaluateAndShowResult();
         } else {
-            mResult.clear();
+            mResultText.clear();
         }
     }
 
@@ -482,7 +490,7 @@
                 onModeChanged(mode);
 
                 setState(CalculatorState.INPUT);
-                mResult.clear();
+                mResultText.clear();
                 if (mEvaluator.getExpr().hasInterestingOps()) {
                     mEvaluator.evaluateAndShowResult();
                 }
@@ -525,7 +533,7 @@
         // Invalidate any options that may depend on the current result.
         invalidateOptionsMenu();
 
-        mResult.displayResult(initDisplayPrec, leastDigPos, truncatedWholeNumber);
+        mResultText.displayResult(initDisplayPrec, leastDigPos, truncatedWholeNumber);
         if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
             onResult(mCurrentState != CalculatorState.INIT);
         }
@@ -535,13 +543,13 @@
         // We should be in EVALUATE state.
         // Display is still in input state.
         setState(CalculatorState.INPUT);
-        mResult.clear();
+        mResultText.clear();
     }
 
     // Reevaluation completed; ask result to redisplay current value.
     public void onReevaluate()
     {
-        mResult.redisplay();
+        mResultText.redisplay();
     }
 
     @Override
@@ -660,7 +668,7 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 mUnprocessedChars = null;
-                mResult.clear();
+                mResultText.clear();
                 mEvaluator.clear();
                 setState(CalculatorState.INPUT);
                 redisplayFormula();
@@ -677,14 +685,14 @@
                         @Override
                         public void onAnimationEnd(Animator animation) {
                            setState(CalculatorState.ERROR);
-                           mResult.displayError(errorResourceId);
+                           mResultText.displayError(errorResourceId);
                         }
                     });
         } else if (mCurrentState == CalculatorState.INIT) {
             setState(CalculatorState.ERROR);
-            mResult.displayError(errorResourceId);
+            mResultText.displayError(errorResourceId);
         } else {
-            mResult.clear();
+            mResultText.clear();
         }
     }
 
@@ -692,50 +700,49 @@
     // Animate movement of result into the top formula slot.
     // Result window now remains translated in the top slot while the result is displayed.
     // (We convert it back to formula use only when the user provides new input.)
-    // Historical note: In the Lollipop version, this invisibly and instantaeously moved
+    // Historical note: In the Lollipop version, this invisibly and instantaneously moved
     // formula and result displays back at the end of the animation.  We no longer do that,
     // so that we can continue to properly support scrolling of the result.
     // We assume the result already contains the text to be expanded.
     private void onResult(boolean animate) {
-        // Calculate the values needed to perform the scale and translation animations.
-        // The nominal font size in the result display is fixed.  But the magnification we
-        // use when the user hits "=" is variable, with a scrollable result always getting
-        // minimum magnification.
-        // Display.xml is designed to ensure that a 5/4 increase is always possible.
-        // More is possible if the display is not fully occupied.
-        // Pivot the result around the bottom of the text.
-        final float resultScale = (float)Math.min(1.25f / mResult.getOccupancy(), 2.0);
-        // Keep the right end of text fixed as we scale.
-        mResult.setPivotX(mResult.getWidth() - mResult.getPaddingRight());
-        // Move result up to take place of formula.  Scale around top of formula.
-        mResult.setPivotY(mResult.getPaddingTop());
-        float resultTranslationY = -mFormulaText.getHeight();
-        // Move formula off screen.
+        // Calculate the textSize that would be used to display the result in the formula.
+        // For scrollable results just use the minimum textSize to maximize the number of digits
+        // that are visible on screen.
+        float textSize = mFormulaText.getMinimumTextSize();
+        if (!mResultText.isScrollable()) {
+            textSize = mFormulaText.getVariableTextSize(mResultText.getText().toString());
+        }
+
+        // Scale the result to match the calculated textSize, minimizing the jump-cut transition
+        // when a result is reused in a subsequent expression.
+        final float resultScale = textSize / mResultText.getTextSize();
+
+        // Set the result's pivot to match its gravity.
+        mResultText.setPivotX(mResultText.getWidth() - mResultText.getPaddingRight());
+        mResultText.setPivotY(mResultText.getHeight() - mResultText.getPaddingBottom());
+
+        // Calculate the necessary translations so the result takes the place of the formula and
+        // the formula moves off the top of the screen.
+        final float resultTranslationY = (mFormulaText.getBottom() - mResultText.getBottom())
+                - (mFormulaText.getPaddingBottom() - mResultText.getPaddingBottom());
         final float formulaTranslationY = -mFormulaText.getBottom();
 
-        // TODO: Reintroduce textColorAnimator?
-        //       The initial and final colors seemed to be the same in L.
-        //       With the new model, the result logically changes back to a formula
-        //       only when we switch back to INPUT state, so it's unclear that animating
-        //       a color change here makes sense.
+        // Change the result's textColor to match the formula.
+        final int formulaTextColor = mFormulaText.getCurrentTextColor();
+
         if (animate) {
             final AnimatorSet animatorSet = new AnimatorSet();
             animatorSet.playTogether(
-                    ObjectAnimator.ofFloat(mResult, View.SCALE_X, resultScale),
-                    ObjectAnimator.ofFloat(mResult, View.SCALE_Y, resultScale),
-                    ObjectAnimator.ofFloat(mResult, View.TRANSLATION_Y, resultTranslationY),
-                    ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y,
-                                           formulaTranslationY));
-            animatorSet.setDuration(
-                    getResources().getInteger(android.R.integer.config_longAnimTime));
-            animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+                    ObjectAnimator.ofPropertyValuesHolder(mResultText,
+                            PropertyValuesHolder.ofFloat(View.SCALE_X, resultScale),
+                            PropertyValuesHolder.ofFloat(View.SCALE_Y, resultScale),
+                            PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, resultTranslationY)),
+                    ObjectAnimator.ofArgb(mResultText, TEXT_COLOR, formulaTextColor),
+                    ObjectAnimator.ofFloat(mFormulaText, View.TRANSLATION_Y, formulaTranslationY));
+            animatorSet.setDuration(getResources().getInteger(
+                    android.R.integer.config_longAnimTime));
             animatorSet.addListener(new AnimatorListenerAdapter() {
                 @Override
-                public void onAnimationStart(Animator animation) {
-                    // Result should already be displayed; no need to do anything.
-                }
-
-                @Override
                 public void onAnimationEnd(Animator animation) {
                     setState(CalculatorState.RESULT);
                     mCurrentAnimator = null;
@@ -745,9 +752,10 @@
             mCurrentAnimator = animatorSet;
             animatorSet.start();
         } else /* No animation desired; get there fast, e.g. when restarting */ {
-            mResult.setScaleX(resultScale);
-            mResult.setScaleY(resultScale);
-            mResult.setTranslationY(resultTranslationY);
+            mResultText.setScaleX(resultScale);
+            mResultText.setScaleY(resultScale);
+            mResultText.setTranslationY(resultTranslationY);
+            mResultText.setTextColor(formulaTextColor);
             mFormulaText.setTranslationY(formulaTranslationY);
             setState(CalculatorState.RESULT);
         }
@@ -757,12 +765,12 @@
     // pre-animation state.
     private void restoreDisplayPositions() {
         // Clear result.
-        mResult.setText("");
+        mResultText.setText("");
         // Reset all of the values modified during the animation.
-        mResult.setScaleX(1.0f);
-        mResult.setScaleY(1.0f);
-        mResult.setTranslationX(0.0f);
-        mResult.setTranslationY(0.0f);
+        mResultText.setScaleX(1.0f);
+        mResultText.setScaleY(1.0f);
+        mResultText.setTranslationX(0.0f);
+        mResultText.setTranslationY(0.0f);
         mFormulaText.setTranslationY(0.0f);
 
         mFormulaText.requestFocus();
@@ -808,13 +816,10 @@
     }
 
     private void displayMessage(String s) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setMessage(s)
-               .setNegativeButton(R.string.dismiss,
-                    new DialogInterface.OnClickListener() {
-                        public void onClick(DialogInterface d, int which) { }
-                    })
-               .show();
+        new AlertDialog.Builder(this)
+                .setMessage(s)
+                .setNegativeButton(R.string.dismiss, null /* listener */)
+                .show();
     }
 
     private void displayFraction() {
@@ -825,8 +830,8 @@
     // Display full result to currently evaluated precision
     private void displayFull() {
         Resources res = getResources();
-        String msg = mResult.getFullText() + " ";
-        if (mResult.fullTextIsExact()) {
+        String msg = mResultText.getFullText() + " ";
+        if (mResultText.fullTextIsExact()) {
             msg += res.getString(R.string.exact);
         } else {
             msg += res.getString(R.string.approximate);
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index a916c30..5b4fb86 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -16,23 +16,16 @@
 
 package com.android.calculator2;
 
-import android.content.ClipboardManager;
 import android.content.ClipData;
 import android.content.ClipDescription;
+import android.content.ClipboardManager;
 import android.content.Context;
-import android.graphics.Typeface;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.Color;
-import android.net.Uri;
-import android.widget.TextView;
-import android.widget.OverScroller;
-import android.text.Editable;
+import android.text.Layout;
 import android.text.SpannableString;
 import android.text.Spanned;
+import android.text.TextPaint;
 import android.text.style.ForegroundColorSpan;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.ActionMode;
 import android.view.GestureDetector;
 import android.view.Menu;
@@ -40,14 +33,12 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import android.widget.OverScroller;
 import android.widget.Toast;
 
-import android.support.v4.view.ViewCompat;
-
-
 // A text widget that is "infinitely" scrollable to the right,
 // and obtains the text to display via a callback to Logic.
-public class CalculatorResult extends TextView {
+public class CalculatorResult extends AlignedTextView {
     static final int MAX_RIGHT_SCROLL = 10000000;
     static final int INVALID = MAX_RIGHT_SCROLL + 10000;
         // A larger value is unlikely to avoid running out of space
@@ -56,8 +47,7 @@
     class MyTouchListener implements View.OnTouchListener {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
-            boolean res = mGestureDetector.onTouchEvent(event);
-            return res;
+            return mGestureDetector.onTouchEvent(event);
         }
     }
     final MyTouchListener mTouchListener = new MyTouchListener();
@@ -75,11 +65,11 @@
     private int mMinPos;    // Minimum position before all digits disappear off the right. Pixels.
     private int mMaxPos;    // Maximum position before we start displaying the infinite
                             // sequence of trailing zeroes on the right. Pixels.
-    private Object mWidthLock = new Object();
+    private final Object mWidthLock = new Object();
                             // Protects the next two fields.
     private int mWidthConstraint = -1;
                             // Our total width in pixels.
-    private int mCharWidth = 1;
+    private float mCharWidth = 1;
                             // Maximum character width. For now we pretend that all characters
                             // have this width.
                             // TODO: We're not really using a fixed width font.  But it appears
@@ -112,7 +102,7 @@
                     if (!mScrollable) return true;
                     mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0  /* horizontal only */,
                                     mMinPos, mMaxPos, 0, 0);
-                    ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
+                    postInvalidateOnAnimation();
                     return true;
                 }
                 @Override
@@ -134,7 +124,7 @@
                     int duration = (int)(e2.getEventTime() - e1.getEventTime());
                     if (duration < 1 || duration > 100) duration = 10;
                     mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
-                    ViewCompat.postInvalidateOnAnimation(CalculatorResult.this);
+                    postInvalidateOnAnimation();
                     return true;
                 }
                 @Override
@@ -163,21 +153,11 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        char testChar = KeyMaps.translateResult("5").charAt(0);
-        // TODO: Redo on Locale change?  Doesn't seem to matter?
-        // We try to determine the maximal size of a digit plus corresponding inter-character
-        // space. We assume that "5" has maximal width.  Since any string includes one fewer
-        // inter-character space than characters, me measure one that's longer than any real
-        // display string, and then divide by the number of characters.  This should bound
-        // the per-character space we need for any real string.
-        StringBuilder sb = new StringBuilder(MAX_WIDTH);
-        for (int i = 0; i < MAX_WIDTH; ++i) {
-            sb.append(testChar);
-        }
-        final int newWidthConstraint =
-                MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
-        final int newCharWidth =
-                (int)Math.ceil(getPaint().measureText(sb.toString()) / MAX_WIDTH);
+        final TextPaint paint = getPaint();
+        final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
+                - (getPaddingLeft() + getPaddingRight())
+                - (int) Math.ceil(Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint));
+        final float newCharWidth = Layout.getDesiredWidth("\u2007", paint);
         synchronized(mWidthLock) {
             mWidthConstraint = newWidthConstraint;
             mCharWidth = newCharWidth;
@@ -208,15 +188,16 @@
     void displayResult(int initPrec, int leastDigPos, String truncatedWholePart) {
         mLastPos = INVALID;
         synchronized(mWidthLock) {
-            mCurrentPos = initPrec * mCharWidth;
+            mCurrentPos = (int) Math.ceil(initPrec * mCharWidth);
         }
         // Should logically be
         // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
         // we eventually transalate to a character position by dividing by mCharWidth.
         // To avoid rounding issues, we use the analogous computation here.
-        mMinPos = - truncatedWholePart.length() * mCharWidth;
+        mMinPos = - (int) Math.ceil(truncatedWholePart.length() * mCharWidth);
         if (leastDigPos < MAX_RIGHT_SCROLL) {
-            mMaxPos = Math.min(addExpSpace(leastDigPos) * mCharWidth, MAX_RIGHT_SCROLL);
+            mMaxPos = Math.min((int) Math.ceil(addExpSpace(leastDigPos) * mCharWidth),
+                    MAX_RIGHT_SCROLL);
         } else {
             mMaxPos = MAX_RIGHT_SCROLL;
         }
@@ -350,11 +331,9 @@
      * May be called asynchronously from non-UI thread.
      */
     int getMaxChars() {
-        // We only use 4/5 of the available space, since at least the left 4/5 of the result
-        // is not visible when it is shown in large size.
         int result;
         synchronized(mWidthLock) {
-            result = 4 * mWidthConstraint / (5 * mCharWidth);
+            result = (int) Math.floor(mWidthConstraint / mCharWidth);
             // We can apparently finish evaluating before onMeasure in CalculatorText has been
             // called, in which case we get 0 or -1 as the width constraint.
         }
@@ -362,26 +341,22 @@
             // Return something conservatively big, to force sufficient evaluation.
             return MAX_WIDTH;
         } else {
-            return result;
+            // Always allow for the ellipsis character which already accounted for in the width
+            // constraint.
+            return result + 1;
         }
     }
 
     /**
-     * Return the fraction of the available character space occupied by the
-     * current result.
-     * Should be called only with a valid result displayed.
+     * @return {@code true} if the currently displayed result is scrollable
      */
-    float getOccupancy() {
-        if (mScrollable) {
-            return 1.0f;
-        } else {
-            return (float)getText().length() / getMaxChars();
-        }
+    public boolean isScrollable() {
+        return mScrollable;
     }
 
     int getCurrentCharPos() {
         synchronized(mWidthLock) {
-            return mCurrentPos/mCharWidth;
+            return (int) Math.ceil(mCurrentPos / mCharWidth);
         }
     }
 
@@ -419,7 +394,7 @@
                 redisplay();
             }
             if (!mScroller.isFinished()) {
-                ViewCompat.postInvalidateOnAnimation(this);
+                postInvalidateOnAnimation();
             }
         }
     }
diff --git a/src/com/android/calculator2/CalculatorText.java b/src/com/android/calculator2/CalculatorText.java
index 1b16bca..c6f38ae 100644
--- a/src/com/android/calculator2/CalculatorText.java
+++ b/src/com/android/calculator2/CalculatorText.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,39 +16,31 @@
 
 package com.android.calculator2;
 
-import android.content.ClipboardManager;
 import android.content.ClipData;
+import android.content.ClipboardManager;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Paint;
-import android.graphics.Paint.FontMetricsInt;
-import android.graphics.Rect;
 import android.net.Uri;
-import android.os.Parcelable;
-import android.text.method.ScrollingMovementMethod;
 import android.text.TextPaint;
+import android.text.method.ScrollingMovementMethod;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.TypedValue;
 import android.view.ActionMode;
-import android.view.GestureDetector;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.MotionEvent;
 import android.view.View;
 import android.widget.TextView;
 
 /**
  * TextView adapted for Calculator display.
  */
-
-public class CalculatorText extends TextView implements View.OnLongClickListener{
+public class CalculatorText extends AlignedTextView implements View.OnLongClickListener {
 
     private ActionMode mActionMode;
 
-    private final ActionMode.Callback mPasteActionModeCallback =
-            new ActionMode.Callback() {
+    private final ActionMode.Callback mPasteActionModeCallback = new ActionMode.Callback() {
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
             switch (item.getItemId()) {
@@ -109,9 +101,8 @@
     private final float mMinimumTextSize;
     private final float mStepTextSize;
 
-    // Temporary objects for use in layout methods.
+    // Temporary paint for use in layout methods.
     private final Paint mTempPaint = new TextPaint();
-    private final Rect mTempRect = new Rect();
 
     private int mWidthConstraint = -1;
     private OnTextSizeChangeListener mOnTextSizeChangeListener;
@@ -146,7 +137,6 @@
         setMovementMethod(ScrollingMovementMethod.getInstance());
 
         setTextSize(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize);
-        setMinHeight(getLineHeight() + getCompoundPaddingBottom() + getCompoundPaddingTop());
     }
 
     @Override
@@ -159,8 +149,13 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        mWidthConstraint =
-                MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
+        // Prevent shrinking/resizing with our variable textSize.
+        if (!isLaidOut()) {
+            setMinHeight(getLineHeight() + getCompoundPaddingBottom() + getCompoundPaddingTop());
+        }
+
+        mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
+                - getPaddingLeft() - getPaddingRight();
         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(getText().toString()));
     }
 
@@ -170,7 +165,6 @@
     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter);
 
-        final int textLength = text.length();
         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
     }
 
@@ -188,6 +182,14 @@
         mOnTextSizeChangeListener = listener;
     }
 
+    public float getMinimumTextSize() {
+        return mMinimumTextSize;
+    }
+
+    public float getMaximumTextSize() {
+        return mMaximumTextSize;
+    }
+
     public float getVariableTextSize(String text) {
         if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
             // Not measured, bail early.
@@ -212,25 +214,6 @@
         return lastFitTextSize;
     }
 
-    @Override
-    public int getCompoundPaddingTop() {
-        // Measure the top padding from the capital letter height of the text instead of the top,
-        // but don't remove more than the available top padding otherwise clipping may occur.
-        getPaint().getTextBounds("H", 0, 1, mTempRect);
-
-        final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
-        final int paddingOffset = -(fontMetrics.ascent + mTempRect.height());
-        return super.getCompoundPaddingTop() - Math.min(getPaddingTop(), paddingOffset);
-    }
-
-    @Override
-    public int getCompoundPaddingBottom() {
-        // Measure the bottom padding from the baseline of the text instead of the bottom, but don't
-        // remove more than the available bottom padding otherwise clipping may occur.
-        final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
-        return super.getCompoundPaddingBottom() - Math.min(getPaddingBottom(), fontMetrics.descent);
-    }
-
     public boolean stopActionMode() {
         if (mActionMode != null) {
             mActionMode.finish();
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 6362efe..4ddaf15 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -796,6 +796,14 @@
         return mDegreeMode;
     }
 
+    /**
+     * @return the {@link CalculatorExpr} representation of the current result
+     */
+    CalculatorExpr getResultExpr() {
+        final BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
+        return mExpr.abbreviate(mVal, mRatVal, mDegreeMode, getShortString(mCache, intVal));
+    }
+
     // Abbreviate the current expression to a pre-evaluated
     // expression node, which will display as a short number.
     // This should not be called unless the expression was
@@ -805,11 +813,8 @@
     // diverges.  Subsequent re-evaluation will also not diverge,
     // though it may generate errors of various kinds.
     // E.g. sqrt(-10^-1000)
-    void collapse () {
-        BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
-        CalculatorExpr abbrvExpr = mExpr.abbreviate(
-                                      mVal, mRatVal, mDegreeMode,
-                                      getShortString(mCache, intVal));
+    void collapse() {
+        final CalculatorExpr abbrvExpr = getResultExpr();
         clear();
         mExpr.append(abbrvExpr);
     }
@@ -817,11 +822,11 @@
     // Same as above, but put result in mSaved, leaving mExpr alone.
     // Return false if result is unavailable.
     boolean collapseToSaved() {
-        if (mCache == null) return false;
-        BigInteger intVal = BoundedRational.asBigInteger(mRatVal);
-        CalculatorExpr abbrvExpr = mExpr.abbreviate(
-                                      mVal, mRatVal, mDegreeMode,
-                                      getShortString(mCache, intVal));
+        if (mCache == null) {
+            return false;
+        }
+
+        final CalculatorExpr abbrvExpr = getResultExpr();
         mSaved.clear();
         mSaved.append(abbrvExpr);
         return true;