UPDATE: [I’ve created lib project on gitHub]
Having the need of auto resizable EditText usage in Android is certainly something you’ll come across creating interactive applications at some point. I came to that point and to me it seemed that somebody has to have developed this custom widget/library and put it somewhere (gitHub) for reutilization. Partially, I was right, but it turned out that most of the solutions that are out there, need(ed) fixing, changing and a lot of customization (which takes a lot of time and I don’t advise doing, because it can consume as much time as starting from scratch).
First of all, let me say that there’s no “EditText” auto resize widgets and all of them extend TextView, which won’t be an issue since EditText extends TextView, right? Wrong. And I’ll get back at this later. Some of the best/worth mentioning AutoResize libs/widgets:
1) AndroidDeveloperLB/AutoFitTextView
2) skimarxall/RealTextView (editText widget is included in the lib)
3) danclarke/AutoResizeTextView
4) lucamtudor/AutoResizeTextView
5) grantland/android-autofittextview
The stackoverflow question that tracks this issue of auto-resizing TextView (EditText) is here. It seems that android-developer has aggregated all the possible approaches, ideas and concepts in order to tackle this issue most efficiently (I suggest you read the post and go through it before you continue reading). As a result of this AndroidDeveloperLB/AutoFitTextView lib/widget was created and I’ll discuss only this solution since I think is the most complete. If you’re willing you can try the others, most of the things here should apply to them also.
You start off by changing the extend class from TextView to EditText. For those of you that are lazy to do this, here is copy-paste:
public class AutoResizeEditText extends EditText {
private static final int NO_LINE_LIMIT = -1;
private final RectF _availableSpaceRect = new RectF();
private final SparseIntArray _textCachedSizes = new SparseIntArray();
private final SizeTester _sizeTester;
private float _maxTextSize;
private float _spacingMult = 1.0f;
private float _spacingAdd = 0.0f;
private float _minTextSize;
private int _widthLimit;
private int _maxLines;
private boolean _enableSizeCache = true;
private boolean _initiallized = false;
private TextPaint paint;
private interface SizeTester {
/**
* AutoResizeEditText
*
* @param suggestedSize
* Size of text to be tested
* @param availableSpace
* available space in which text must fit
* @return an integer < 0 if after applying {@code suggestedSize} to
* text, it takes less space than {@code availableSpace}, > 0
* otherwise
*/
public int onTestSize(int suggestedSize, RectF availableSpace);
}
public AutoResizeEditText(final Context context) {
this(context, null, 0);
}
public AutoResizeEditText(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoResizeEditText(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
// using the minimal recommended font size
_minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
12, getResources().getDisplayMetrics());
_maxTextSize = getTextSize();
if (_maxLines == 0)
// no value was assigned during construction
_maxLines = NO_LINE_LIMIT;
// prepare size tester:
_sizeTester = new SizeTester() {
final RectF textRect = new RectF();
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public int onTestSize(final int suggestedSize,
final RectF availableSPace) {
paint.setTextSize(suggestedSize);
final String text = getText().toString();
final boolean singleline = getMaxLines() == 1;
if (singleline) {
textRect.bottom = paint.getFontSpacing();
textRect.right = paint.measureText(text);
} else {
final StaticLayout layout = new StaticLayout(text, paint,
_widthLimit, Alignment.ALIGN_NORMAL, _spacingMult,
_spacingAdd, true);
// return early if we have more lines
Log.d("NLN", "Current Lines = "+Integer.toString(layout.getLineCount()));
Log.d("NLN", "Max Lines = "+Integer.toString(getMaxLines()));
if (getMaxLines() != NO_LINE_LIMIT
&& layout.getLineCount() > getMaxLines())
return 1;
textRect.bottom = layout.getHeight();
int maxWidth = -1;
for (int i = 0; i < layout.getLineCount(); i++)
if (maxWidth < layout.getLineWidth(i))
maxWidth = (int) layout.getLineWidth(i);
textRect.right = maxWidth;
}
textRect.offsetTo(0, 0);
if (availableSPace.contains(textRect))
// may be too small, don't worry we will find the best match
return -1;
// else, too big
return 1;
}
};
_initiallized = true;
}
@Override
public void setTypeface(final Typeface tf) {
if (paint == null)
paint = new TextPaint(getPaint());
paint.setTypeface(tf);
super.setTypeface(tf);
}
@Override
public void setTextSize(final float size) {
_maxTextSize = size;
_textCachedSizes.clear();
adjustTextSize();
}
@Override
public void setMaxLines(final int maxlines) {
super.setMaxLines(maxlines);
_maxLines = maxlines;
reAdjust();
}
@Override
public int getMaxLines() {
return _maxLines;
}
@Override
public void setSingleLine() {
super.setSingleLine();
_maxLines = 1;
reAdjust();
}
@Override
public void setSingleLine(final boolean singleLine) {
super.setSingleLine(singleLine);
if (singleLine)
_maxLines = 1;
else
_maxLines = NO_LINE_LIMIT;
reAdjust();
}
@Override
public void setLines(final int lines) {
super.setLines(lines);
_maxLines = lines;
reAdjust();
}
@Override
public void setTextSize(final int unit, final float size) {
final Context c = getContext();
Resources r;
if (c == null)
r = Resources.getSystem();
else
r = c.getResources();
_maxTextSize = TypedValue.applyDimension(unit, size,
r.getDisplayMetrics());
_textCachedSizes.clear();
adjustTextSize();
}
@Override
public void setLineSpacing(final float add, final float mult) {
super.setLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
/**
* Set the lower text size limit and invalidate the view
*
* @param
*/
public void setMinTextSize(final float minTextSize) {
_minTextSize = minTextSize;
reAdjust();
}
private void reAdjust() {
adjustTextSize();
}
private void adjustTextSize() {
if (!_initiallized)
return;
final int startSize = (int) _minTextSize;
final int heightLimit = getMeasuredHeight()
- getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMeasuredWidth() - getCompoundPaddingLeft()
- getCompoundPaddingRight();
if (_widthLimit <= 0)
return;
_availableSpaceRect.right = _widthLimit;
_availableSpaceRect.bottom = heightLimit;
super.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
efficientTextSizeSearch(startSize, (int) _maxTextSize,
_sizeTester, _availableSpaceRect));
}
/**
* Enables or disables size caching, enabling it will improve performance
* where you are animating a value inside TextView. This stores the font
* size against getText().length() Be careful though while enabling it as 0
* takes more space than 1 on some fonts and so on.
*
* @param enable
* enable font size caching
*/
public void setEnableSizeCache(final boolean enable) {
_enableSizeCache = enable;
_textCachedSizes.clear();
adjustTextSize();
}
private int efficientTextSizeSearch(final int start, final int end,
final SizeTester sizeTester, final RectF availableSpace) {
if (!_enableSizeCache)
return binarySearch(start, end, sizeTester, availableSpace);
final String text = getText().toString();
final int key = text == null ? 0 : text.length();
int size = _textCachedSizes.get(key);
if (size != 0)
return size;
size = binarySearch(start, end, sizeTester, availableSpace);
_textCachedSizes.put(key, size);
return size;
}
private int binarySearch(final int start, final int end,
final SizeTester sizeTester, final RectF availableSpace) {
int lastBest = start;
int lo = start;
int hi = end - 1;
int mid = 0;
while (lo <= hi) {
mid = lo + hi >>> 1;
final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
if (midValCmp < 0) {
lastBest = lo;
lo = mid + 1;
} else if (midValCmp > 0) {
hi = mid - 1;
lastBest = hi;
} else
return mid;
}
// make sure to return last best
// this is what should always be returned
return lastBest;
}
@Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
super.onTextChanged(text, start, before, after);
reAdjust();
}
@Override
protected void onSizeChanged(final int width, final int height,
final int oldwidth, final int oldheight) {
_textCachedSizes.clear();
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
reAdjust();
}
}
Needless to say, you need to import this in your project. Put the newly imported widget in xml (add values for maxLines, maxLength in order to make the fitting more precise) and reference it in your Activity/Fragment as follows:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:real="http://schemas.android.com/apk/res-auto"
android:id="@+id/rlRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.testapplication.MainActivity" >
<FrameLayout
android:id="@+id/container"
android:layout_width="500dp"
android:layout_height="333dp"
android:layout_centerInParent="true"
android:background="#ffff0000" >
<com.example.autofit.et.AutoResizeEditText
android:id="@+id/rET"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="#FF00FF"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_vertical|center_horizontal"
android:hint="HINT"
android:isScrollContainer="false"
android:inputType="textMultiLine|textNoSuggestions"
android:maxLength="240"
android:textColor="#000000"
android:textSize="90sp" />
</FrameLayout>
</RelativeLayout>
public class MainActivity extends Activity {
private AutoResizeEditText mAutoResizeEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAutoResizeEditText = (AutoResizeEditText) findViewById(R.id.rET);
}
@Override
protected void onResume() {
super.onResume();
}
When you run the application you might not be able to click on the EditText, i.e. it behaves as TextView. Now coming back to the part that I mentioned in the beginning, in order to fix this, you MUST provide this lines in your xml declaration of the file:
android:focusable="true"
android:focusableInTouchMode="true"
It seems that we’re done and everything should work fine, but this s*it happens:



Text is not in bounds and pressing new line seems to distort the expected behavior. There are a lot of possible limitations in order to set bounds (maxLines, maxLength, maxTextSize, maxHeight) to the text and stop this aberrant behavior, yet most of them doesn’t seem to work. After long hours spent on this and a lot of cup of coffees, this is what I think works best. The CRUCIAL FIX is anchoring your logic on maxHeight by adding the following lines after the widget reference:
mAutoResizeEditText = (AutoResizeEditText) findViewById(R.id.rET);
mAutoResizeEditText.setEnabled(true);
mAutoResizeEditText.setFocusableInTouchMode(true);
mAutoResizeEditText.setFocusable(true);
mAutoResizeEditText.setEnableSizeCache(false);
mAutoResizeEditText.setMovementMethod(null);
// can be added after layout inflation; it doesn't have to be fixed
// value
mAutoResizeEditText.setMaxHeight(330);
BONUS. To make things look slick (soft keys disappear if you tap outside the EditText container; also if font size is too small due to a lot of empty new lines, the EditText string is trimmed) add this code:
@Override
protected void onResume() {
super.onResume();
setupUI(findViewById(R.id.rlRoot), mAutoResizeEditText);
}
Where setupUI is defined in the following manner:
public void setupUI(View view, final AutoResizeEditText aText) {
// if the view is not instance of AutoResizeEditText
// i.e. if the user taps outside of the box
if (!(view instanceof AutoResizeEditText)) {
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard();
Log.d("TXTS",
"Text Size = "
+ aText.getTextSize());
if (aText.getTextSize() < 50f) {
// you can define your minSize, in this case is 50f
// trim all the new lines and set the text as it was
// before
aText.setText(aText.getText().toString().replaceAll("(?m)^[ \t]*\r?\n", ""));
}
return false;
}
});
}
// If a layout container, iterate over children and seed recursion.
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
View innerView = ((ViewGroup) view).getChildAt(i);
setupUI(innerView, aText);
}
}
}
public void hideSoftKeyboard() {
InputMethodManager inputMethodManager = (InputMethodManager) this
.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (this.getCurrentFocus() != null
&& this.getCurrentFocus().getWindowToken() != null)
inputMethodManager.hideSoftInputFromWindow(this
.getCurrentFocus().getWindowToken(), 0);
}
Result:



If you have any questions or suggestions, feel free to use the comments section of the blog post or hit me up.
P.S. The videos were recorder using Recordable.