blob: f9440715424a36e6ae66a67e14d0909bf3da310f [file] [log] [blame]
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -08001/*
Justin Klaassen44595162015-05-28 17:55:20 -07002 * Copyright (C) 2015 The Android Open Source Project
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -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 *
Justin Klaassen4b3af052014-05-27 17:53:10 -07008 * http://www.apache.org/licenses/LICENSE-2.0
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -08009 *
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
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070019import android.content.ClipData;
Justin Klaassen44595162015-05-28 17:55:20 -070020import android.content.ClipboardManager;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080021import android.content.Context;
Justin Klaassen4b3af052014-05-27 17:53:10 -070022import android.content.res.TypedArray;
Hans Boehm7f83e362015-06-10 15:41:04 -070023import android.graphics.Rect;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070024import android.text.Layout;
Justin Klaassen4b3af052014-05-27 17:53:10 -070025import android.text.TextPaint;
Justin Klaassen44595162015-05-28 17:55:20 -070026import android.text.method.ScrollingMovementMethod;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080027import android.util.AttributeSet;
Hongwei Wang245925e2014-05-11 14:38:47 -070028import android.util.TypedValue;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080029import android.view.ActionMode;
30import android.view.Menu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070031import android.view.MenuInflater;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080032import android.view.MenuItem;
Hans Boehm76b78152015-04-17 10:50:35 -070033import android.view.View;
Justin Klaassenfed941a2014-06-09 18:42:40 +010034import android.widget.TextView;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080035
Hans Boehm84614952014-11-25 18:46:17 -080036/**
Hans Boehm08e8f322015-04-21 13:18:38 -070037 * TextView adapted for Calculator display.
Hans Boehm84614952014-11-25 18:46:17 -080038 */
Justin Klaassen44595162015-05-28 17:55:20 -070039public class CalculatorText extends AlignedTextView implements View.OnLongClickListener {
Alan Viverette461992d2014-03-07 13:29:56 -080040
Hans Boehm7f83e362015-06-10 15:41:04 -070041 private final ActionMode.Callback2 mPasteActionModeCallback = new ActionMode.Callback2() {
42
Gilles Debunnef57b8b42011-01-27 10:54:07 -080043 @Override
44 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070045 if (item.getItemId() == R.id.menu_paste) {
46 paste();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070047 mode.finish();
48 return true;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070049 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -070050 return false;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080051 }
52
53 @Override
54 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070055 final ClipboardManager clipboard = (ClipboardManager) getContext()
56 .getSystemService(Context.CLIPBOARD_SERVICE);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070057 if (clipboard.hasPrimaryClip()) {
Hans Boehm7f83e362015-06-10 15:41:04 -070058 bringPointIntoView(length());
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070059 MenuInflater inflater = mode.getMenuInflater();
60 inflater.inflate(R.menu.paste, menu);
61 return true;
62 }
Gilles Debunnef57b8b42011-01-27 10:54:07 -080063 // Prevents the selection action mode on double tap.
64 return false;
65 }
66
67 @Override
Gilles Debunnef57b8b42011-01-27 10:54:07 -080068 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
69 return false;
70 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -070071
72 @Override
73 public void onDestroyActionMode(ActionMode mode) {
74 mActionMode = null;
75 }
Hans Boehm7f83e362015-06-10 15:41:04 -070076
77 @Override
78 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
79 super.onGetContentRect(mode, view, outRect);
80 outRect.top += getTotalPaddingTop();
81 outRect.right -= getTotalPaddingRight();
82 outRect.bottom -= getTotalPaddingBottom();
83 // Encourage menu positioning towards the right, possibly over formula.
84 outRect.left = outRect.right;
85 }
Justin Klaassen4b3af052014-05-27 17:53:10 -070086 };
87
Justin Klaassenfc5ac822015-06-18 13:15:17 -070088 // Temporary paint for use in layout methods.
89 private final TextPaint mTempPaint = new TextPaint();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070090
Justin Klaassen4b3af052014-05-27 17:53:10 -070091 private final float mMaximumTextSize;
92 private final float mMinimumTextSize;
93 private final float mStepTextSize;
94
95 private int mWidthConstraint = -1;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070096
97 private ActionMode mActionMode;
98
99 private OnPasteListener mOnPasteListener;
Justin Klaassenfed941a2014-06-09 18:42:40 +0100100 private OnTextSizeChangeListener mOnTextSizeChangeListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700101
Hans Boehm08e8f322015-04-21 13:18:38 -0700102 public CalculatorText(Context context) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700103 this(context, null /* attrs */);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700104 }
105
Hans Boehm08e8f322015-04-21 13:18:38 -0700106 public CalculatorText(Context context, AttributeSet attrs) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700107 this(context, attrs, 0 /* defStyleAttr */);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700108 }
109
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700110 public CalculatorText(Context context, AttributeSet attrs, int defStyleAttr) {
111 super(context, attrs, defStyleAttr);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700112
113 final TypedArray a = context.obtainStyledAttributes(
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700114 attrs, R.styleable.CalculatorText, defStyleAttr, 0);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700115 mMaximumTextSize = a.getDimension(
Hans Boehm08e8f322015-04-21 13:18:38 -0700116 R.styleable.CalculatorText_maxTextSize, getTextSize());
Justin Klaassen4b3af052014-05-27 17:53:10 -0700117 mMinimumTextSize = a.getDimension(
Hans Boehm08e8f322015-04-21 13:18:38 -0700118 R.styleable.CalculatorText_minTextSize, getTextSize());
119 mStepTextSize = a.getDimension(R.styleable.CalculatorText_stepTextSize,
Justin Klaassen4b3af052014-05-27 17:53:10 -0700120 (mMaximumTextSize - mMinimumTextSize) / 3);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700121 a.recycle();
122
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700123 // Allow scrolling by default.
Hans Boehm08e8f322015-04-21 13:18:38 -0700124 setMovementMethod(ScrollingMovementMethod.getInstance());
125
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700126 // Reset the clickable flag, which is added when specifying a movement method.
127 setClickable(false);
128
129 // Add a long click to start the ActionMode manually.
130 setOnLongClickListener(this);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700131 }
132
133 @Override
Hans Boehm76b78152015-04-17 10:50:35 -0700134 public boolean onLongClick(View v) {
Hans Boehm1176f232015-05-11 16:26:03 -0700135 mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
Hans Boehm76b78152015-04-17 10:50:35 -0700136 return true;
137 }
Justin Klaassen4b3af052014-05-27 17:53:10 -0700138
139 @Override
140 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700141 // Re-calculate our textSize based on new width.
142 final int width = MeasureSpec.getSize(widthMeasureSpec)
Justin Klaassen44595162015-05-28 17:55:20 -0700143 - getPaddingLeft() - getPaddingRight();
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700144 if (mWidthConstraint != width) {
145 mWidthConstraint = width;
Hans Boehm11e37a82015-10-01 14:41:37 -0700146
147 if (!isLaidOut()) {
148 // Prevent shrinking/resizing with our variable textSize.
149 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
150 false /* notifyListener */);
151 setMinHeight(getLineHeight() + getCompoundPaddingBottom()
152 + getCompoundPaddingTop());
153 }
154
155 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(getText()),
156 false);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700157 }
158
159 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700160 }
161
Hans Boehm84614952014-11-25 18:46:17 -0800162 public int getWidthConstraint() { return mWidthConstraint; }
163
Justin Klaassen4b3af052014-05-27 17:53:10 -0700164 @Override
165 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
166 super.onTextChanged(text, start, lengthBefore, lengthAfter);
Justin Klaassenbfc4e4d2014-08-27 10:56:49 -0700167
Justin Klaassen4b3af052014-05-27 17:53:10 -0700168 setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
169 }
170
Hans Boehm11e37a82015-10-01 14:41:37 -0700171 private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100172 final float oldTextSize = getTextSize();
173 super.setTextSize(unit, size);
Hans Boehm11e37a82015-10-01 14:41:37 -0700174 if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100175 mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
176 }
177 }
178
Hans Boehm11e37a82015-10-01 14:41:37 -0700179 @Override
180 public void setTextSize(int unit, float size) {
181 setTextSizeInternal(unit, size, true);
182 }
183
Justin Klaassen44595162015-05-28 17:55:20 -0700184 public float getMinimumTextSize() {
185 return mMinimumTextSize;
186 }
187
188 public float getMaximumTextSize() {
189 return mMaximumTextSize;
190 }
191
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700192 public float getVariableTextSize(CharSequence text) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700193 if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
194 // Not measured, bail early.
195 return getTextSize();
196 }
197
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700198 // Capture current paint state.
199 mTempPaint.set(getPaint());
200
201 // Step through increasing text sizes until the text would no longer fit.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700202 float lastFitTextSize = mMinimumTextSize;
203 while (lastFitTextSize < mMaximumTextSize) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700204 mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
205 if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700206 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700207 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700208 lastFitTextSize = mTempPaint.getTextSize();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700209 }
210
211 return lastFitTextSize;
212 }
213
Hans Boehmccc55662015-07-07 14:16:59 -0700214 private static boolean startsWith(CharSequence whole, CharSequence prefix) {
215 int wholeLen = whole.length();
216 int prefixLen = prefix.length();
217 if (prefixLen > wholeLen) {
218 return false;
219 }
220 for (int i = 0; i < prefixLen; ++i) {
221 if (prefix.charAt(i) != whole.charAt(i)) {
222 return false;
223 }
224 }
225 return true;
226 }
227
228 /**
229 * Functionally equivalent to setText(), but explicitly announce changes.
230 * If the new text is an extension of the old one, announce the addition.
231 * Otherwise, e.g. after deletion, announce the entire new text.
232 */
233 public void changeTextTo(CharSequence newText) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700234 final CharSequence oldText = getText();
Hans Boehmccc55662015-07-07 14:16:59 -0700235 if (startsWith(newText, oldText)) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700236 final int newLen = newText.length();
237 final int oldLen = oldText.length();
238 if (newLen == oldLen + 1) {
239 // The algorithm for pronouncing a single character doesn't seem
240 // to respect our hints. Don't give it the choice.
241 final char c = newText.charAt(oldLen);
242 final int id = KeyMaps.keyForChar(c);
243 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
244 if (descr != null) {
245 announceForAccessibility(descr);
246 } else {
247 announceForAccessibility(String.valueOf(c));
248 }
249 } else if (newLen > oldLen) {
Hans Boehmccc55662015-07-07 14:16:59 -0700250 announceForAccessibility(newText.subSequence(oldLen, newLen));
251 }
252 } else {
253 announceForAccessibility(newText);
254 }
255 setText(newText);
256 }
257
Hans Boehm1176f232015-05-11 16:26:03 -0700258 public boolean stopActionMode() {
259 if (mActionMode != null) {
260 mActionMode.finish();
261 return true;
262 }
263 return false;
264 }
265
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700266 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
267 mOnTextSizeChangeListener = listener;
268 }
269
270 public void setOnPasteListener(OnPasteListener listener) {
271 mOnPasteListener = listener;
272 }
273
274 private void paste() {
275 final ClipboardManager clipboard = (ClipboardManager) getContext()
276 .getSystemService(Context.CLIPBOARD_SERVICE);
277 final ClipData primaryClip = clipboard.getPrimaryClip();
278 if (primaryClip != null && mOnPasteListener != null) {
279 mOnPasteListener.onPaste(primaryClip);
280 }
281 }
282
Justin Klaassenfed941a2014-06-09 18:42:40 +0100283 public interface OnTextSizeChangeListener {
284 void onTextSizeChanged(TextView textView, float oldSize);
285 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700286
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700287 public interface OnPasteListener {
288 boolean onPaste(ClipData clip);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700289 }
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -0800290}