1 /*
2  * Copyright (C) 2014 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.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Paint;
22 import android.graphics.Paint.FontMetricsInt;
23 import android.graphics.Rect;
24 import android.os.Parcelable;
25 import android.text.method.ScrollingMovementMethod;
26 import android.text.TextPaint;
27 import android.util.AttributeSet;
28 import android.util.TypedValue;
29 import android.view.ActionMode;
30 import android.view.Menu;
31 import android.view.MenuItem;
32 import android.view.MotionEvent;
33 import android.widget.EditText;
34 import android.widget.TextView;
35 
36 public class CalculatorEditText extends EditText {
37 
38     private final static ActionMode.Callback NO_SELECTION_ACTION_MODE_CALLBACK =
39             new ActionMode.Callback() {
40         @Override
41         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
42             return false;
43         }
44 
45         @Override
46         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
47             // Prevents the selection action mode on double tap.
48             return false;
49         }
50 
51         @Override
52         public void onDestroyActionMode(ActionMode mode) {
53         }
54 
55         @Override
56         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
57             return false;
58         }
59     };
60 
61     private final float mMaximumTextSize;
62     private final float mMinimumTextSize;
63     private final float mStepTextSize;
64 
65     // Temporary objects for use in layout methods.
66     private final Paint mTempPaint = new TextPaint();
67     private final Rect mTempRect = new Rect();
68 
69     private int mWidthConstraint = -1;
70     private OnTextSizeChangeListener mOnTextSizeChangeListener;
71 
CalculatorEditText(Context context)72     public CalculatorEditText(Context context) {
73         this(context, null);
74     }
75 
CalculatorEditText(Context context, AttributeSet attrs)76     public CalculatorEditText(Context context, AttributeSet attrs) {
77         this(context, attrs, 0);
78     }
79 
CalculatorEditText(Context context, AttributeSet attrs, int defStyle)80     public CalculatorEditText(Context context, AttributeSet attrs, int defStyle) {
81         super(context, attrs, defStyle);
82 
83         final TypedArray a = context.obtainStyledAttributes(
84                 attrs, R.styleable.CalculatorEditText, defStyle, 0);
85         mMaximumTextSize = a.getDimension(
86                 R.styleable.CalculatorEditText_maxTextSize, getTextSize());
87         mMinimumTextSize = a.getDimension(
88                 R.styleable.CalculatorEditText_minTextSize, getTextSize());
89         mStepTextSize = a.getDimension(R.styleable.CalculatorEditText_stepTextSize,
90                 (mMaximumTextSize - mMinimumTextSize) / 3);
91 
92         a.recycle();
93 
94         setCustomSelectionActionModeCallback(NO_SELECTION_ACTION_MODE_CALLBACK);
95         if (isFocusable()) {
96             setMovementMethod(ScrollingMovementMethod.getInstance());
97         }
98         setTextSize(TypedValue.COMPLEX_UNIT_PX, mMaximumTextSize);
99         setMinHeight(getLineHeight() + getCompoundPaddingBottom() + getCompoundPaddingTop());
100     }
101 
102     @Override
onTouchEvent(MotionEvent event)103     public boolean onTouchEvent(MotionEvent event) {
104         if (event.getActionMasked() == MotionEvent.ACTION_UP) {
105             // Hack to prevent keyboard and insertion handle from showing.
106             cancelLongPress();
107         }
108         return super.onTouchEvent(event);
109     }
110 
111     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)112     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
113         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
114 
115         mWidthConstraint =
116                 MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
117         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(getText().toString()));
118     }
119 
120     @Override
onSaveInstanceState()121     public Parcelable onSaveInstanceState() {
122         super.onSaveInstanceState();
123 
124         // EditText will freeze any text with a selection regardless of getFreezesText() ->
125         // return null to prevent any state from being preserved at the instance level.
126         return null;
127     }
128 
129     @Override
onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)130     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
131         super.onTextChanged(text, start, lengthBefore, lengthAfter);
132 
133         final int textLength = text.length();
134         if (getSelectionStart() != textLength || getSelectionEnd() != textLength) {
135             // Pin the selection to the end of the current text.
136             setSelection(textLength);
137         }
138         setTextSize(TypedValue.COMPLEX_UNIT_PX, getVariableTextSize(text.toString()));
139     }
140 
141     @Override
setTextSize(int unit, float size)142     public void setTextSize(int unit, float size) {
143         final float oldTextSize = getTextSize();
144         super.setTextSize(unit, size);
145 
146         if (mOnTextSizeChangeListener != null && getTextSize() != oldTextSize) {
147             mOnTextSizeChangeListener.onTextSizeChanged(this, oldTextSize);
148         }
149     }
150 
setOnTextSizeChangeListener(OnTextSizeChangeListener listener)151     public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
152         mOnTextSizeChangeListener = listener;
153     }
154 
getVariableTextSize(String text)155     public float getVariableTextSize(String text) {
156         if (mWidthConstraint < 0 || mMaximumTextSize <= mMinimumTextSize) {
157             // Not measured, bail early.
158             return getTextSize();
159         }
160 
161         // Capture current paint state.
162         mTempPaint.set(getPaint());
163 
164         // Step through increasing text sizes until the text would no longer fit.
165         float lastFitTextSize = mMinimumTextSize;
166         while (lastFitTextSize < mMaximumTextSize) {
167             final float nextSize = Math.min(lastFitTextSize + mStepTextSize, mMaximumTextSize);
168             mTempPaint.setTextSize(nextSize);
169             if (mTempPaint.measureText(text) > mWidthConstraint) {
170                 break;
171             } else {
172                 lastFitTextSize = nextSize;
173             }
174         }
175 
176         return lastFitTextSize;
177     }
178 
179     @Override
getCompoundPaddingTop()180     public int getCompoundPaddingTop() {
181         // Measure the top padding from the capital letter height of the text instead of the top,
182         // but don't remove more than the available top padding otherwise clipping may occur.
183         getPaint().getTextBounds("H", 0, 1, mTempRect);
184 
185         final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
186         final int paddingOffset = -(fontMetrics.ascent + mTempRect.height());
187         return super.getCompoundPaddingTop() - Math.min(getPaddingTop(), paddingOffset);
188     }
189 
190     @Override
getCompoundPaddingBottom()191     public int getCompoundPaddingBottom() {
192         // Measure the bottom padding from the baseline of the text instead of the bottom, but don't
193         // remove more than the available bottom padding otherwise clipping may occur.
194         final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
195         return super.getCompoundPaddingBottom() - Math.min(getPaddingBottom(), fontMetrics.descent);
196     }
197 
198     public interface OnTextSizeChangeListener {
onTextSizeChanged(TextView textView, float oldSize)199         void onTextSizeChanged(TextView textView, float oldSize);
200     }
201 }
202