blob: ec45123effca90f920333cc1622f387596036552 [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 Klaassen0ace4eb2016-02-05 11:38:12 -0800141 if (!isLaidOut()) {
142 // Prevent shrinking/resizing with our variable textSize.
143 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize,
144 false /* notifyListener */);
145 setMinHeight(getLineHeight() + getCompoundPaddingBottom()
146 + getCompoundPaddingTop());
147 }
148
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700149 // Re-calculate our textSize based on new width.
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800150 mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec)
Justin Klaassen44595162015-05-28 17:55:20 -0700151 - getPaddingLeft() - getPaddingRight();
Justin Klaassen0ace4eb2016-02-05 11:38:12 -0800152 final float textSize = getVariableTextSize(getText());
153 if (getTextSize() != textSize) {
154 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */);
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700155 }
156
157 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Justin Klaassen4b3af052014-05-27 17:53:10 -0700158 }
159
Hans Boehm84614952014-11-25 18:46:17 -0800160 public int getWidthConstraint() { return mWidthConstraint; }
161
Justin Klaassen4b3af052014-05-27 17:53:10 -0700162 @Override
163 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
164 super.onTextChanged(text, start, lengthBefore, lengthAfter);
Justin Klaassenbfc4e4d2014-08-27 10:56:49 -0700165
Justin Klaassen4b3af052014-05-27 17:53:10 -0700166 setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
167 }
168
Hans Boehm11e37a82015-10-01 14:41:37 -0700169 private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100170 final float oldTextSize = getTextSize();
171 super.setTextSize(unit, size);
Hans Boehm11e37a82015-10-01 14:41:37 -0700172 if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
Justin Klaassenfed941a2014-06-09 18:42:40 +0100173 mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
174 }
175 }
176
Hans Boehm11e37a82015-10-01 14:41:37 -0700177 @Override
178 public void setTextSize(int unit, float size) {
179 setTextSizeInternal(unit, size, true);
180 }
181
Justin Klaassen44595162015-05-28 17:55:20 -0700182 public float getMinimumTextSize() {
183 return mMinimumTextSize;
184 }
185
186 public float getMaximumTextSize() {
187 return mMaximumTextSize;
188 }
189
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700190 public float getVariableTextSize(CharSequence text) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700191 if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
192 // Not measured, bail early.
193 return getTextSize();
194 }
195
Justin Klaassen2be4fdb2014-08-06 19:54:09 -0700196 // Capture current paint state.
197 mTempPaint.set(getPaint());
198
199 // Step through increasing text sizes until the text would no longer fit.
Justin Klaassen4b3af052014-05-27 17:53:10 -0700200 float lastFitTextSize = mMinimumTextSize;
201 while (lastFitTextSize < mMaximumTextSize) {
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700202 mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
203 if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
Justin Klaassen4b3af052014-05-27 17:53:10 -0700204 break;
Justin Klaassen4b3af052014-05-27 17:53:10 -0700205 }
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700206 lastFitTextSize = mTempPaint.getTextSize();
Justin Klaassen4b3af052014-05-27 17:53:10 -0700207 }
208
209 return lastFitTextSize;
210 }
211
Hans Boehmccc55662015-07-07 14:16:59 -0700212 private static boolean startsWith(CharSequence whole, CharSequence prefix) {
213 int wholeLen = whole.length();
214 int prefixLen = prefix.length();
215 if (prefixLen > wholeLen) {
216 return false;
217 }
218 for (int i = 0; i < prefixLen; ++i) {
219 if (prefix.charAt(i) != whole.charAt(i)) {
220 return false;
221 }
222 }
223 return true;
224 }
225
226 /**
227 * Functionally equivalent to setText(), but explicitly announce changes.
228 * If the new text is an extension of the old one, announce the addition.
229 * Otherwise, e.g. after deletion, announce the entire new text.
230 */
231 public void changeTextTo(CharSequence newText) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700232 final CharSequence oldText = getText();
Hans Boehmccc55662015-07-07 14:16:59 -0700233 if (startsWith(newText, oldText)) {
Hans Boehm8a4f81c2015-07-09 10:41:25 -0700234 final int newLen = newText.length();
235 final int oldLen = oldText.length();
236 if (newLen == oldLen + 1) {
237 // The algorithm for pronouncing a single character doesn't seem
238 // to respect our hints. Don't give it the choice.
239 final char c = newText.charAt(oldLen);
240 final int id = KeyMaps.keyForChar(c);
241 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
242 if (descr != null) {
243 announceForAccessibility(descr);
244 } else {
245 announceForAccessibility(String.valueOf(c));
246 }
247 } else if (newLen > oldLen) {
Hans Boehmccc55662015-07-07 14:16:59 -0700248 announceForAccessibility(newText.subSequence(oldLen, newLen));
249 }
250 } else {
251 announceForAccessibility(newText);
252 }
253 setText(newText);
254 }
255
Hans Boehm1176f232015-05-11 16:26:03 -0700256 public boolean stopActionMode() {
257 if (mActionMode != null) {
258 mActionMode.finish();
259 return true;
260 }
261 return false;
262 }
263
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700264 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
265 mOnTextSizeChangeListener = listener;
266 }
267
268 public void setOnPasteListener(OnPasteListener listener) {
269 mOnPasteListener = listener;
270 }
271
272 private void paste() {
273 final ClipboardManager clipboard = (ClipboardManager) getContext()
274 .getSystemService(Context.CLIPBOARD_SERVICE);
275 final ClipData primaryClip = clipboard.getPrimaryClip();
276 if (primaryClip != null && mOnPasteListener != null) {
277 mOnPasteListener.onPaste(primaryClip);
278 }
279 }
280
Justin Klaassenfed941a2014-06-09 18:42:40 +0100281 public interface OnTextSizeChangeListener {
282 void onTextSizeChanged(TextView textView, float oldSize);
283 }
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700284
Justin Klaassenfc5ac822015-06-18 13:15:17 -0700285 public interface OnPasteListener {
286 boolean onPaste(ClipData clip);
Hans Boehm4a6b7cb2015-04-03 18:41:52 -0700287 }
Dmitri Plotnikovde3eec22011-01-17 18:23:37 -0800288}