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