Hans Boehm | 8461495 | 2014-11-25 18:46:17 -0800 | [diff] [blame^] | 1 | /* |
| 2 | * Copyright (C) 2014 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.calculator2; |
| 18 | |
| 19 | import android.widget.TextView; |
| 20 | import android.graphics.Typeface; |
| 21 | import android.graphics.Paint; |
| 22 | import android.graphics.Rect; |
| 23 | import android.graphics.Color; |
| 24 | import android.widget.OverScroller; |
| 25 | import android.view.GestureDetector; |
| 26 | import android.content.Context; |
| 27 | import android.util.AttributeSet; |
| 28 | import android.view.MotionEvent; |
| 29 | import android.view.View; |
| 30 | import android.text.Editable; |
| 31 | import android.text.Spanned; |
| 32 | import android.text.SpannableString; |
| 33 | import android.text.style.ForegroundColorSpan; |
| 34 | |
| 35 | import android.support.v4.view.ViewCompat; |
| 36 | |
| 37 | |
| 38 | // A text widget that is "infinitely" scrollable to the right, |
| 39 | // and obtains the text to display via a callback to Logic. |
| 40 | public class CalculatorResult extends CalculatorEditText { |
| 41 | final static int MAX_RIGHT_SCROLL = 100000000; |
| 42 | final static int INVALID = MAX_RIGHT_SCROLL + 10000; |
| 43 | // A larger value is unlikely to avoid running out of space |
| 44 | final OverScroller mScroller; |
| 45 | final GestureDetector mGestureDetector; |
| 46 | class MyTouchListener implements View.OnTouchListener { |
| 47 | @Override |
| 48 | public boolean onTouch(View v, MotionEvent event) { |
| 49 | boolean res = mGestureDetector.onTouchEvent(event); |
| 50 | return res; |
| 51 | } |
| 52 | } |
| 53 | final MyTouchListener mTouchListener = new MyTouchListener(); |
| 54 | private Evaluator mEvaluator; |
| 55 | private boolean mScrollable = false; |
| 56 | // A scrollable result is currently displayed. |
| 57 | private int mCurrentPos;// Position of right of display relative |
| 58 | // to decimal point, in pixels. |
| 59 | // Large positive values mean the decimal |
| 60 | // point is scrolled off the left of the |
| 61 | // display. Zero means decimal point is |
| 62 | // barely displayed on the right. |
| 63 | private int mLastPos; // Position already reflected in display. |
| 64 | private int mMinPos; // Maximum position before all digits |
| 65 | // digits disappear of the right. |
| 66 | private int mCharWidth; // Use monospaced font for now. |
| 67 | // This shouldn't be much harder with a variable |
| 68 | // width font, except it may be even less smooth |
| 69 | // FIXME: This is not really a fixed width font anymore. |
| 70 | private Paint mPaint; // Paint object matching display. |
| 71 | |
| 72 | public CalculatorResult(Context context, AttributeSet attrs) { |
| 73 | super(context, attrs); |
| 74 | mScroller = new OverScroller(context); |
| 75 | mGestureDetector = new GestureDetector(context, |
| 76 | new GestureDetector.SimpleOnGestureListener() { |
| 77 | @Override |
| 78 | public boolean onFling(MotionEvent e1, MotionEvent e2, |
| 79 | float velocityX, float velocityY) { |
| 80 | if (!mScroller.isFinished()) { |
| 81 | mCurrentPos = mScroller.getFinalX(); |
| 82 | } |
| 83 | mScroller.forceFinished(true); |
| 84 | CalculatorResult.this.cancelLongPress(); // Ignore scrolls of error string, etc. |
| 85 | if (!mScrollable) return true; |
| 86 | mScroller.fling(mCurrentPos, 0, - (int) velocityX, |
| 87 | 0 /* horizontal only */, mMinPos, |
| 88 | MAX_RIGHT_SCROLL, 0, 0); |
| 89 | ViewCompat.postInvalidateOnAnimation(CalculatorResult.this); |
| 90 | return true; |
| 91 | } |
| 92 | @Override |
| 93 | public boolean onScroll(MotionEvent e1, MotionEvent e2, |
| 94 | float distanceX, float distanceY) { |
| 95 | // TODO: Should we be dealing with any edge effects here? |
| 96 | if (!mScroller.isFinished()) { |
| 97 | mCurrentPos = mScroller.getFinalX(); |
| 98 | } |
| 99 | mScroller.forceFinished(true); |
| 100 | CalculatorResult.this.cancelLongPress(); |
| 101 | if (!mScrollable) return true; |
| 102 | int duration = (int)(e2.getEventTime() - e1.getEventTime()); |
| 103 | if (duration < 1 || duration > 100) duration = 10; |
| 104 | mScroller.startScroll(mCurrentPos, 0, (int)distanceX, 0, |
| 105 | (int)duration); |
| 106 | ViewCompat.postInvalidateOnAnimation(CalculatorResult.this); |
| 107 | return true; |
| 108 | } |
| 109 | }); |
| 110 | setOnTouchListener(mTouchListener); |
| 111 | setHorizontallyScrolling(false); // do it ourselves |
| 112 | setCursorVisible(false); |
| 113 | setTypeface(Typeface.MONOSPACE); |
| 114 | mPaint = getPaint(); |
| 115 | mCharWidth = (int) mPaint.measureText("5"); |
| 116 | } |
| 117 | |
| 118 | void setEvaluator(Evaluator evaluator) { |
| 119 | mEvaluator = evaluator; |
| 120 | } |
| 121 | |
| 122 | // Display a new result, given initial displayed |
| 123 | // precision and the string representing the whole part of |
| 124 | // the number to be displayed. |
| 125 | // We pass the string, instead of just the length, so we have |
| 126 | // one less place to fix in case we ever decide to use a variable |
| 127 | // width font. |
| 128 | void displayResult(int initPrec, String truncatedWholePart) { |
| 129 | mLastPos = INVALID; |
| 130 | mCurrentPos = initPrec * mCharWidth; |
| 131 | mMinPos = - (int) Math.ceil(mPaint.measureText(truncatedWholePart)); |
| 132 | redisplay(); |
| 133 | } |
| 134 | |
| 135 | // May be called from non-UI thread, but after initialization. |
| 136 | int getCharWidth() { |
| 137 | return mCharWidth; |
| 138 | } |
| 139 | |
| 140 | void displayError(int resourceId) { |
| 141 | mScrollable = false; |
| 142 | setText(resourceId); |
| 143 | } |
| 144 | |
| 145 | // Return entire result (within reason) up to current displayed precision. |
| 146 | public CharSequence getFullText() { |
| 147 | if (!mScrollable) return getText(); |
| 148 | int currentCharPos = mCurrentPos/mCharWidth; |
| 149 | return mEvaluator.getString(currentCharPos, 1000000); |
| 150 | } |
| 151 | |
| 152 | int getMaxChars() { |
| 153 | int result = getWidthConstraint() / mCharWidth; |
| 154 | // FIXME: We can apparently finish evaluating before |
| 155 | // onMeasure in CalculatorEditText has been called, in |
| 156 | // which case we get 0 or -1 as the width constraint. |
| 157 | // Perhaps guess conservatively here and reevaluate |
| 158 | // in InitialResult.onPostExecute? |
| 159 | if (result <= 0) { |
| 160 | return 8; |
| 161 | } else { |
| 162 | return result; |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | void clear() { |
| 167 | setText(""); |
| 168 | } |
| 169 | |
| 170 | void redisplay() { |
| 171 | int currentCharPos = mCurrentPos/mCharWidth; |
| 172 | int maxChars = getMaxChars(); |
| 173 | String result = mEvaluator.getString(currentCharPos, maxChars); |
| 174 | int epos = result.indexOf('e'); |
| 175 | // TODO: Internationalization for decimal point? |
| 176 | if (epos > 0 && result.indexOf('.') == -1) { |
| 177 | // Gray out exponent if used as position indicator |
| 178 | SpannableString formattedResult = new SpannableString(result); |
| 179 | formattedResult.setSpan(new ForegroundColorSpan(Color.GRAY), |
| 180 | epos, result.length(), |
| 181 | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); |
| 182 | setText(formattedResult); |
| 183 | } else { |
| 184 | setText(result); |
| 185 | } |
| 186 | mScrollable = true; |
| 187 | } |
| 188 | |
| 189 | @Override |
| 190 | public void computeScroll() { |
| 191 | if (!mScrollable) return; |
| 192 | if (mScroller.computeScrollOffset()) { |
| 193 | mCurrentPos = mScroller.getCurrX(); |
| 194 | if (mCurrentPos != mLastPos) { |
| 195 | mLastPos = mCurrentPos; |
| 196 | redisplay(); |
| 197 | } |
| 198 | if (!mScroller.isFinished()) { |
| 199 | ViewCompat.postInvalidateOnAnimation(this); |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | } |