blob: 52006ea73d9f09e822c5628a3403ad5c9301b0ef [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
Chenjie Yu3937b652016-06-01 23:14:26 -070019import android.annotation.TargetApi;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070020import android.content.ClipData;
Justin Klaassen44595162015-05-28 17:55:20 -070021import android.content.ClipboardManager;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080022import android.content.Context;
Justin Klaassen4b3af052014-05-27 17:53:10 -070023import android.content.res.TypedArray;
Hans Boehm7f83e362015-06-10 15:41:04 -070024import android.graphics.Rect;
Chenjie Yu3937b652016-06-01 23:14:26 -070025import android.os.Build;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070026import android.text.Layout;
Justin Klaassen4b3af052014-05-27 17:53:10 -070027import android.text.TextPaint;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080028import android.util.AttributeSet;
Hongwei Wang245925e2014-05-11 14:38:47 -070029import android.util.TypedValue;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080030import android.view.ActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070031import android.view.ContextMenu;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080032import android.view.Menu;
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070033import android.view.MenuInflater;
Gilles Debunnef57b8b42011-01-27 10:54:07 -080034import android.view.MenuItem;
Hans Boehm76b78152015-04-17 10:50:35 -070035import android.view.View;
Justin Klaassenfed941a2014-06-09 18:42:40 +010036import android.widget.TextView;
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -080037
Hans Boehm84614952014-11-25 18:46:17 -080038/**
Hans Boehm08e8f322015-04-21 13:18:38 -070039 * TextView adapted for Calculator display.
Hans Boehm84614952014-11-25 18:46:17 -080040 */
Chenjie Yu3937b652016-06-01 23:14:26 -070041public class CalculatorText extends AlignedTextView implements MenuItem.OnMenuItemClickListener {
Alan Viverette461992d2014-03-07 13:29:56 -080042
Annie Chine918fd22016-03-09 11:07:54 -080043 public static final String TAG_ACTION_MODE = "ACTION_MODE";
44
Justin Klaassenfc5ac822015-06-18 13:15:17 -070045 // Temporary paint for use in layout methods.
46 private final TextPaint mTempPaint = new TextPaint();
Hans Boehm4a6b7cb2015-04-03 18:41:52 -070047
Justin Klaassen4b3af052014-05-27 17:53:10 -070048 private final float mMaximumTextSize;
49 private final float mMinimumTextSize;
50 private final float mStepTextSize;
51
52 private int mWidthConstraint = -1;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070053 private ActionMode mActionMode;
Chenjie Yu3937b652016-06-01 23:14:26 -070054 private ActionMode.Callback mPasteActionModeCallback;
55 private ContextMenu mContextMenu;
Justin Klaassenfc5ac822015-06-18 13:15:17 -070056 private OnPasteListener mOnPasteListener;
Justin Klaassenfed941a2014-06-09 18:42:40 +010057 private OnTextSizeChangeListener mOnTextSizeChangeListener;
Justin Klaassen4b3af052014-05-27 17:53:10 -070058
Hans Boehm08e8f322015-04-21 13:18:38 -070059 public CalculatorText(Context context) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070060 this(context, null /* attrs */);
Justin Klaassen4b3af052014-05-27 17:53:10 -070061 }
62
Hans Boehm08e8f322015-04-21 13:18:38 -070063 public CalculatorText(Context context, AttributeSet attrs) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -070064 this(context, attrs, 0 /* defStyleAttr */);
Justin Klaassen4b3af052014-05-27 17:53:10 -070065 }
66
Justin Klaassenfc5ac822015-06-18 13:15:17 -070067 public CalculatorText(Context context, AttributeSet attrs, int defStyleAttr) {
68 super(context, attrs, defStyleAttr);
Justin Klaassen4b3af052014-05-27 17:53:10 -070069
70 final TypedArray a = context.obtainStyledAttributes(
Justin Klaassenfc5ac822015-06-18 13:15:17 -070071 attrs, R.styleable.CalculatorText, defStyleAttr, 0);
Justin Klaassen4b3af052014-05-27 17:53:10 -070072 mMaximumTextSize = a.getDimension(
Hans Boehm08e8f322015-04-21 13:18:38 -070073 R.styleable.CalculatorText_maxTextSize, getTextSize());
Justin Klaassen4b3af052014-05-27 17:53:10 -070074 mMinimumTextSize = a.getDimension(
Hans Boehm08e8f322015-04-21 13:18:38 -070075 R.styleable.CalculatorText_minTextSize, getTextSize());
76 mStepTextSize = a.getDimension(R.styleable.CalculatorText_stepTextSize,
Justin Klaassen4b3af052014-05-27 17:53:10 -070077 (mMaximumTextSize - mMinimumTextSize) / 3);
Justin Klaassen4b3af052014-05-27 17:53:10 -070078 a.recycle();
79
Chenjie Yu3937b652016-06-01 23:14:26 -070080 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
81 setupActionMode();
82 } else {
83 setupContextMenu();
84 }
Hans Boehm76b78152015-04-17 10:50:35 -070085 }
Justin Klaassen4b3af052014-05-27 17:53:10 -070086
87 @Override
88 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Justin Klaassen0ace4eb2016-02-05 11:38:12 -080089 if (!isLaidOut()) {
90 // Prevent shrinking/resizing with our variable textSize.
91 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
92 false /* notifyListener */);
Justin Klaassenf3076c82016-06-03 13:18:55 -070093 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom()
Justin Klaassen0ace4eb2016-02-05 11:38:12 -080094 + getCompoundPaddingTop());
95 }
96
Justin Klaassenf3076c82016-06-03 13:18:55 -070097 // Ensure we are at least as big as our parent.
98 final int width = MeasureSpec.getSize(widthMeasureSpec);
99 if (getMinimumWidth() != width) {
100 setMinimumWidth(width);
101 }
102
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700103 // Re-calculate our textSize based on new width.
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800104 mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Justin Klaassen44595162015-05-28 17:55:20 -0700105 - getPaddingLeft() - getPaddingRight();
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800106 final float textSize = getVariableTextSize(getText());
107 if (getTextSize() != textSize) {
108 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700109 }
110
111 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700112 }
113
114 @Override
115 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
116 super.onTextChanged(text, start, lengthBefore, lengthAfter);
Justin Klaassenbfc4e4d2014-08-27 10:56:49 -0700117
Justin Klaassen4b3af052014-05-27 17:53:10 -0700118 setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
119 }
120
Hans Boehm11e37a82015-10-01 14:41:37 -0700121 private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100122 final float oldTextSize = getTextSize();
123 super.setTextSize(unit, size);
Hans Boehm11e37a82015-10-01 14:41:37 -0700124 if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100125 mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
126 }
127 }
128
Hans Boehm11e37a82015-10-01 14:41:37 -0700129 @Override
130 public void setTextSize(int unit, float size) {
131 setTextSizeInternal(unit, size, true);
132 }
133
Justin Klaassen44595162015-05-28 17:55:20 -0700134 public float getMinimumTextSize() {
135 return mMinimumTextSize;
136 }
137
138 public float getMaximumTextSize() {
139 return mMaximumTextSize;
140 }
141
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700142 public float getVariableTextSize(CharSequence text) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700143 if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
144 // Not measured, bail early.
145 return getTextSize();
146 }
147
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700148 // Capture current paint state.
149 mTempPaint.set(getPaint());
150
151 // Step through increasing text sizes until the text would no longer fit.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700152 float lastFitTextSize = mMinimumTextSize;
153 while (lastFitTextSize < mMaximumTextSize) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700154 mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
155 if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700156 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700157 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700158 lastFitTextSize = mTempPaint.getTextSize();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700159 }
160
161 return lastFitTextSize;
162 }
163
Hans Boehmccc55662015-07-07 14:16:59 -0700164 private static boolean startsWith(CharSequence whole, CharSequence prefix) {
165 int wholeLen = whole.length();
166 int prefixLen = prefix.length();
167 if (prefixLen > wholeLen) {
168 return false;
169 }
170 for (int i = 0; i < prefixLen; ++i) {
171 if (prefix.charAt(i) != whole.charAt(i)) {
172 return false;
173 }
174 }
175 return true;
176 }
177
178 /**
179 * Functionally equivalent to setText(), but explicitly announce changes.
180 * If the new text is an extension of the old one, announce the addition.
181 * Otherwise, e.g. after deletion, announce the entire new text.
182 */
183 public void changeTextTo(CharSequence newText) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700184 final CharSequence oldText = getText();
Hans Boehmccc55662015-07-07 14:16:59 -0700185 if (startsWith(newText, oldText)) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700186 final int newLen = newText.length();
187 final int oldLen = oldText.length();
188 if (newLen == oldLen + 1) {
189 // The algorithm for pronouncing a single character doesn't seem
190 // to respect our hints. Don't give it the choice.
191 final char c = newText.charAt(oldLen);
192 final int id = KeyMaps.keyForChar(c);
193 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
194 if (descr != null) {
195 announceForAccessibility(descr);
196 } else {
197 announceForAccessibility(String.valueOf(c));
198 }
199 } else if (newLen > oldLen) {
Hans Boehmccc55662015-07-07 14:16:59 -0700200 announceForAccessibility(newText.subSequence(oldLen, newLen));
201 }
202 } else {
203 announceForAccessibility(newText);
204 }
205 setText(newText);
206 }
207
Chenjie Yu3937b652016-06-01 23:14:26 -0700208 public boolean stopActionModeOrContextMenu() {
Hans Boehm1176f232015-05-11 16:26:03 -0700209 if (mActionMode != null) {
210 mActionMode.finish();
211 return true;
212 }
Chenjie Yu3937b652016-06-01 23:14:26 -0700213 if (mContextMenu != null) {
214 mContextMenu.close();
215 return true;
216 }
Hans Boehm1176f232015-05-11 16:26:03 -0700217 return false;
218 }
219
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700220 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
221 mOnTextSizeChangeListener = listener;
222 }
223
224 public void setOnPasteListener(OnPasteListener listener) {
225 mOnPasteListener = listener;
226 }
227
Chenjie Yu3937b652016-06-01 23:14:26 -0700228 /**
229 * Use ActionMode for paste support on M and higher.
230 */
231 @TargetApi(Build.VERSION_CODES.M)
232 private void setupActionMode() {
233 mPasteActionModeCallback = new ActionMode.Callback2() {
234
235 @Override
236 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
237 if (onMenuItemClick(item)) {
238 mode.finish();
239 return true;
240 } else {
241 return false;
242 }
243 }
244
245 @Override
246 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
247 mode.setTag(TAG_ACTION_MODE);
248 final MenuInflater inflater = mode.getMenuInflater();
249 return createPasteMenu(inflater, menu);
250 }
251
252 @Override
253 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
254 return false;
255 }
256
257 @Override
258 public void onDestroyActionMode(ActionMode mode) {
259 mActionMode = null;
260 }
261
262 @Override
263 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
264 super.onGetContentRect(mode, view, outRect);
265 outRect.top += getTotalPaddingTop();
266 outRect.right -= getTotalPaddingRight();
267 outRect.bottom -= getTotalPaddingBottom();
268 // Encourage menu positioning towards the right, possibly over formula.
269 outRect.left = outRect.right;
270 }
271 };
272 setOnLongClickListener(new View.OnLongClickListener() {
273 @Override
274 public boolean onLongClick(View v) {
275 mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
276 return true;
277 }
278 });
279 }
280
281 /**
282 * Use ContextMenu for paste support on L and lower.
283 */
284 private void setupContextMenu() {
285 setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
286 @Override
287 public void onCreateContextMenu(ContextMenu contextMenu, View view,
288 ContextMenu.ContextMenuInfo contextMenuInfo) {
289 final MenuInflater inflater = new MenuInflater(getContext());
290 createPasteMenu(inflater, contextMenu);
291 mContextMenu = contextMenu;
292 for(int i = 0; i < contextMenu.size(); i++) {
293 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorText.this);
294 }
295 }
296 });
297 setOnLongClickListener(new View.OnLongClickListener() {
298 @Override
299 public boolean onLongClick(View v) {
300 return showContextMenu();
301 }
302 });
303 }
304
305 private boolean createPasteMenu(MenuInflater inflater, Menu menu) {
306 final ClipboardManager clipboard = (ClipboardManager) getContext()
307 .getSystemService(Context.CLIPBOARD_SERVICE);
308 if (clipboard.hasPrimaryClip()) {
309 bringPointIntoView(length());
310 inflater.inflate(R.menu.paste, menu);
311 return true;
312 }
313 // Prevents the selection action mode on double tap.
314 return false;
315 }
316
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700317 private void paste() {
318 final ClipboardManager clipboard = (ClipboardManager) getContext()
319 .getSystemService(Context.CLIPBOARD_SERVICE);
320 final ClipData primaryClip = clipboard.getPrimaryClip();
321 if (primaryClip != null && mOnPasteListener != null) {
322 mOnPasteListener.onPaste(primaryClip);
323 }
324 }
325
Chenjie Yu3937b652016-06-01 23:14:26 -0700326 @Override
327 public boolean onMenuItemClick(MenuItem item) {
328 if (item.getItemId() == R.id.menu_paste) {
329 paste();
330 return true;
331 }
332 return false;
333 }
334
Justin Klaassenfed941a2014-06-09 18:42:40 +0100335 public interface OnTextSizeChangeListener {
336 void onTextSizeChanged(TextView textView, float oldSize);
337 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700338
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700339 public interface OnPasteListener {
340 boolean onPaste(ClipData clip);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700341 }
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -0800342}