Home | History | Annotate | Download | only in calculator2
      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 
     65     public CalculatorFormula(Context context) {
     66         this(context, null /* attrs */);
     67     }
     68 
     69     public CalculatorFormula(Context context, AttributeSet attrs) {
     70         this(context, attrs, 0 /* defStyleAttr */);
     71     }
     72 
     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
     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
    123     protected void onAttachedToWindow() {
    124         super.onAttachedToWindow();
    125 
    126         mClipboardManager.addPrimaryClipChangedListener(this);
    127         onPrimaryClipChanged();
    128     }
    129 
    130     @Override
    131     protected void onDetachedFromWindow() {
    132         super.onDetachedFromWindow();
    133 
    134         mClipboardManager.removePrimaryClipChangedListener(this);
    135     }
    136 
    137     @Override
    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 
    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
    153     public void setTextSize(int unit, float size) {
    154         setTextSizeInternal(unit, size, true);
    155     }
    156 
    157     public float getMinimumTextSize() {
    158         return mMinimumTextSize;
    159     }
    160 
    161     public float getMaximumTextSize() {
    162         return mMaximumTextSize;
    163     }
    164 
    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      */
    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 
    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 
    229     public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
    230         mOnTextSizeChangeListener = listener;
    231     }
    232 
    233     public void setOnContextMenuClickListener(OnFormulaContextMenuClickListener listener) {
    234         mOnContextMenuClickListener = listener;
    235     }
    236 
    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)
    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      */
    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 
    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 
    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
    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
    357     public void onPrimaryClipChanged() {
    358         setLongClickable(isPasteEnabled() || isMemoryEnabled());
    359     }
    360 
    361     public void onMemoryStateChanged() {
    362         setLongClickable(isPasteEnabled() || isMemoryEnabled());
    363     }
    364 
    365     private boolean isMemoryEnabled() {
    366         return mOnDisplayMemoryOperationsListener != null
    367                 && mOnDisplayMemoryOperationsListener.shouldDisplayMemory();
    368     }
    369 
    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 {
    385         void onTextSizeChanged(TextView textView, float oldSize);
    386     }
    387 
    388     public interface OnFormulaContextMenuClickListener {
    389         boolean onPaste(ClipData clip);
    390         void onMemoryRecall();
    391     }
    392 }
    393