I started with Chase's AutoResizeTextView class, and made a minor change so it would fit both vertically and horizontally.
I also discovered a bug which causes a Null Pointer Exception in the Layout Editor (in Eclipse) under some rather obscure conditions.
Change 1: Fit the text both vertically and horizontally
Chase's original version reduces the text size until it fits vertically, but allows the text to be wider than the target. In my case, I needed the text to fit a specified width.
This change makes it resize until the text fits both vertically and horizontally.
In resizeText(
int,
int)
change from:
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
while(textHeight > height && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
}
to:
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
int textWidth = getTextWidth(text, textPaint, width, targetTextSize);
while(((textHeight >= height) || (textWidth >= width) ) && targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
textWidth = getTextWidth(text, textPaint, width, targetTextSize);
}
Then, at the end of the file, append the getTextWidth()
routine; it's just a slightly modified getTextHeight()
. It probably would be more efficient to combine them to one routine which returns both height and width.
private int getTextWidth(CharSequence source, TextPaint paint, int width, float textSize) {
paint.setTextSize(textSize);
StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
layout.draw(sTextResizeCanvas);
return layout.getWidth();
}
Change 2: Fix a EmptyStackException in the Eclipse Android Layout Editor
Under rather obscure and very precise conditions, the Layout Editor will fail to display the graphical display of the layout; it will throw an "EmptyStackException: null" exception in com.android.ide.eclipse.adt.
The conditions required are:
- create an AutoResizeTextView widget
- create a style for that widget
- specify the text item in the style; not in the widget definition
as in:
res/layout/main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<com.ajw.DemoCrashInADT.AutoResizeTextView
android:id="@+id/resizingText"
style="@style/myTextStyle" />
</LinearLayout>
res/values/myStyles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="myTextStyle" parent="@android:style/Widget.TextView">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:text">some message</item>
</style>
</resources>
With these files, selecting the Graphical Layout tab when editing main.xml
will display:
error!
EmptyStackException: null
Exception details are logged in Window > Show View > Error Log
instead of the graphical view of the layout.
To keep an already too-long story shorter, I tracked this down to the following lines (again in resizeText
):
// If there is a max text size set, use the lesser of that and the default text size
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;
The problem is that under the specific conditions, mTextSize is never initialized; it has the value 0.
With the above, targetTextSize
is set to zero (as a result of Math.min).
That zero is passed to getTextHeight()
(and getTextWidth()
) as the textSize
argument. When it gets to
layout.draw(sTextResizeCanvas);
we get the exception.
It's more efficient to test if (mTextSize == 0)
at the beginning of resizeText()
rather than testing in getTextHeight()
and getTextWidth()
; testing earlier saves all the intervening work.
With these updates, the file (as in my crash-demo test app) is now:
package com.ajw.DemoCrashInADT;
import android.content.Context;
import android.graphics.Canvas;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
public class AutoResizeTextView extends TextView {
public static final float MIN_TEXT_SIZE = 20;
public interface OnTextResizeListener {
public void onTextResize(TextView textView, float oldSize, float newSize);
}
private static final Canvas sTextResizeCanvas = new Canvas();
private static final String mEllipsis = "...";
private OnTextResizeListener mTextResizeListener;
private boolean mNeedsResize = false;
private float mTextSize;
private float mMaxTextSize = 0;
private float mMinTextSize = MIN_TEXT_SIZE;
private float mSpacingMult = 1.0f;
private float mSpacingAdd = 0.0f;
private boolean mAddEllipsis = true;
public AutoResizeTextView(Context context) {
this(context, null);
}
public AutoResizeTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTextSize = getTextSize();
}
@Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
mNeedsResize = true;
resetTextSize();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh) {
mNeedsResize = true;
}
}
public void setOnResizeListener(OnTextResizeListener listener) {
mTextResizeListener = listener;
}
@Override
public void setTextSize(float size) {
super.setTextSize(size);
mTextSize = getTextSize();
}
@Override
public void setTextSize(int unit, float size) {
super.setTextSize(unit, size);
mTextSize = getTextSize();
}
@Override
public void setLineSpacing(float add, float mult) {
super.setLineSpacing(add, mult);
mSpacingMult = mult;
mSpacingAdd = add;
}
public void setMaxTextSize(float maxTextSize) {
mMaxTextSize = maxTextSize;
requestLayout();
invalidate();
}
public float getMaxTextSize() {
return mMaxTextSize;
}
public void setMinTextSize(float minTextSize) {
mMinTextSize = minTextSize;
requestLayout();
invalidate();
}
public float getMinTextSize() {
return mMinTextSize;
}
public void setAddEllipsis(boolean addEllipsis) {
mAddEllipsis = addEllipsis;
}
public boolean getAddEllipsis() {
return mAddEllipsis;
}
public void resetTextSize() {
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
mMaxTextSize = mTextSize;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed || mNeedsResize) {
int widthLimit = (right - left) - getCompoundPaddingLeft()
- getCompoundPaddingRight();
int heightLimit = (bottom - top) - getCompoundPaddingBottom()
- getCompoundPaddingTop();
resizeText(widthLimit, heightLimit);
}
super.onLayout(changed, left, top, right, bottom);
}
public void resizeText() {
int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();
int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();
resizeText(widthLimit, heightLimit);
}
public void resizeText(int width, int height) {
CharSequence text = getText();
if (text == null || text.length() == 0 || height <= 0 || width <= 0
|| mTextSize == 0) {
return;
}
TextPaint textPaint = getPaint();
float oldTextSize = textPaint.getTextSize();
float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize)
: mTextSize;
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
int textWidth = getTextWidth(text, textPaint, width, targetTextSize);
while (((textHeight > height) || (textWidth > width))
&& targetTextSize > mMinTextSize) {
targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
textHeight = getTextHeight(text, textPaint, width, targetTextSize);
textWidth = getTextWidth(text, textPaint, width, targetTextSize);
}
if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
StaticLayout layout = new StaticLayout(text, textPaint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);
layout.draw(sTextResizeCanvas);
int lastLine = layout.getLineForVertical(height) - 1;
int start = layout.getLineStart(lastLine);
int end = layout.getLineEnd(lastLine);
float lineWidth = layout.getLineWidth(lastLine);
float ellipseWidth = textPaint.measureText(mEllipsis);
while (width < lineWidth + ellipseWidth) {
lineWidth = textPaint.measureText(text.subSequence(start, --end + 1)
.toString());
}
setText(text.subSequence(0, end) + mEllipsis);
}
textPaint.setTextSize(targetTextSize);
setLineSpacing(mSpacingAdd, mSpacingMult);
if (mTextResizeListener != null) {
mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);
}
mNeedsResize = false;
}
private int getTextHeight(CharSequence source, TextPaint paint, int width,
float textSize) {
paint.setTextSize(textSize);
StaticLayout layout = new StaticLayout(source, paint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
layout.draw(sTextResizeCanvas);
return layout.getHeight();
}
private int getTextWidth(CharSequence source, TextPaint paint, int width,
float textSize) {
paint.setTextSize(textSize);
StaticLayout layout = new StaticLayout(source, paint, width,
Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
layout.draw(sTextResizeCanvas);
return layout.getWidth();
}
}
A big thank you to Chase for posting the initial code. I enjoyed reading through it to see how it worked, and I'm pleased to be able to add to it.