1 /*
2  * Copyright (C) 2015 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.content.ClipData;
20 import android.content.ClipboardManager;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Rect;
24 import android.text.Layout;
25 import android.text.TextPaint;
26 import android.text.method.ScrollingMovementMethod;
27 import android.util.AttributeSet;
28 import android.util.TypedValue;
29 import android.view.ActionMode;
30 import android.view.Menu;
31 import android.view.MenuInflater;
32 import android.view.MenuItem;
33 import android.view.View;
34 import android.widget.TextView;
35 
36 /**
37  * TextView adapted for Calculator display.
38  */
39 public class CalculatorText extends AlignedTextView implements View.OnLongClickListener {
40 
41     private final ActionMode.Callback2 mPasteActionModeCallback = new ActionMode.Callback2() {
42 
43         @Override
44         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
45             if (item.getItemId() == R.id.menu_paste) {
46                 paste();
47                 mode.finish();
48                 return true;
49             }
50             return false;
51         }
52 
53         @Override
54         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
55             final ClipboardManager clipboard = (ClipboardManager) getContext()
56                     .getSystemService(Context.CLIPBOARD_SERVICE);
57             if (clipboard.hasPrimaryClip()) {
58                 bringPointIntoView(length());
59                 MenuInflater inflater = mode.getMenuInflater();
60                 inflater.inflate(R.menu.paste, menu);
61                 return true;
62             }
63             // Prevents the selection action mode on double tap.
64             return false;
65         }
66 
67         @Override
68         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
69             return false;
70         }
71 
72         @Override
73         public void onDestroyActionMode(ActionMode mode) {
74             mActionMode = null;
75         }
76 
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         }
86     };
87 
88     // Temporary paint for use in layout methods.
89     private final TextPaint mTempPaint = new TextPaint();
90 
91     private final float mMaximumTextSize;
92     private final float mMinimumTextSize;
93     private final float mStepTextSize;
94 
95     private int mWidthConstraint = -1;
96 
97     private ActionMode mActionMode;
98 
99     private OnPasteListener mOnPasteListener;
100     private OnTextSizeChangeListener mOnTextSizeChangeListener;
101 
CalculatorText(Context context)102     public CalculatorText(Context context) {
103         this(context, null /* attrs */);
104     }
105 
CalculatorText(Context context, AttributeSet attrs)106     public CalculatorText(Context context, AttributeSet attrs) {
107         this(context, attrs, 0 /* defStyleAttr */);
108     }
109 
CalculatorText(Context context, AttributeSet attrs, int defStyleAttr)110     public CalculatorText(Context context, AttributeSet attrs, int defStyleAttr) {
111         super(context, attrs, defStyleAttr);
112 
113         final TypedArray a = context.obtainStyledAttributes(
114                 attrs, R.styleable.CalculatorText, defStyleAttr, 0);
115         mMaximumTextSize = a.getDimension(
116                 R.styleable.CalculatorText_maxTextSize, getTextSize());
117         mMinimumTextSize = a.getDimension(
118                 R.styleable.CalculatorText_minTextSize, getTextSize());
119         mStepTextSize = a.getDimension(R.styleable.CalculatorText_stepTextSize,
120                 (mMaximumTextSize - mMinimumTextSize) / 3);
121         a.recycle();
122 
123         // Allow scrolling by default.
124         setMovementMethod(ScrollingMovementMethod.getInstance());
125 
126         // 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);
131     }
132 
133     @Override
onLongClick(View v)134     public boolean onLongClick(View v) {
135         mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
136         return true;
137     }
138 
139     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)140     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
141         // Re-calculate our textSize based on new width.
142         final int width = MeasureSpec.getSize(widthMeasureSpec)
143                 - getPaddingLeft() - getPaddingRight();
144         if (mWidthConstraint != width) {
145             mWidthConstraint = width;
146 
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);
157         }
158 
159         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
160     }
161 
getWidthConstraint()162     public int getWidthConstraint() { return mWidthConstraint; }
163 
164     @Override
onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)165     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
166         super.onTextChanged(text, start, lengthBefore, lengthAfter);
167 
168         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
169     }
170 
setTextSizeInternal(int unit, float size, boolean notifyListener)171     private void setTextSizeInternal(int unit, float size, boolean notifyListener) {
172         final float oldTextSize = getTextSize();
173         super.setTextSize(unit, size);
174         if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
175             mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
176         }
177     }
178 
179     @Override
setTextSize(int unit, float size)180     public void setTextSize(int unit, float size) {
181         setTextSizeInternal(unit, size, true);
182     }
183 
getMinimumTextSize()184     public float getMinimumTextSize() {
185         return mMinimumTextSize;
186     }
187 
getMaximumTextSize()188     public float getMaximumTextSize() {
189         return mMaximumTextSize;
190     }
191 
getVariableTextSize(CharSequence text)192     public float getVariableTextSize(CharSequence text) {
193         if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
194             // Not measured, bail early.
195             return getTextSize();
196         }
197 
198         // Capture current paint state.
199         mTempPaint.set(getPaint());
200 
201         // Step through increasing text sizes until the text would no longer fit.
202         float lastFitTextSize = mMinimumTextSize;
203         while (lastFitTextSize < mMaximumTextSize) {
204             mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
205             if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
206                 break;
207             }
208             lastFitTextSize = mTempPaint.getTextSize();
209         }
210 
211         return lastFitTextSize;
212     }
213 
startsWith(CharSequence whole, CharSequence prefix)214     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      */
changeTextTo(CharSequence newText)233     public void changeTextTo(CharSequence newText) {
234         final CharSequence oldText = getText();
235         if (startsWith(newText, oldText)) {
236             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) {
250                 announceForAccessibility(newText.subSequence(oldLen, newLen));
251             }
252         } else {
253             announceForAccessibility(newText);
254         }
255         setText(newText);
256     }
257 
stopActionMode()258     public boolean stopActionMode() {
259         if (mActionMode != null) {
260             mActionMode.finish();
261             return true;
262         }
263         return false;
264     }
265 
setOnTextSizeChangeListener(OnTextSizeChangeListener listener)266     public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
267         mOnTextSizeChangeListener = listener;
268     }
269 
setOnPasteListener(OnPasteListener listener)270     public void setOnPasteListener(OnPasteListener listener) {
271         mOnPasteListener = listener;
272     }
273 
paste()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 
283     public interface OnTextSizeChangeListener {
onTextSizeChanged(TextView textView, float oldSize)284         void onTextSizeChanged(TextView textView, float oldSize);
285     }
286 
287     public interface OnPasteListener {
onPaste(ClipData clip)288         boolean onPaste(ClipData clip);
289     }
290 }
291