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