blob: 29c20f7e46c14eb4303c1f38a15d8d44ebecbeac [file] [log] [blame]
Hans Boehm84614952014-11-25 18:46:17 -08001/*
Hans Boehm24c91ed2016-06-30 18:53:44 -07002 * Copyright (C) 2016 The Android Open Source Project
Hans Boehm84614952014-11-25 18:46:17 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calculator2;
18
Chenjie Yu3937b652016-06-01 23:14:26 -070019import android.annotation.TargetApi;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070020import android.content.ClipData;
21import android.content.ClipDescription;
Justin Klaassen44595162015-05-28 17:55:20 -070022import android.content.ClipboardManager;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070023import android.content.Context;
Hans Boehm7f83e362015-06-10 15:41:04 -070024import android.graphics.Rect;
Chenjie Yu3937b652016-06-01 23:14:26 -070025import android.os.Build;
26import android.support.v4.content.ContextCompat;
Justin Klaassenf1b61f42016-04-27 16:00:11 -070027import android.support.v4.os.BuildCompat;
Justin Klaassen44595162015-05-28 17:55:20 -070028import android.text.Layout;
Hans Boehm7f83e362015-06-10 15:41:04 -070029import android.text.Spannable;
Hans Boehm84614952014-11-25 18:46:17 -080030import android.text.SpannableString;
Hans Boehm1176f232015-05-11 16:26:03 -070031import android.text.Spanned;
Justin Klaassen44595162015-05-28 17:55:20 -070032import android.text.TextPaint;
Hans Boehm7f83e362015-06-10 15:41:04 -070033import android.text.style.BackgroundColorSpan;
Hans Boehm84614952014-11-25 18:46:17 -080034import android.text.style.ForegroundColorSpan;
Hans Boehm14344ff2016-06-08 13:01:51 -070035import android.text.style.RelativeSizeSpan;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070036import android.util.AttributeSet;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070037import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070038import android.view.ContextMenu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070039import android.view.GestureDetector;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.view.MotionEvent;
44import android.view.View;
Justin Klaassen44595162015-05-28 17:55:20 -070045import android.widget.OverScroller;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070046import android.widget.Toast;
Hans Boehm84614952014-11-25 18:46:17 -080047
Hans Boehm84614952014-11-25 18:46:17 -080048// A text widget that is "infinitely" scrollable to the right,
49// and obtains the text to display via a callback to Logic.
Hans Boehm8f051c32016-10-03 16:53:58 -070050public class CalculatorResult extends AlignedTextView implements MenuItem.OnMenuItemClickListener,
51 Evaluator.EvaluationListener, Evaluator.CharMetricsInfo {
Hans Boehm61568a12015-05-18 18:25:41 -070052 static final int MAX_RIGHT_SCROLL = 10000000;
Hans Boehm08e8f322015-04-21 13:18:38 -070053 static final int INVALID = MAX_RIGHT_SCROLL + 10000;
Hans Boehm84614952014-11-25 18:46:17 -080054 // A larger value is unlikely to avoid running out of space
55 final OverScroller mScroller;
56 final GestureDetector mGestureDetector;
Hans Boehm8f051c32016-10-03 16:53:58 -070057 private long mIndex; // Index of expression we are displaying.
Hans Boehm84614952014-11-25 18:46:17 -080058 private Evaluator mEvaluator;
59 private boolean mScrollable = false;
60 // A scrollable result is currently displayed.
Hans Boehm760a9dc2015-04-20 10:27:12 -070061 private boolean mValid = false;
Hans Boehmc01cd7f2015-05-12 18:32:19 -070062 // The result holds something valid; either a a number or an error
63 // message.
Hans Boehm5e802f32015-06-22 17:18:52 -070064 // A suffix of "Pos" denotes a pixel offset. Zero represents a scroll position
65 // in which the decimal point is just barely visible on the right of the display.
Hans Boehmc01cd7f2015-05-12 18:32:19 -070066 private int mCurrentPos;// Position of right of display relative to decimal point, in pixels.
67 // Large positive values mean the decimal point is scrolled off the
68 // left of the display. Zero means decimal point is barely displayed
69 // on the right.
Hans Boehm61568a12015-05-18 18:25:41 -070070 private int mLastPos; // Position already reflected in display. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080071 private int mMinPos; // Minimum position to avoid unnecessary blanks on the left. Pixels.
Hans Boehm61568a12015-05-18 18:25:41 -070072 private int mMaxPos; // Maximum position before we start displaying the infinite
73 // sequence of trailing zeroes on the right. Pixels.
Hans Boehm65a99a42016-02-03 18:16:07 -080074 private int mWholeLen; // Length of the whole part of current result.
Hans Boehm5e802f32015-06-22 17:18:52 -070075 // In the following, we use a suffix of Offset to denote a character position in a numeric
76 // string relative to the decimal point. Positive is to the right and negative is to
77 // the left. 1 = tenths position, -1 = units. Integer.MAX_VALUE is sometimes used
78 // for the offset of the last digit in an a nonterminating decimal expansion.
79 // We use the suffix "Index" to denote a zero-based index into a string representing a
80 // result.
Hans Boehm5e802f32015-06-22 17:18:52 -070081 private int mMaxCharOffset; // Character offset from decimal point of rightmost digit
Hans Boehm24c91ed2016-06-30 18:53:44 -070082 // that should be displayed, plus the length of any exponent
83 // needed to display that digit.
84 // Limited to MAX_RIGHT_SCROLL. Often the same as:
Hans Boehm5e802f32015-06-22 17:18:52 -070085 private int mLsdOffset; // Position of least-significant digit in result
86 private int mLastDisplayedOffset; // Offset of last digit actually displayed after adding
Hans Boehmf6dae112015-06-18 17:57:50 -070087 // exponent.
Hans Boehm24c91ed2016-06-30 18:53:44 -070088 private boolean mWholePartFits; // Scientific notation not needed for initial display.
89 private float mNoExponentCredit;
90 // Fraction of digit width saved by avoiding scientific notation.
91 // Only accessed from UI thread.
92 private boolean mAppendExponent;
93 // The result fits entirely in the display, even with an exponent,
94 // but not with grouping separators. Since the result is not
95 // scrollable, and we do not add the exponent to max. scroll position,
96 // append an exponent insteadd of replacing trailing digits.
Justin Klaassen44595162015-05-28 17:55:20 -070097 private final Object mWidthLock = new Object();
Hans Boehm24c91ed2016-06-30 18:53:44 -070098 // Protects the next five fields. These fields are only
99 // Updated by the UI thread, and read accesses by the UI thread
100 // sometimes do not acquire the lock.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700101 private int mWidthConstraint = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700102 // Our total width in pixels minus space for ellipsis.
Justin Klaassen44595162015-05-28 17:55:20 -0700103 private float mCharWidth = 1;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700104 // Maximum character width. For now we pretend that all characters
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700105 // have this width.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700106 // TODO: We're not really using a fixed width font. But it appears
107 // to be close enough for the characters we use that the difference
108 // is not noticeable.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700109 private float mGroupingSeparatorWidthRatio;
110 // Fraction of digit width occupied by a digit separator.
111 private float mDecimalCredit;
112 // Fraction of digit width saved by replacing digit with decimal point.
113 private float mNoEllipsisCredit;
114 // Fraction of digit width saved by both replacing ellipsis with digit
115 // and avoiding scientific notation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700116 private static final int MAX_WIDTH = 100;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700117 // Maximum number of digits displayed.
Hans Boehm50ed3202015-06-09 14:35:49 -0700118 public static final int MAX_LEADING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700119 // Maximum number of leading zeroes after decimal point before we
120 // switch to scientific notation with negative exponent.
Hans Boehm50ed3202015-06-09 14:35:49 -0700121 public static final int MAX_TRAILING_ZEROES = 6;
Hans Boehma0e45f32015-05-30 13:20:35 -0700122 // Maximum number of trailing zeroes before the decimal point before
123 // we switch to scientific notation with positive exponent.
124 private static final int SCI_NOTATION_EXTRA = 1;
125 // Extra digits for standard scientific notation. In this case we
Hans Boehm80018c82015-08-02 16:59:07 -0700126 // have a decimal point and no ellipsis.
127 // We assume that we do not drop digits to make room for the decimal
128 // point in ordinary scientific notation. Thus >= 1.
Hans Boehm65a99a42016-02-03 18:16:07 -0800129 private static final int MAX_COPY_EXTRA = 100;
130 // The number of extra digits we are willing to compute to copy
131 // a result as an exact number.
132 private static final int MAX_RECOMPUTE_DIGITS = 2000;
133 // The maximum number of digits we're willing to recompute in the UI
134 // thread. We only do this for known rational results, where we
135 // can bound the computation cost.
Chenjie Yu3937b652016-06-01 23:14:26 -0700136 private final ForegroundColorSpan mExponentColorSpan;
137 private final BackgroundColorSpan mHighlightSpan;
Hans Boehm65a99a42016-02-03 18:16:07 -0800138
Hans Boehm1176f232015-05-11 16:26:03 -0700139 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -0700140 private ActionMode.Callback mCopyActionModeCallback;
141 private ContextMenu mContextMenu;
Hans Boehm84614952014-11-25 18:46:17 -0800142
143 public CalculatorResult(Context context, AttributeSet attrs) {
144 super(context, attrs);
145 mScroller = new OverScroller(context);
Chenjie Yu3937b652016-06-01 23:14:26 -0700146 mHighlightSpan = new BackgroundColorSpan(getHighlightColor());
147 mExponentColorSpan = new ForegroundColorSpan(
148 ContextCompat.getColor(context, R.color.display_result_exponent_text_color));
Hans Boehm84614952014-11-25 18:46:17 -0800149 mGestureDetector = new GestureDetector(context,
150 new GestureDetector.SimpleOnGestureListener() {
151 @Override
Justin Klaassend48b7562015-04-16 16:51:38 -0700152 public boolean onDown(MotionEvent e) {
153 return true;
154 }
155 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800156 public boolean onFling(MotionEvent e1, MotionEvent e2,
157 float velocityX, float velocityY) {
158 if (!mScroller.isFinished()) {
159 mCurrentPos = mScroller.getFinalX();
160 }
161 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700162 stopActionModeOrContextMenu();
Hans Boehmfbcef702015-04-27 18:07:47 -0700163 CalculatorResult.this.cancelLongPress();
164 // Ignore scrolls of error string, etc.
165 if (!mScrollable) return true;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700166 mScroller.fling(mCurrentPos, 0, - (int) velocityX, 0 /* horizontal only */,
Hans Boehm61568a12015-05-18 18:25:41 -0700167 mMinPos, mMaxPos, 0, 0);
Justin Klaassen44595162015-05-28 17:55:20 -0700168 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800169 return true;
170 }
171 @Override
172 public boolean onScroll(MotionEvent e1, MotionEvent e2,
173 float distanceX, float distanceY) {
Hans Boehm61568a12015-05-18 18:25:41 -0700174 int distance = (int)distanceX;
Hans Boehm84614952014-11-25 18:46:17 -0800175 if (!mScroller.isFinished()) {
176 mCurrentPos = mScroller.getFinalX();
177 }
178 mScroller.forceFinished(true);
Chenjie Yu3937b652016-06-01 23:14:26 -0700179 stopActionModeOrContextMenu();
Hans Boehm84614952014-11-25 18:46:17 -0800180 CalculatorResult.this.cancelLongPress();
181 if (!mScrollable) return true;
Hans Boehm61568a12015-05-18 18:25:41 -0700182 if (mCurrentPos + distance < mMinPos) {
183 distance = mMinPos - mCurrentPos;
184 } else if (mCurrentPos + distance > mMaxPos) {
185 distance = mMaxPos - mCurrentPos;
186 }
Hans Boehm84614952014-11-25 18:46:17 -0800187 int duration = (int)(e2.getEventTime() - e1.getEventTime());
188 if (duration < 1 || duration > 100) duration = 10;
Hans Boehm61568a12015-05-18 18:25:41 -0700189 mScroller.startScroll(mCurrentPos, 0, distance, 0, (int)duration);
Justin Klaassen44595162015-05-28 17:55:20 -0700190 postInvalidateOnAnimation();
Hans Boehm84614952014-11-25 18:46:17 -0800191 return true;
192 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700193 @Override
194 public void onLongPress(MotionEvent e) {
Hans Boehm1176f232015-05-11 16:26:03 -0700195 if (mValid) {
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800196 performLongClick();
Hans Boehm1176f232015-05-11 16:26:03 -0700197 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700198 }
Hans Boehm84614952014-11-25 18:46:17 -0800199 });
Justin Klaassen3a05c7e2016-03-04 12:40:02 -0800200 setOnTouchListener(new View.OnTouchListener() {
201 @Override
202 public boolean onTouch(View v, MotionEvent event) {
203 return mGestureDetector.onTouchEvent(event);
204 }
205 });
Hans Boehm14344ff2016-06-08 13:01:51 -0700206
Chenjie Yu3937b652016-06-01 23:14:26 -0700207 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
208 setupActionMode();
209 } else {
210 setupContextMenu();
211 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700212
Hans Boehm84614952014-11-25 18:46:17 -0800213 setCursorVisible(false);
Christine Franksafe28bb2016-07-29 17:24:52 -0700214 setLongClickable(false);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700215 setContentDescription(context.getString(R.string.desc_result));
Hans Boehm84614952014-11-25 18:46:17 -0800216 }
217
Hans Boehm8f051c32016-10-03 16:53:58 -0700218 void setEvaluator(Evaluator evaluator, long index) {
Hans Boehm84614952014-11-25 18:46:17 -0800219 mEvaluator = evaluator;
Hans Boehm8f051c32016-10-03 16:53:58 -0700220 mIndex = index;
Hans Boehm84614952014-11-25 18:46:17 -0800221 }
222
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700223 // Compute maximum digit width the hard way.
224 private static float getMaxDigitWidth(TextPaint paint) {
225 // Compute the maximum advance width for each digit, thus accounting for between-character
226 // spaces. If we ever support other kinds of digits, we may have to avoid kerning effects
227 // that could reduce the advance width within this particular string.
228 final String allDigits = "0123456789";
229 final float[] widths = new float[allDigits.length()];
230 paint.getTextWidths(allDigits, widths);
231 float maxWidth = 0;
232 for (float x : widths) {
233 maxWidth = Math.max(x, maxWidth);
234 }
235 return maxWidth;
236 }
237
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700238 @Override
239 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassend06f51d2016-08-03 00:41:31 -0700240 if (!isLaidOut()) {
241 // Set a minimum height so scaled error messages won't affect our layout.
242 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
243 + getCompoundPaddingTop());
244 }
245
Justin Klaassen44595162015-05-28 17:55:20 -0700246 final TextPaint paint = getPaint();
Hans Boehm80018c82015-08-02 16:59:07 -0700247 final Context context = getContext();
Hans Boehmcd72f7e2016-06-01 16:21:25 -0700248 final float newCharWidth = getMaxDigitWidth(paint);
Hans Boehm80018c82015-08-02 16:59:07 -0700249 // Digits are presumed to have no more than newCharWidth.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700250 // There are two instances when we know that the result is otherwise narrower than
251 // expected:
252 // 1. For standard scientific notation (our type 1), we know that we have a norrow decimal
253 // point and no (usually wide) ellipsis symbol. We allow one extra digit
254 // (SCI_NOTATION_EXTRA) to compensate, and consider that in determining available width.
255 // 2. If we are using digit grouping separators and a decimal point, we give ourselves
256 // a fractional extra space for those separators, the value of which depends on whether
257 // there is also an ellipsis.
258 //
259 // Maximum extra space we need in various cases:
260 // Type 1 scientific notation, assuming ellipsis, minus sign and E are wider than a digit:
261 // Two minus signs + "E" + "." - 3 digits.
262 // Type 2 scientific notation:
263 // Ellipsis + "E" + "-" - 3 digits.
264 // In the absence of scientific notation, we may need a little less space.
265 // We give ourselves a bit of extra credit towards comma insertion and give
266 // ourselves more if we have either
267 // No ellipsis, or
268 // A decimal separator.
269
270 // Calculate extra space we need to reserve, in addition to character count.
Hans Boehm80018c82015-08-02 16:59:07 -0700271 final float decimalSeparatorWidth = Layout.getDesiredWidth(
272 context.getString(R.string.dec_point), paint);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700273 final float minusWidth = Layout.getDesiredWidth(context.getString(R.string.op_sub), paint);
274 final float minusExtraWidth = Math.max(minusWidth - newCharWidth, 0.0f);
275 final float ellipsisWidth = Layout.getDesiredWidth(KeyMaps.ELLIPSIS, paint);
276 final float ellipsisExtraWidth = Math.max(ellipsisWidth - newCharWidth, 0.0f);
277 final float expWidth = Layout.getDesiredWidth(KeyMaps.translateResult("e"), paint);
278 final float expExtraWidth = Math.max(expWidth - newCharWidth, 0.0f);
279 final float type1Extra = 2 * minusExtraWidth + expExtraWidth + decimalSeparatorWidth;
280 final float type2Extra = ellipsisExtraWidth + expExtraWidth + minusExtraWidth;
281 final float extraWidth = Math.max(type1Extra, type2Extra);
282 final int intExtraWidth = (int) Math.ceil(extraWidth) + 1 /* to cover rounding sins */;
Hans Boehm80018c82015-08-02 16:59:07 -0700283 final int newWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Hans Boehm24c91ed2016-06-30 18:53:44 -0700284 - (getPaddingLeft() + getPaddingRight()) - intExtraWidth;
285
286 // Calculate other width constants we need to handle grouping separators.
287 final float groupingSeparatorW =
288 Layout.getDesiredWidth(KeyMaps.translateResult(","), paint);
289 // Credits in the absence of any scientific notation:
290 float noExponentCredit = extraWidth - Math.max(ellipsisExtraWidth, minusExtraWidth);
291 final float noEllipsisCredit = extraWidth - minusExtraWidth; // includes noExponentCredit.
292 final float decimalCredit = Math.max(newCharWidth - decimalSeparatorWidth, 0.0f);
293
294 mNoExponentCredit = noExponentCredit / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700295 synchronized(mWidthLock) {
Hans Boehm013969e2015-04-13 20:29:47 -0700296 mWidthConstraint = newWidthConstraint;
297 mCharWidth = newCharWidth;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700298 mNoEllipsisCredit = noEllipsisCredit / newCharWidth;
299 mDecimalCredit = decimalCredit / newCharWidth;
300 mGroupingSeparatorWidthRatio = groupingSeparatorW / newCharWidth;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700301 }
Hans Boehm14344ff2016-06-08 13:01:51 -0700302
303 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700304 }
305
Annie Chin06fd3cf2016-11-07 16:04:33 -0800306 @Override
307 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
308 super.onLayout(changed, left, top, right, bottom);
309
310 if (mEvaluator != null) {
311 final CalculatorExpr expr = mEvaluator.getExpr(mIndex);
312 if (expr != null && expr.hasInterestingOps()) {
313 mEvaluator.requireResult(mIndex, this, this);
314 }
315 }
316 }
317
Hans Boehm8f051c32016-10-03 16:53:58 -0700318 // From Evaluator.CharMetricsInfo.
319 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700320 public float separatorChars(String s, int len) {
321 int start = 0;
322 while (start < len && !Character.isDigit(s.charAt(start))) {
323 ++start;
324 }
325 // We assume the rest consists of digits, and for consistency with the rest
326 // of the code, we assume all digits have width mCharWidth.
327 final int nDigits = len - start;
328 // We currently insert a digit separator every three digits.
329 final int nSeparators = (nDigits - 1) / 3;
330 synchronized(mWidthLock) {
331 // Always return an upper bound, even in the presence of rounding errors.
332 return nSeparators * mGroupingSeparatorWidthRatio;
333 }
334 }
335
Hans Boehm8f051c32016-10-03 16:53:58 -0700336 // From Evaluator.CharMetricsInfo.
337 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700338 public float getNoEllipsisCredit() {
339 synchronized(mWidthLock) {
340 return mNoEllipsisCredit;
341 }
342 }
343
Hans Boehm8f051c32016-10-03 16:53:58 -0700344 // From Evaluator.CharMetricsInfo.
345 @Override
Hans Boehm24c91ed2016-06-30 18:53:44 -0700346 public float getDecimalCredit() {
347 synchronized(mWidthLock) {
348 return mDecimalCredit;
349 }
350 }
351
Hans Boehma0e45f32015-05-30 13:20:35 -0700352 // Return the length of the exponent representation for the given exponent, in
353 // characters.
354 private final int expLen(int exp) {
355 if (exp == 0) return 0;
Hans Boehm5e802f32015-06-22 17:18:52 -0700356 final int abs_exp_digits = (int) Math.ceil(Math.log10(Math.abs((double)exp))
357 + 0.0000000001d /* Round whole numbers to next integer */);
358 return abs_exp_digits + (exp >= 0 ? 1 : 2);
Hans Boehm61568a12015-05-18 18:25:41 -0700359 }
360
Hans Boehma0e45f32015-05-30 13:20:35 -0700361 /**
362 * Initiate display of a new result.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700363 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700364 * The parameters specify various properties of the result.
Hans Boehm8f051c32016-10-03 16:53:58 -0700365 * @param index Index of expression that was just evaluated. Currently ignored, since we only
366 * expect notification for the expression result being displayed.
Hans Boehma0e45f32015-05-30 13:20:35 -0700367 * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
368 * @param msd Position of most significant digit. Offset from left of string.
369 Evaluator.INVALID_MSD if unknown.
370 * @param leastDigPos Position of least significant digit (1 = tenths digit)
371 * or Integer.MAX_VALUE.
372 * @param truncatedWholePart Result up to but not including decimal point.
373 Currently we only use the length.
374 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700375 @Override
376 public void onEvaluate(long index, int initPrec, int msd, int leastDigPos,
377 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700378 initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
Hans Boehm84614952014-11-25 18:46:17 -0800379 redisplay();
380 }
381
Hans Boehma0e45f32015-05-30 13:20:35 -0700382 /**
Hans Boehm5e802f32015-06-22 17:18:52 -0700383 * Set up scroll bounds (mMinPos, mMaxPos, etc.) and determine whether the result is
384 * scrollable, based on the supplied information about the result.
Hans Boehma0e45f32015-05-30 13:20:35 -0700385 * This is unfortunately complicated because we need to predict whether trailing digits
386 * will eventually be replaced by an exponent.
387 * Just appending the exponent during formatting would be simpler, but would produce
388 * jumpier results during transitions.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700389 * Only called from UI thread.
Hans Boehma0e45f32015-05-30 13:20:35 -0700390 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700391 private void initPositions(int initPrecOffset, int msdIndex, int lsdOffset,
392 String truncatedWholePart) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700393 int maxChars = getMaxChars();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700394 mWholeLen = truncatedWholePart.length();
395 // Allow a tiny amount of slop for associativity/rounding differences in length
396 // calculation. If getPreferredPrec() decided it should fit, we want to make it fit, too.
397 // We reserved one extra pixel, so the extra length is OK.
398 final int nSeparatorChars = (int) Math.ceil(
399 separatorChars(truncatedWholePart, truncatedWholePart.length())
400 - getNoEllipsisCredit() - 0.0001f);
401 mWholePartFits = mWholeLen + nSeparatorChars <= maxChars;
Hans Boehma0e45f32015-05-30 13:20:35 -0700402 mLastPos = INVALID;
Hans Boehm5e802f32015-06-22 17:18:52 -0700403 mLsdOffset = lsdOffset;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700404 mAppendExponent = false;
Hans Boehma0e45f32015-05-30 13:20:35 -0700405 // Prevent scrolling past initial position, which is calculated to show leading digits.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700406 mCurrentPos = mMinPos = (int) Math.round(initPrecOffset * mCharWidth);
Hans Boehm5e802f32015-06-22 17:18:52 -0700407 if (msdIndex == Evaluator.INVALID_MSD) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700408 // Possible zero value
Hans Boehm5e802f32015-06-22 17:18:52 -0700409 if (lsdOffset == Integer.MIN_VALUE) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700410 // Definite zero value.
411 mMaxPos = mMinPos;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700412 mMaxCharOffset = (int) Math.round(mMaxPos/mCharWidth);
Hans Boehma0e45f32015-05-30 13:20:35 -0700413 mScrollable = false;
414 } else {
415 // May be very small nonzero value. Allow user to find out.
Hans Boehm5e802f32015-06-22 17:18:52 -0700416 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700417 mMinPos -= mCharWidth; // Allow for future minus sign.
Hans Boehma0e45f32015-05-30 13:20:35 -0700418 mScrollable = true;
419 }
420 return;
421 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700422 int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
Hans Boehm65a99a42016-02-03 18:16:07 -0800423 if (msdIndex > mWholeLen && msdIndex <= mWholeLen + 3) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700424 // Avoid tiny negative exponent; pretend msdIndex is just to the right of decimal point.
Hans Boehm65a99a42016-02-03 18:16:07 -0800425 msdIndex = mWholeLen - 1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700426 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700427 // Set to position of leftmost significant digit relative to dec. point. Usually negative.
Hans Boehm65a99a42016-02-03 18:16:07 -0800428 int minCharOffset = msdIndex - mWholeLen;
Hans Boehm5e802f32015-06-22 17:18:52 -0700429 if (minCharOffset > -1 && minCharOffset < MAX_LEADING_ZEROES + 2) {
Hans Boehma0e45f32015-05-30 13:20:35 -0700430 // Small number of leading zeroes, avoid scientific notation.
Hans Boehm5e802f32015-06-22 17:18:52 -0700431 minCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700432 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700433 if (lsdOffset < MAX_RIGHT_SCROLL) {
434 mMaxCharOffset = lsdOffset;
435 if (mMaxCharOffset < -1 && mMaxCharOffset > -(MAX_TRAILING_ZEROES + 2)) {
436 mMaxCharOffset = -1;
Hans Boehma0e45f32015-05-30 13:20:35 -0700437 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700438 // lsdOffset is positive or negative, never 0.
439 int currentExpLen = 0; // Length of required standard scientific notation exponent.
440 if (mMaxCharOffset < -1) {
441 currentExpLen = expLen(-minCharOffset - 1);
442 } else if (minCharOffset > -1 || mMaxCharOffset >= maxChars) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700443 // Number is either entirely to the right of decimal point, or decimal point is
444 // not visible when scrolled to the right.
Hans Boehm5e802f32015-06-22 17:18:52 -0700445 currentExpLen = expLen(-minCharOffset);
Hans Boehma0e45f32015-05-30 13:20:35 -0700446 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700447 // Exponent length does not included added decimal point. But whenever we add a
448 // decimal point, we allow an extra character (SCI_NOTATION_EXTRA).
449 final int separatorLength = mWholePartFits && minCharOffset < -3 ? nSeparatorChars : 0;
450 mScrollable = (mMaxCharOffset + currentExpLen + separatorLength - minCharOffset
451 + negative >= maxChars);
452 // Now adjust mMaxCharOffset for any required exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700453 int newMaxCharOffset;
454 if (currentExpLen > 0) {
455 if (mScrollable) {
456 // We'll use exponent corresponding to leastDigPos when scrolled to right.
457 newMaxCharOffset = mMaxCharOffset + expLen(-lsdOffset);
458 } else {
459 newMaxCharOffset = mMaxCharOffset + currentExpLen;
460 }
461 if (mMaxCharOffset <= -1 && newMaxCharOffset > -1) {
462 // Very unlikely; just drop exponent.
463 mMaxCharOffset = -1;
464 } else {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700465 mMaxCharOffset = Math.min(newMaxCharOffset, MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700466 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700467 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
468 MAX_RIGHT_SCROLL);
469 } else if (!mWholePartFits && !mScrollable) {
470 // Corner case in which entire number fits, but not with grouping separators. We
471 // will use an exponent in un-scrolled position, which may hide digits. Scrolling
472 // by one character will remove the exponent and reveal the last digits. Note
473 // that in the forced scientific notation case, the exponent length is not
474 // factored into mMaxCharOffset, since we do not want such an increase to impact
475 // scrolling behavior. In the unscrollable case, we thus have to append the
476 // exponent at the end using the forcePrecision argument to formatResult, in order
477 // to ensure that we get the entire result.
478 mScrollable = (mMaxCharOffset + expLen(-minCharOffset - 1) - minCharOffset
479 + negative >= maxChars);
480 if (mScrollable) {
481 mMaxPos = (int) Math.ceil(mMinPos + mCharWidth);
482 // Single character scroll will remove exponent and show remaining piece.
483 } else {
484 mMaxPos = mMinPos;
485 mAppendExponent = true;
486 }
487 } else {
488 mMaxPos = Math.min((int) Math.round(mMaxCharOffset * mCharWidth),
489 MAX_RIGHT_SCROLL);
Hans Boehma0e45f32015-05-30 13:20:35 -0700490 }
Hans Boehma0e45f32015-05-30 13:20:35 -0700491 if (!mScrollable) {
492 // Position the number consistently with our assumptions to make sure it
493 // actually fits.
494 mCurrentPos = mMaxPos;
495 }
496 } else {
Hans Boehm5e802f32015-06-22 17:18:52 -0700497 mMaxPos = mMaxCharOffset = MAX_RIGHT_SCROLL;
Hans Boehma0e45f32015-05-30 13:20:35 -0700498 mScrollable = true;
499 }
500 }
501
Hans Boehm24c91ed2016-06-30 18:53:44 -0700502 /**
503 * Display error message indicated by resourceId.
504 * UI thread only.
505 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700506 @Override
507 public void onError(long index, int resourceId) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700508 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700509 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800510 mScrollable = false;
Hans Boehm14344ff2016-06-08 13:01:51 -0700511 final String msg = getContext().getString(resourceId);
Hans Boehm14344ff2016-06-08 13:01:51 -0700512 final float measuredWidth = Layout.getDesiredWidth(msg, getPaint());
Hans Boehm24c91ed2016-06-30 18:53:44 -0700513 if (measuredWidth > mWidthConstraint) {
Hans Boehm14344ff2016-06-08 13:01:51 -0700514 // Multiply by .99 to avoid rounding effects.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700515 final float scaleFactor = 0.99f * mWidthConstraint / measuredWidth;
Hans Boehm14344ff2016-06-08 13:01:51 -0700516 final RelativeSizeSpan smallTextSpan = new RelativeSizeSpan(scaleFactor);
517 final SpannableString scaledMsg = new SpannableString(msg);
518 scaledMsg.setSpan(smallTextSpan, 0, msg.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
519 setText(scaledMsg);
520 } else {
521 setText(msg);
522 }
Hans Boehm84614952014-11-25 18:46:17 -0800523 }
524
Hans Boehm013969e2015-04-13 20:29:47 -0700525 private final int MAX_COPY_SIZE = 1000000;
526
Hans Boehma0e45f32015-05-30 13:20:35 -0700527 /*
528 * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
Hans Boehm3666e632015-07-27 18:33:12 -0700529 * Unlike Evaluator.getMsdIndexOf, we treat a final 1 as significant.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700530 * Pure function; callable from anywhere.
Hans Boehma0e45f32015-05-30 13:20:35 -0700531 */
Hans Boehm3666e632015-07-27 18:33:12 -0700532 public static int getNaiveMsdIndexOf(String s) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800533 final int len = s.length();
Hans Boehma0e45f32015-05-30 13:20:35 -0700534 for (int i = 0; i < len; ++i) {
535 char c = s.charAt(i);
536 if (c != '-' && c != '.' && c != '0') {
537 return i;
538 }
539 }
540 return Evaluator.INVALID_MSD;
541 }
542
Hans Boehm24c91ed2016-06-30 18:53:44 -0700543 /**
544 * Format a result returned by Evaluator.getString() into a single line containing ellipses
545 * (if appropriate) and an exponent (if appropriate).
546 * We add two distinct kinds of exponents:
547 * (1) If the final result contains the leading digit we use standard scientific notation.
548 * (2) If not, we add an exponent corresponding to an interpretation of the final result as
549 * an integer.
550 * We add an ellipsis on the left if the result was truncated.
551 * We add ellipses and exponents in a way that leaves most digits in the position they
552 * would have been in had we not done so. This minimizes jumps as a result of scrolling.
553 * Result is NOT internationalized, uses "E" for exponent.
554 * Called only from UI thread; We sometimes omit locking for fields.
555 * @param precOffset The value that was passed to getString. Identifies the significance of
556 the rightmost digit. A value of 1 means the rightmost digits corresponds to tenths.
557 * @param maxDigs The maximum number of characters in the result
558 * @param truncated The in parameter was already truncated, beyond possibly removing the
559 minus sign.
560 * @param negative The in parameter represents a negative result. (Minus sign may be removed
561 without setting truncated.)
562 * @param lastDisplayedOffset If not null, we set lastDisplayedOffset[0] to the offset of
563 the last digit actually appearing in the display.
564 * @param forcePrecision If true, we make sure that the last displayed digit corresponds to
565 precOffset, and allow maxDigs to be exceeded in adding the exponent and commas.
566 * @param forceSciNotation Force scientific notation. May be set because we don't have
567 space for grouping separators, but whole number otherwise fits.
568 * @param insertCommas Insert commas (literally, not internationalized) as digit separators.
569 We only ever do this for the integral part of a number, and only when no
570 exponent is displayed in the initial position. The combination of which means
571 that we only do it when no exponent is displayed.
572 We insert commas in a way that does consider the width of the actual localized digit
573 separator. Commas count towards maxDigs as the appropriate fraction of a digit.
574 */
575 private String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
576 boolean negative, int lastDisplayedOffset[], boolean forcePrecision,
577 boolean forceSciNotation, boolean insertCommas) {
Hans Boehm5e802f32015-06-22 17:18:52 -0700578 final int minusSpace = negative ? 1 : 0;
Hans Boehm3666e632015-07-27 18:33:12 -0700579 final int msdIndex = truncated ? -1 : getNaiveMsdIndexOf(in); // INVALID_MSD is OK.
Hans Boehm5e802f32015-06-22 17:18:52 -0700580 String result = in;
Hans Boehm24c91ed2016-06-30 18:53:44 -0700581 boolean needEllipsis = false;
Hans Boehm73ecff22015-09-03 16:04:50 -0700582 if (truncated || (negative && result.charAt(0) != '-')) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700583 needEllipsis = true;
Hans Boehm73ecff22015-09-03 16:04:50 -0700584 result = KeyMaps.ELLIPSIS + result.substring(1, result.length());
585 // Ellipsis may be removed again in the type(1) scientific notation case.
586 }
587 final int decIndex = result.indexOf('.');
Hans Boehm65a99a42016-02-03 18:16:07 -0800588 if (lastDisplayedOffset != null) {
589 lastDisplayedOffset[0] = precOffset;
590 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700591 if (forceSciNotation || (decIndex == -1 || msdIndex != Evaluator.INVALID_MSD
Hans Boehm5e802f32015-06-22 17:18:52 -0700592 && msdIndex - decIndex > MAX_LEADING_ZEROES + 1) && precOffset != -1) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700593 // Either:
594 // 1) No decimal point displayed, and it's not just to the right of the last digit, or
595 // 2) we are at the front of a number whos integral part is too large to allow
596 // comma insertion, or
597 // 3) we should suppress leading zeroes.
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700598 // Add an exponent to let the user track which digits are currently displayed.
Hans Boehm5e802f32015-06-22 17:18:52 -0700599 // Start with type (2) exponent if we dropped no digits. -1 accounts for decimal point.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700600 // We currently never show digit separators together with an exponent.
Hans Boehm5e802f32015-06-22 17:18:52 -0700601 final int initExponent = precOffset > 0 ? -precOffset : -precOffset - 1;
602 int exponent = initExponent;
Hans Boehm08e8f322015-04-21 13:18:38 -0700603 boolean hasPoint = false;
Hans Boehm5e802f32015-06-22 17:18:52 -0700604 if (!truncated && msdIndex < maxDigs - 1
605 && result.length() - msdIndex + 1 + minusSpace
606 <= maxDigs + SCI_NOTATION_EXTRA) {
607 // Type (1) exponent computation and transformation:
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700608 // Leading digit is in display window. Use standard calculator scientific notation
609 // with one digit to the left of the decimal point. Insert decimal point and
610 // delete leading zeroes.
Hans Boehma0e45f32015-05-30 13:20:35 -0700611 // We try to keep leading digits roughly in position, and never
Hans Boehmf6dae112015-06-18 17:57:50 -0700612 // lengthen the result by more than SCI_NOTATION_EXTRA.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700613 if (decIndex > msdIndex) {
614 // In the forceSciNotation, we can have a decimal point in the relevant digit
615 // range. Remove it.
616 result = result.substring(0, decIndex)
617 + result.substring(decIndex + 1, result.length());
618 // msdIndex and precOffset unaffected.
619 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700620 final int resLen = result.length();
621 String fraction = result.substring(msdIndex + 1, resLen);
622 result = (negative ? "-" : "") + result.substring(msdIndex, msdIndex + 1)
623 + "." + fraction;
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700624 // Original exp was correct for decimal point at right of fraction.
625 // Adjust by length of fraction.
Hans Boehm5e802f32015-06-22 17:18:52 -0700626 exponent = initExponent + resLen - msdIndex - 1;
Hans Boehm08e8f322015-04-21 13:18:38 -0700627 hasPoint = true;
628 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700629 // Exponent can't be zero.
630 // Actually add the exponent of either type:
631 if (!forcePrecision) {
632 int dropDigits; // Digits to drop to make room for exponent.
633 if (hasPoint) {
634 // Type (1) exponent.
635 // Drop digits even if there is room. Otherwise the scrolling gets jumpy.
636 dropDigits = expLen(exponent);
637 if (dropDigits >= result.length() - 1) {
638 // Jumpy is better than no mantissa. Probably impossible anyway.
639 dropDigits = Math.max(result.length() - 2, 0);
Hans Boehma0e45f32015-05-30 13:20:35 -0700640 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700641 } else {
642 // Type (2) exponent.
643 // Exponent depends on the number of digits we drop, which depends on
644 // exponent ...
645 for (dropDigits = 2; expLen(initExponent + dropDigits) > dropDigits;
646 ++dropDigits) {}
647 exponent = initExponent + dropDigits;
648 if (precOffset - dropDigits > mLsdOffset) {
649 // This can happen if e.g. result = 10^40 + 10^10
650 // It turns out we would otherwise display ...10e9 because it takes
651 // the same amount of space as ...1e10 but shows one more digit.
652 // But we don't want to display a trailing zero, even if it's free.
653 ++dropDigits;
654 ++exponent;
655 }
Hans Boehm08e8f322015-04-21 13:18:38 -0700656 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700657 result = result.substring(0, result.length() - dropDigits);
Hans Boehm65a99a42016-02-03 18:16:07 -0800658 if (lastDisplayedOffset != null) {
659 lastDisplayedOffset[0] -= dropDigits;
660 }
Hans Boehm73ecff22015-09-03 16:04:50 -0700661 }
662 result = result + "E" + Integer.toString(exponent);
Hans Boehm24c91ed2016-06-30 18:53:44 -0700663 } else if (insertCommas) {
664 // Add commas to the whole number section, and then truncate on left to fit,
665 // counting commas as a fractional digit.
666 final int wholeStart = needEllipsis ? 1 : 0;
667 int orig_length = result.length();
668 final float nCommaChars;
669 if (decIndex != -1) {
670 nCommaChars = separatorChars(result, decIndex);
671 result = StringUtils.addCommas(result, wholeStart, decIndex)
672 + result.substring(decIndex, orig_length);
673 } else {
674 nCommaChars = separatorChars(result, orig_length);
675 result = StringUtils.addCommas(result, wholeStart, orig_length);
676 }
677 if (needEllipsis) {
678 orig_length -= 1; // Exclude ellipsis.
679 }
680 final float len = orig_length + nCommaChars;
681 int deletedChars = 0;
682 final float ellipsisCredit = getNoEllipsisCredit();
683 final float decimalCredit = getDecimalCredit();
684 final float effectiveLen = len - (decIndex == -1 ? 0 : getDecimalCredit());
685 final float ellipsisAdjustment =
686 needEllipsis ? mNoExponentCredit : getNoEllipsisCredit();
687 // As above, we allow for a tiny amount of extra length here, for consistency with
688 // getPreferredPrec().
689 if (effectiveLen - ellipsisAdjustment > (float) (maxDigs - wholeStart) + 0.0001f
690 && !forcePrecision) {
691 float deletedWidth = 0.0f;
692 while (effectiveLen - mNoExponentCredit - deletedWidth
693 > (float) (maxDigs - 1 /* for ellipsis */)) {
694 if (result.charAt(deletedChars) == ',') {
695 deletedWidth += mGroupingSeparatorWidthRatio;
696 } else {
697 deletedWidth += 1.0f;
698 }
699 deletedChars++;
700 }
701 }
702 if (deletedChars > 0) {
703 result = KeyMaps.ELLIPSIS + result.substring(deletedChars, result.length());
704 } else if (needEllipsis) {
705 result = KeyMaps.ELLIPSIS + result;
706 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700707 }
708 return result;
Hans Boehm08e8f322015-04-21 13:18:38 -0700709 }
710
Hans Boehmf6dae112015-06-18 17:57:50 -0700711 /**
712 * Get formatted, but not internationalized, result from mEvaluator.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700713 * @param precOffset requested position (1 = tenths) of last included digit
714 * @param maxSize maximum number of characters (more or less) in result
715 * @param lastDisplayedOffset zeroth entry is set to actual offset of last included digit,
Hans Boehm65a99a42016-02-03 18:16:07 -0800716 * after adjusting for exponent, etc. May be null.
Hans Boehmf6dae112015-06-18 17:57:50 -0700717 * @param forcePrecision Ensure that last included digit is at pos, at the expense
718 * of treating maxSize as a soft limit.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700719 * @param forceSciNotation Force scientific notation, even if not required by maxSize.
720 * @param insertCommas Insert commas as digit separators.
Hans Boehmf6dae112015-06-18 17:57:50 -0700721 */
Hans Boehm5e802f32015-06-22 17:18:52 -0700722 private String getFormattedResult(int precOffset, int maxSize, int lastDisplayedOffset[],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700723 boolean forcePrecision, boolean forceSciNotation, boolean insertCommas) {
Hans Boehm08e8f322015-04-21 13:18:38 -0700724 final boolean truncated[] = new boolean[1];
725 final boolean negative[] = new boolean[1];
Hans Boehm5e802f32015-06-22 17:18:52 -0700726 final int requestedPrecOffset[] = {precOffset};
Hans Boehm8f051c32016-10-03 16:53:58 -0700727 final String rawResult = mEvaluator.getString(mIndex, requestedPrecOffset, mMaxCharOffset,
728 maxSize, truncated, negative, this);
Hans Boehm5e802f32015-06-22 17:18:52 -0700729 return formatResult(rawResult, requestedPrecOffset[0], maxSize, truncated[0], negative[0],
Hans Boehm24c91ed2016-06-30 18:53:44 -0700730 lastDisplayedOffset, forcePrecision, forceSciNotation, insertCommas);
Hans Boehm08e8f322015-04-21 13:18:38 -0700731 }
732
Hans Boehm65a99a42016-02-03 18:16:07 -0800733 /**
734 * Return entire result (within reason) up to current displayed precision.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700735 * @param withSeparators Add digit separators
Hans Boehm65a99a42016-02-03 18:16:07 -0800736 */
Hans Boehm24c91ed2016-06-30 18:53:44 -0700737 public String getFullText(boolean withSeparators) {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700738 if (!mValid) return "";
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700739 if (!mScrollable) return getText().toString();
Hans Boehm5e802f32015-06-22 17:18:52 -0700740 return KeyMaps.translateResult(getFormattedResult(mLastDisplayedOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700741 null, true /* forcePrecision */, false /* forceSciNotation */, withSeparators));
Hans Boehm84614952014-11-25 18:46:17 -0800742 }
743
Hans Boehm24c91ed2016-06-30 18:53:44 -0700744 /**
745 * Did the above produce a correct result?
746 * UI thread only.
747 */
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700748 public boolean fullTextIsExact() {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700749 return !mScrollable || (mMaxCharOffset == getCharOffset(mCurrentPos)
750 && mMaxCharOffset != MAX_RIGHT_SCROLL);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700751 }
752
Hans Boehm61568a12015-05-18 18:25:41 -0700753 /**
Hans Boehm65a99a42016-02-03 18:16:07 -0800754 * Get entire result up to current displayed precision, or up to MAX_COPY_EXTRA additional
755 * digits, if it will lead to an exact result.
756 */
757 public String getFullCopyText() {
758 if (!mValid
759 || mLsdOffset == Integer.MAX_VALUE
760 || fullTextIsExact()
761 || mWholeLen > MAX_RECOMPUTE_DIGITS
762 || mWholeLen + mLsdOffset > MAX_RECOMPUTE_DIGITS
763 || mLsdOffset - mLastDisplayedOffset > MAX_COPY_EXTRA) {
Hans Boehm24c91ed2016-06-30 18:53:44 -0700764 return getFullText(false /* withSeparators */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800765 }
766 // It's reasonable to compute and copy the exact result instead.
767 final int nonNegLsdOffset = Math.max(0, mLsdOffset);
Hans Boehm8f051c32016-10-03 16:53:58 -0700768 final String rawResult = mEvaluator.getResult(mIndex).toStringTruncated(nonNegLsdOffset);
Hans Boehm65a99a42016-02-03 18:16:07 -0800769 final String formattedResult = formatResult(rawResult, nonNegLsdOffset, MAX_COPY_SIZE,
Hans Boehm24c91ed2016-06-30 18:53:44 -0700770 false, rawResult.charAt(0) == '-', null, true /* forcePrecision */,
771 false /* forceSciNotation */, false /* insertCommas */);
Hans Boehm65a99a42016-02-03 18:16:07 -0800772 return KeyMaps.translateResult(formattedResult);
773 }
774
775 /**
Hans Boehm61568a12015-05-18 18:25:41 -0700776 * Return the maximum number of characters that will fit in the result display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700777 * May be called asynchronously from non-UI thread. From Evaluator.CharMetricsInfo.
Hans Boehm61568a12015-05-18 18:25:41 -0700778 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700779 @Override
780 public int getMaxChars() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700781 int result;
782 synchronized(mWidthLock) {
Justin Klaassen44595162015-05-28 17:55:20 -0700783 result = (int) Math.floor(mWidthConstraint / mCharWidth);
Christine Franks7452d3a2016-10-27 13:41:18 -0700784 // We can apparently finish evaluating before onMeasure in CalculatorFormula has been
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700785 // called, in which case we get 0 or -1 as the width constraint.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700786 }
Hans Boehm84614952014-11-25 18:46:17 -0800787 if (result <= 0) {
Hans Boehmc01cd7f2015-05-12 18:32:19 -0700788 // Return something conservatively big, to force sufficient evaluation.
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700789 return MAX_WIDTH;
Hans Boehm84614952014-11-25 18:46:17 -0800790 } else {
Hans Boehm80018c82015-08-02 16:59:07 -0700791 return result;
Hans Boehm84614952014-11-25 18:46:17 -0800792 }
793 }
794
Hans Boehm61568a12015-05-18 18:25:41 -0700795 /**
Justin Klaassen44595162015-05-28 17:55:20 -0700796 * @return {@code true} if the currently displayed result is scrollable
Hans Boehm61568a12015-05-18 18:25:41 -0700797 */
Justin Klaassen44595162015-05-28 17:55:20 -0700798 public boolean isScrollable() {
799 return mScrollable;
Hans Boehm61568a12015-05-18 18:25:41 -0700800 }
801
Hans Boehm24c91ed2016-06-30 18:53:44 -0700802 /**
803 * Map pixel position to digit offset.
804 * UI thread only.
805 */
806 int getCharOffset(int pos) {
807 return (int) Math.round(pos / mCharWidth); // Lock not needed.
Hans Boehm013969e2015-04-13 20:29:47 -0700808 }
809
Hans Boehm84614952014-11-25 18:46:17 -0800810 void clear() {
Hans Boehm760a9dc2015-04-20 10:27:12 -0700811 mValid = false;
Hans Boehm1176f232015-05-11 16:26:03 -0700812 mScrollable = false;
Hans Boehm84614952014-11-25 18:46:17 -0800813 setText("");
Christine Franksafe28bb2016-07-29 17:24:52 -0700814 setLongClickable(false);
Hans Boehm84614952014-11-25 18:46:17 -0800815 }
816
Hans Boehm8f051c32016-10-03 16:53:58 -0700817 @Override
818 public void onCancelled(long index) {
819 clear();
820 }
821
Hans Boehm24c91ed2016-06-30 18:53:44 -0700822 /**
823 * Refresh display.
Hans Boehm8f051c32016-10-03 16:53:58 -0700824 * Only called in UI thread. Index argument is currently ignored.
Hans Boehm24c91ed2016-06-30 18:53:44 -0700825 */
Hans Boehm8f051c32016-10-03 16:53:58 -0700826 @Override
827 public void onReevaluate(long index) {
828 redisplay();
829 }
830
831 public void redisplay() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700832 if (mScroller.isFinished() && length() > 0) {
Christine Franksd21205c2016-08-04 10:06:15 -0700833 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
834 }
Hans Boehm24c91ed2016-06-30 18:53:44 -0700835 int currentCharOffset = getCharOffset(mCurrentPos);
Hans Boehm84614952014-11-25 18:46:17 -0800836 int maxChars = getMaxChars();
Hans Boehm5e802f32015-06-22 17:18:52 -0700837 int lastDisplayedOffset[] = new int[1];
Hans Boehm24c91ed2016-06-30 18:53:44 -0700838 String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset,
839 mAppendExponent /* forcePrecision; preserve entire result */,
840 !mWholePartFits
841 && currentCharOffset == getCharOffset(mMinPos) /* forceSciNotation */,
842 mWholePartFits /* insertCommas */ );
Hans Boehm0b9806f2015-06-29 16:07:15 -0700843 int expIndex = result.indexOf('E');
Hans Boehm013969e2015-04-13 20:29:47 -0700844 result = KeyMaps.translateResult(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700845 if (expIndex > 0 && result.indexOf('.') == -1) {
Hans Boehm84614952014-11-25 18:46:17 -0800846 // Gray out exponent if used as position indicator
847 SpannableString formattedResult = new SpannableString(result);
Hans Boehm5e802f32015-06-22 17:18:52 -0700848 formattedResult.setSpan(mExponentColorSpan, expIndex, result.length(),
Hans Boehm84614952014-11-25 18:46:17 -0800849 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
850 setText(formattedResult);
851 } else {
852 setText(result);
853 }
Hans Boehm5e802f32015-06-22 17:18:52 -0700854 mLastDisplayedOffset = lastDisplayedOffset[0];
Hans Boehm760a9dc2015-04-20 10:27:12 -0700855 mValid = true;
Christine Franksafe28bb2016-07-29 17:24:52 -0700856 setLongClickable(true);
Hans Boehm84614952014-11-25 18:46:17 -0800857 }
858
859 @Override
Christine Franks6f6c24a2016-09-08 18:21:47 -0700860 protected void onTextChanged(java.lang.CharSequence text, int start, int lengthBefore,
861 int lengthAfter) {
862 super.onTextChanged(text, start, lengthBefore, lengthAfter);
863
864 if (!mScrollable || mScroller.isFinished()) {
865 if (lengthBefore == 0 && lengthAfter > 0) {
866 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
867 setContentDescription(null);
868 } else if (lengthBefore > 0 && lengthAfter == 0) {
869 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
870 setContentDescription(getContext().getString(R.string.desc_result));
871 }
872 }
873 }
874
875 @Override
Hans Boehm84614952014-11-25 18:46:17 -0800876 public void computeScroll() {
Christine Franks6f6c24a2016-09-08 18:21:47 -0700877 if (!mScrollable) {
878 return;
879 }
880
Hans Boehm84614952014-11-25 18:46:17 -0800881 if (mScroller.computeScrollOffset()) {
882 mCurrentPos = mScroller.getCurrX();
Hans Boehm24c91ed2016-06-30 18:53:44 -0700883 if (getCharOffset(mCurrentPos) != getCharOffset(mLastPos)) {
Hans Boehm84614952014-11-25 18:46:17 -0800884 mLastPos = mCurrentPos;
885 redisplay();
886 }
Christine Franks6f6c24a2016-09-08 18:21:47 -0700887 }
888
889 if (!mScroller.isFinished()) {
Justin Klaassen44595162015-05-28 17:55:20 -0700890 postInvalidateOnAnimation();
Christine Franksd21205c2016-08-04 10:06:15 -0700891 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE);
Christine Franks6f6c24a2016-09-08 18:21:47 -0700892 } else if (length() > 0){
Christine Franksd21205c2016-08-04 10:06:15 -0700893 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
Hans Boehm84614952014-11-25 18:46:17 -0800894 }
895 }
896
Chenjie Yu3937b652016-06-01 23:14:26 -0700897 /**
898 * Use ActionMode for copy support on M and higher.
899 */
900 @TargetApi(Build.VERSION_CODES.M)
901 private void setupActionMode() {
902 mCopyActionModeCallback = new ActionMode.Callback2() {
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700903
Chenjie Yu3937b652016-06-01 23:14:26 -0700904 @Override
905 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
906 final MenuInflater inflater = mode.getMenuInflater();
907 return createCopyMenu(inflater, menu);
908 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700909
Chenjie Yu3937b652016-06-01 23:14:26 -0700910 @Override
911 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
912 return false; // Return false if nothing is done
913 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700914
Chenjie Yu3937b652016-06-01 23:14:26 -0700915 @Override
916 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
917 if (onMenuItemClick(item)) {
Hans Boehm65a99a42016-02-03 18:16:07 -0800918 mode.finish();
919 return true;
Chenjie Yu3937b652016-06-01 23:14:26 -0700920 } else {
921 return false;
Hans Boehm65a99a42016-02-03 18:16:07 -0800922 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700923 }
924
925 @Override
926 public void onDestroyActionMode(ActionMode mode) {
927 unhighlightResult();
928 mActionMode = null;
929 }
930
931 @Override
932 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
933 super.onGetContentRect(mode, view, outRect);
934
935 outRect.left += view.getPaddingLeft();
936 outRect.top += view.getPaddingTop();
937 outRect.right -= view.getPaddingRight();
938 outRect.bottom -= view.getPaddingBottom();
939 final int width = (int) Layout.getDesiredWidth(getText(), getPaint());
940 if (width < outRect.width()) {
941 outRect.left = outRect.right - width;
942 }
943
944 if (!BuildCompat.isAtLeastN()) {
945 // The CAB (prior to N) only takes the translation of a view into account, so
946 // if a scale is applied to the view then the offset outRect will end up being
947 // positioned incorrectly. We workaround that limitation by manually applying
948 // the scale to the outRect, which the CAB will then offset to the correct
949 // position.
950 final float scaleX = view.getScaleX();
951 final float scaleY = view.getScaleY();
952 outRect.left *= scaleX;
953 outRect.right *= scaleX;
954 outRect.top *= scaleY;
955 outRect.bottom *= scaleY;
956 }
957 }
958 };
959 setOnLongClickListener(new View.OnLongClickListener() {
960 @Override
961 public boolean onLongClick(View v) {
962 if (mValid) {
963 mActionMode = startActionMode(mCopyActionModeCallback,
964 ActionMode.TYPE_FLOATING);
965 return true;
966 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700967 return false;
968 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700969 });
970 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700971
Chenjie Yu3937b652016-06-01 23:14:26 -0700972 /**
973 * Use ContextMenu for copy support on L and lower.
974 */
975 private void setupContextMenu() {
976 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
977 @Override
978 public void onCreateContextMenu(ContextMenu contextMenu, View view,
979 ContextMenu.ContextMenuInfo contextMenuInfo) {
980 final MenuInflater inflater = new MenuInflater(getContext());
981 createCopyMenu(inflater, contextMenu);
982 mContextMenu = contextMenu;
983 for(int i = 0; i < contextMenu.size(); i ++) {
984 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorResult.this);
985 }
Hans Boehm7f83e362015-06-10 15:41:04 -0700986 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700987 });
988 setOnLongClickListener(new View.OnLongClickListener() {
989 @Override
990 public boolean onLongClick(View v) {
991 if (mValid) {
992 return showContextMenu();
993 }
994 return false;
Justin Klaassenf1b61f42016-04-27 16:00:11 -0700995 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700996 });
997 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700998
Chenjie Yu3937b652016-06-01 23:14:26 -0700999 private boolean createCopyMenu(MenuInflater inflater, Menu menu) {
1000 inflater.inflate(R.menu.copy, menu);
1001 highlightResult();
1002 return true;
1003 }
1004
1005 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -07001006 if (mActionMode != null) {
1007 mActionMode.finish();
1008 return true;
1009 }
Chenjie Yu3937b652016-06-01 23:14:26 -07001010 if (mContextMenu != null) {
1011 unhighlightResult();
1012 mContextMenu.close();
1013 return true;
1014 }
Hans Boehm1176f232015-05-11 16:26:03 -07001015 return false;
1016 }
1017
Chenjie Yu3937b652016-06-01 23:14:26 -07001018 private void highlightResult() {
1019 final Spannable text = (Spannable) getText();
1020 text.setSpan(mHighlightSpan, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1021 }
1022
1023 private void unhighlightResult() {
1024 final Spannable text = (Spannable) getText();
1025 text.removeSpan(mHighlightSpan);
1026 }
1027
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001028 private void setPrimaryClip(ClipData clip) {
1029 ClipboardManager clipboard = (ClipboardManager) getContext().
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001030 getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001031 clipboard.setPrimaryClip(clip);
1032 }
1033
1034 private void copyContent() {
Hans Boehm65a99a42016-02-03 18:16:07 -08001035 final CharSequence text = getFullCopyText();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001036 ClipboardManager clipboard =
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001037 (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
1038 // We include a tag URI, to allow us to recognize our own results and handle them
1039 // specially.
Hans Boehm8f051c32016-10-03 16:53:58 -07001040 ClipData.Item newItem = new ClipData.Item(text, null, mEvaluator.capture(mIndex));
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001041 String[] mimeTypes = new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN};
1042 ClipData cd = new ClipData("calculator result", mimeTypes, newItem);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001043 clipboard.setPrimaryClip(cd);
Hans Boehmc01cd7f2015-05-12 18:32:19 -07001044 Toast.makeText(getContext(), R.string.text_copied_toast, Toast.LENGTH_SHORT).show();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -07001045 }
1046
Chenjie Yu3937b652016-06-01 23:14:26 -07001047 @Override
1048 public boolean onMenuItemClick(MenuItem item) {
1049 switch (item.getItemId()) {
1050 case R.id.menu_copy:
Hans Boehm8f051c32016-10-03 16:53:58 -07001051 if (mEvaluator.evaluationInProgress(mIndex)) {
Chenjie Yu3937b652016-06-01 23:14:26 -07001052 // Refuse to copy placeholder characters.
1053 return false;
1054 } else {
1055 copyContent();
1056 unhighlightResult();
1057 return true;
1058 }
1059 default:
1060 return false;
1061 }
1062 }
Hans Boehm84614952014-11-25 18:46:17 -08001063}