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.annotation.TargetApi; 20 import android.content.ClipData; 21 import android.content.ClipboardManager; 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Rect; 25 import android.os.Build; 26 import android.text.Layout; 27 import android.text.TextPaint; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.util.TypedValue; 32 import android.view.ActionMode; 33 import android.view.ContextMenu; 34 import android.view.Menu; 35 import android.view.MenuInflater; 36 import android.view.MenuItem; 37 import android.view.View; 38 import android.widget.TextView; 39 40 /** 41 * TextView adapted for displaying the formula and allowing pasting. 42 */ 43 public class CalculatorFormula extends AlignedTextView implements MenuItem.OnMenuItemClickListener, 44 ClipboardManager.OnPrimaryClipChangedListener { 45 46 public static final String TAG_ACTION_MODE = "ACTION_MODE"; 47 48 // Temporary paint for use in layout methods. 49 private final TextPaint mTempPaint = new TextPaint(); 50 51 private final float mMaximumTextSize; 52 private final float mMinimumTextSize; 53 private final float mStepTextSize; 54 55 private final ClipboardManager mClipboardManager; 56 57 private int mWidthConstraint = -1; 58 private ActionMode mActionMode; 59 private ActionMode.Callback mPasteActionModeCallback; 60 private ContextMenu mContextMenu; 61 private OnTextSizeChangeListener mOnTextSizeChangeListener; 62 private OnFormulaContextMenuClickListener mOnContextMenuClickListener; 63 private Calculator.OnDisplayMemoryOperationsListener mOnDisplayMemoryOperationsListener; 64 CalculatorFormula(Context context)65 public CalculatorFormula(Context context) { 66 this(context, null /* attrs */); 67 } 68 CalculatorFormula(Context context, AttributeSet attrs)69 public CalculatorFormula(Context context, AttributeSet attrs) { 70 this(context, attrs, 0 /* defStyleAttr */); 71 } 72 CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr)73 public CalculatorFormula(Context context, AttributeSet attrs, int defStyleAttr) { 74 super(context, attrs, defStyleAttr); 75 76 mClipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 77 78 final TypedArray a = context.obtainStyledAttributes( 79 attrs, R.styleable.CalculatorFormula, defStyleAttr, 0); 80 mMaximumTextSize = a.getDimension( 81 R.styleable.CalculatorFormula_maxTextSize, getTextSize()); 82 mMinimumTextSize = a.getDimension( 83 R.styleable.CalculatorFormula_minTextSize, getTextSize()); 84 mStepTextSize = a.getDimension(R.styleable.CalculatorFormula_stepTextSize, 85 (mMaximumTextSize - mMinimumTextSize) / 3); 86 a.recycle(); 87 88 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 89 setupActionMode(); 90 } else { 91 setupContextMenu(); 92 } 93 } 94 95 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)96 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 97 if (!isLaidOut()) { 98 // Prevent shrinking/resizing with our variable textSize. 99 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize, 100 false /* notifyListener */); 101 setMinimumHeight(getLineHeight() + getCompoundPaddingBottom() 102 + getCompoundPaddingTop()); 103 } 104 105 // Ensure we are at least as big as our parent. 106 final int width = MeasureSpec.getSize(widthMeasureSpec); 107 if (getMinimumWidth() != width) { 108 setMinimumWidth(width); 109 } 110 111 // Re-calculate our textSize based on new width. 112 mWidthConstraint = MeasureSpec.getSize(widthMeasureSpec) 113 - getPaddingLeft() - getPaddingRight(); 114 final float textSize = getVariableTextSize(getText()); 115 if (getTextSize() != textSize) { 116 setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, textSize, false /* notifyListener */); 117 } 118 119 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 120 } 121 122 @Override onAttachedToWindow()123 protected void onAttachedToWindow() { 124 super.onAttachedToWindow(); 125 126 mClipboardManager.addPrimaryClipChangedListener(this); 127 onPrimaryClipChanged(); 128 } 129 130 @Override onDetachedFromWindow()131 protected void onDetachedFromWindow() { 132 super.onDetachedFromWindow(); 133 134 mClipboardManager.removePrimaryClipChangedListener(this); 135 } 136 137 @Override onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)138 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { 139 super.onTextChanged(text, start, lengthBefore, lengthAfter); 140 141 setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString())); 142 } 143 setTextSizeInternal(int unit, float size, boolean notifyListener)144 private void setTextSizeInternal(int unit, float size, boolean notifyListener) { 145 final float oldTextSize = getTextSize(); 146 super.setTextSize(unit, size); 147 if (notifyListener && mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) { 148 mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize); 149 } 150 } 151 152 @Override setTextSize(int unit, float size)153 public void setTextSize(int unit, float size) { 154 setTextSizeInternal(unit, size, true); 155 } 156 getMinimumTextSize()157 public float getMinimumTextSize() { 158 return mMinimumTextSize; 159 } 160 getMaximumTextSize()161 public float getMaximumTextSize() { 162 return mMaximumTextSize; 163 } 164 getVariableTextSize(CharSequence text)165 public float getVariableTextSize(CharSequence text) { 166 if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) { 167 // Not measured, bail early. 168 return getTextSize(); 169 } 170 171 // Capture current paint state. 172 mTempPaint.set(getPaint()); 173 174 // Step through increasing text sizes until the text would no longer fit. 175 float lastFitTextSize = mMinimumTextSize; 176 while (lastFitTextSize < mMaximumTextSize) { 177 mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize)); 178 if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) { 179 break; 180 } 181 lastFitTextSize = mTempPaint.getTextSize(); 182 } 183 184 return lastFitTextSize; 185 } 186 187 /** 188 * Functionally equivalent to setText(), but explicitly announce changes. 189 * If the new text is an extension of the old one, announce the addition. 190 * Otherwise, e.g. after deletion, announce the entire new text. 191 */ changeTextTo(CharSequence newText)192 public void changeTextTo(CharSequence newText) { 193 final CharSequence oldText = getText(); 194 final char separator = KeyMaps.translateResult(",").charAt(0); 195 final CharSequence added = StringUtils.getExtensionIgnoring(newText, oldText, separator); 196 if (added != null) { 197 if (added.length() == 1) { 198 // The algorithm for pronouncing a single character doesn't seem 199 // to respect our hints. Don't give it the choice. 200 final char c = added.charAt(0); 201 final int id = KeyMaps.keyForChar(c); 202 final String descr = KeyMaps.toDescriptiveString(getContext(), id); 203 if (descr != null) { 204 announceForAccessibility(descr); 205 } else { 206 announceForAccessibility(String.valueOf(c)); 207 } 208 } else if (added.length() != 0) { 209 announceForAccessibility(added); 210 } 211 } else { 212 announceForAccessibility(newText); 213 } 214 setText(newText, BufferType.SPANNABLE); 215 } 216 stopActionModeOrContextMenu()217 public boolean stopActionModeOrContextMenu() { 218 if (mActionMode != null) { 219 mActionMode.finish(); 220 return true; 221 } 222 if (mContextMenu != null) { 223 mContextMenu.close(); 224 return true; 225 } 226 return false; 227 } 228 setOnTextSizeChangeListener(OnTextSizeChangeListener listener)229 public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) { 230 mOnTextSizeChangeListener = listener; 231 } 232 setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener)233 public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) { 234 mOnContextMenuClickListener = listener; 235 } 236 setOnDisplayMemoryOperationsListener( Calculator.OnDisplayMemoryOperationsListener listener)237 public void setOnDisplayMemoryOperationsListener( 238 Calculator.OnDisplayMemoryOperationsListener listener) { 239 mOnDisplayMemoryOperationsListener = listener; 240 } 241 242 /** 243 * Use ActionMode for paste support on M and higher. 244 */ 245 @TargetApi(Build.VERSION_CODES.M) setupActionMode()246 private void setupActionMode() { 247 mPasteActionModeCallback = new ActionMode.Callback2() { 248 249 @Override 250 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 251 if (onMenuItemClick(item)) { 252 mode.finish(); 253 return true; 254 } else { 255 return false; 256 } 257 } 258 259 @Override 260 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 261 mode.setTag(TAG_ACTION_MODE); 262 final MenuInflater inflater = mode.getMenuInflater(); 263 return createContextMenu(inflater, menu); 264 } 265 266 @Override 267 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 268 return false; 269 } 270 271 @Override 272 public void onDestroyActionMode(ActionMode mode) { 273 mActionMode = null; 274 } 275 276 @Override 277 public void onGetContentRect(ActionMode mode, View view, Rect outRect) { 278 super.onGetContentRect(mode, view, outRect); 279 outRect.top += getTotalPaddingTop(); 280 outRect.right -= getTotalPaddingRight(); 281 outRect.bottom -= getTotalPaddingBottom(); 282 // Encourage menu positioning over the rightmost 10% of the screen. 283 outRect.left = (int) (outRect.right * 0.9f); 284 } 285 }; 286 setOnLongClickListener(new View.OnLongClickListener() { 287 @Override 288 public boolean onLongClick(View v) { 289 mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING); 290 return true; 291 } 292 }); 293 } 294 295 /** 296 * Use ContextMenu for paste support on L and lower. 297 */ setupContextMenu()298 private void setupContextMenu() { 299 setOnCreateContextMenuListener(new OnCreateContextMenuListener() { 300 @Override 301 public void onCreateContextMenu(ContextMenu contextMenu, View view, 302 ContextMenu.ContextMenuInfo contextMenuInfo) { 303 final MenuInflater inflater = new MenuInflater(getContext()); 304 createContextMenu(inflater, contextMenu); 305 mContextMenu = contextMenu; 306 for (int i = 0; i < contextMenu.size(); i++) { 307 contextMenu.getItem(i).setOnMenuItemClickListener(CalculatorFormula.this); 308 } 309 } 310 }); 311 setOnLongClickListener(new View.OnLongClickListener() { 312 @Override 313 public boolean onLongClick(View v) { 314 return showContextMenu(); 315 } 316 }); 317 } 318 createContextMenu(MenuInflater inflater, Menu menu)319 private boolean createContextMenu(MenuInflater inflater, Menu menu) { 320 final boolean isPasteEnabled = isPasteEnabled(); 321 final boolean isMemoryEnabled = isMemoryEnabled(); 322 if (!isPasteEnabled && !isMemoryEnabled) { 323 return false; 324 } 325 326 bringPointIntoView(length()); 327 inflater.inflate(R.menu.menu_formula, menu); 328 final MenuItem pasteItem = menu.findItem(R.id.menu_paste); 329 final MenuItem memoryRecallItem = menu.findItem(R.id.memory_recall); 330 pasteItem.setEnabled(isPasteEnabled); 331 memoryRecallItem.setEnabled(isMemoryEnabled); 332 return true; 333 } 334 paste()335 private void paste() { 336 final ClipData primaryClip = mClipboardManager.getPrimaryClip(); 337 if (primaryClip != null && mOnContextMenuClickListener != null) { 338 mOnContextMenuClickListener.onPaste(primaryClip); 339 } 340 } 341 342 @Override onMenuItemClick(MenuItem item)343 public boolean onMenuItemClick(MenuItem item) { 344 switch (item.getItemId()) { 345 case R.id.memory_recall: 346 mOnContextMenuClickListener.onMemoryRecall(); 347 return true; 348 case R.id.menu_paste: 349 paste(); 350 return true; 351 default: 352 return false; 353 } 354 } 355 356 @Override onPrimaryClipChanged()357 public void onPrimaryClipChanged() { 358 setLongClickable(isPasteEnabled() || isMemoryEnabled()); 359 } 360 onMemoryStateChanged()361 public void onMemoryStateChanged() { 362 setLongClickable(isPasteEnabled() || isMemoryEnabled()); 363 } 364 isMemoryEnabled()365 private boolean isMemoryEnabled() { 366 return mOnDisplayMemoryOperationsListener != null 367 && mOnDisplayMemoryOperationsListener.shouldDisplayMemory(); 368 } 369 isPasteEnabled()370 private boolean isPasteEnabled() { 371 final ClipData clip = mClipboardManager.getPrimaryClip(); 372 if (clip == null || clip.getItemCount() == 0) { 373 return false; 374 } 375 CharSequence clipText = null; 376 try { 377 clipText = clip.getItemAt(0).coerceToText(getContext()); 378 } catch (Exception e) { 379 Log.i("Calculator", "Error reading clipboard:", e); 380 } 381 return !TextUtils.isEmpty(clipText); 382 } 383 384 public interface OnTextSizeChangeListener { onTextSizeChanged(TextView textView, float oldSize)385 void onTextSizeChanged(TextView textView, float oldSize); 386 } 387 388 public interface OnFormulaContextMenuClickListener { onPaste(ClipData clip)389 boolean onPaste(ClipData clip); onMemoryRecall()390 void onMemoryRecall(); 391 } 392 } 393