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.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 
    102     public CalculatorText(Context context) {
    103         this(context, null /* attrs */);
    104     }
    105 
    106     public CalculatorText(Context context, AttributeSet attrs) {
    107         this(context, attrs, 0 /* defStyleAttr */);
    108     }
    109 
    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
    134     public boolean onLongClick(View v) {
    135         mActionMode = startActionMode(mPasteActionModeCallback, ActionMode.TYPE_FLOATING);
    136         return true;
    137     }
    138 
    139     @Override
    140     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    141         // Prevent shrinking/resizing with our variable textSize.
    142         if (!isLaidOut()) {
    143             setTextSize(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize);
    144             setMinHeight(getLineHeight() + getCompoundPaddingBottom() + getCompoundPaddingTop());
    145         }
    146 
    147         // Re-calculate our textSize based on new width.
    148         final int width = MeasureSpec.getSize(widthMeasureSpec)
    149                 - getPaddingLeft() - getPaddingRight();
    150         if (mWidthConstraint != width) {
    151             mWidthConstraint = width;
    152             setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(getText()));
    153         }
    154 
    155         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    156     }
    157 
    158     public int getWidthConstraint() { return mWidthConstraint; }
    159 
    160     @Override
    161     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
    162         super.onTextChanged(text, start, lengthBefore, lengthAfter);
    163 
    164         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
    165     }
    166 
    167     @Override
    168     public void setTextSize(int unit, float size) {
    169         final float oldTextSize = getTextSize();
    170         super.setTextSize(unit, size);
    171 
    172         if (mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
    173             mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
    174         }
    175     }
    176 
    177     public float getMinimumTextSize() {
    178         return mMinimumTextSize;
    179     }
    180 
    181     public float getMaximumTextSize() {
    182         return mMaximumTextSize;
    183     }
    184 
    185     public float getVariableTextSize(CharSequence text) {
    186         if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
    187             // Not measured, bail early.
    188             return getTextSize();
    189         }
    190 
    191         // Capture current paint state.
    192         mTempPaint.set(getPaint());
    193 
    194         // Step through increasing text sizes until the text would no longer fit.
    195         float lastFitTextSize = mMinimumTextSize;
    196         while (lastFitTextSize < mMaximumTextSize) {
    197             mTempPaint.setTextSize(Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize));
    198             if (Layout.getDesiredWidth(text, mTempPaint) > mWidthConstraint) {
    199                 break;
    200             }
    201             lastFitTextSize = mTempPaint.getTextSize();
    202         }
    203 
    204         return lastFitTextSize;
    205     }
    206 
    207     private static boolean startsWith(CharSequence whole, CharSequence prefix) {
    208         int wholeLen = whole.length();
    209         int prefixLen = prefix.length();
    210         if (prefixLen > wholeLen) {
    211             return false;
    212         }
    213         for (int i = 0; i < prefixLen; ++i) {
    214             if (prefix.charAt(i) != whole.charAt(i)) {
    215                 return false;
    216             }
    217         }
    218         return true;
    219     }
    220 
    221     /**
    222      * Functionally equivalent to setText(), but explicitly announce changes.
    223      * If the new text is an extension of the old one, announce the addition.
    224      * Otherwise, e.g. after deletion, announce the entire new text.
    225      */
    226     public void changeTextTo(CharSequence newText) {
    227         final CharSequence oldText = getText();
    228         if (startsWith(newText, oldText)) {
    229             final int newLen = newText.length();
    230             final int oldLen = oldText.length();
    231             if (newLen == oldLen + 1) {
    232                 // The algorithm for pronouncing a single character doesn't seem
    233                 // to respect our hints.  Don't give it the choice.
    234                 final char c = newText.charAt(oldLen);
    235                 final int id = KeyMaps.keyForChar(c);
    236                 final String descr = KeyMaps.toDescriptiveString(getContext(), id);
    237                 if (descr != null) {
    238                     announceForAccessibility(descr);
    239                 } else {
    240                     announceForAccessibility(String.valueOf(c));
    241                 }
    242             } else if (newLen > oldLen) {
    243                 announceForAccessibility(newText.subSequence(oldLen, newLen));
    244             }
    245         } else {
    246             announceForAccessibility(newText);
    247         }
    248         setText(newText);
    249     }
    250 
    251     public boolean stopActionMode() {
    252         if (mActionMode != null) {
    253             mActionMode.finish();
    254             return true;
    255         }
    256         return false;
    257     }
    258 
    259     public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
    260         mOnTextSizeChangeListener = listener;
    261     }
    262 
    263     public void setOnPasteListener(OnPasteListener listener) {
    264         mOnPasteListener = listener;
    265     }
    266 
    267     private void paste() {
    268         final ClipboardManager clipboard = (ClipboardManager) getContext()
    269                 .getSystemService(Context.CLIPBOARD_SERVICE);
    270         final ClipData primaryClip = clipboard.getPrimaryClip();
    271         if (primaryClip != null && mOnPasteListener != null) {
    272             mOnPasteListener.onPaste(primaryClip);
    273         }
    274     }
    275 
    276     public interface OnTextSizeChangeListener {
    277         void onTextSizeChanged(TextView textView, float oldSize);
    278     }
    279 
    280     public interface OnPasteListener {
    281         boolean onPaste(ClipData clip);
    282     }
    283 }
    284