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
17package com.android.calculator2;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Paint;
22import android.graphics.Paint.FontMetricsInt;
23import android.graphics.Rect;
24import android.os.Parcelable;
25import android.text.method.ScrollingMovementMethod;
26import android.text.TextPaint;
27import android.util.AttributeSet;
28import android.util.TypedValue;
29import android.view.ActionMode;
30import android.view.Menu;
31import android.view.MenuItem;
32import android.view.MotionEvent;
33import android.widget.EditText;
34import android.widget.TextView;
35
36public 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
72    public CalculatorEditText(Context context) {
73        this(context, null);
74    }
75
76    public CalculatorEditText(Context context, AttributeSet attrs) {
77        this(context, attrs, 0);
78    }
79
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
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
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
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
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
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
151    public void setOnTextSizeChangeListener(OnTextSizeChangeListener listener) {
152        mOnTextSizeChangeListener = listener;
153    }
154
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
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
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 {
199        void onTextSizeChanged(TextView textView, float oldSize);
200    }
201}
202