Auto Scale TextView Text to Fit within Bounds Auto Scale TextView Text to Fit within Bounds android android

Auto Scale TextView Text to Fit within Bounds


As a mobile developer, I was sad to find nothing native that supports auto resizing. My searches did not turn up anything that worked for me and in the end, I spent the better half of my weekend and created my own auto resize text view. I will post the code here and hopefully it will be useful for someone else.

This class uses a static layout with the text paint of the original text view to measure the height. From there, I step down by 2 font pixels and remeasure until I have a size that fits. At the end, if the text still does not fit, I append an ellipsis. I had requirements to animate the text and reuse views and this seems to work well on the devices I have and seems to run fast enough for me.

/** *               DO WHAT YOU WANT TO PUBLIC LICENSE *                    Version 2, December 2004 *  * Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> *  * Everyone is permitted to copy and distribute verbatim or modified * copies of this license document, and changing it is allowed as long * as the name is changed. *  *            DO WHAT YOU WANT TO PUBLIC LICENSE *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION *  *  0. You just DO WHAT YOU WANT TO. */import android.content.Context;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;/** * Text view that auto adjusts text size to fit within the view. * If the text size equals the minimum text size and still does not * fit, append with an ellipsis. *  * @author Chase Colburn * @since Apr 4, 2011 */public class AutoResizeTextView extends TextView {    // Minimum text size for this text view    public static final float MIN_TEXT_SIZE = 20;    // Interface for resize notifications    public interface OnTextResizeListener {        public void onTextResize(TextView textView, float oldSize, float newSize);    }    // Our ellipse string    private static final String mEllipsis = "...";    // Registered resize listener    private OnTextResizeListener mTextResizeListener;    // Flag for text and/or size changes to force a resize    private boolean mNeedsResize = false;    // Text size that is set from code. This acts as a starting point for resizing    private float mTextSize;    // Temporary upper bounds on the starting text size    private float mMaxTextSize = 0;    // Lower bounds for text size    private float mMinTextSize = MIN_TEXT_SIZE;    // Text view line spacing multiplier    private float mSpacingMult = 1.0f;    // Text view additional line spacing    private float mSpacingAdd = 0.0f;    // Add ellipsis to text that overflows at the smallest text size    private boolean mAddEllipsis = true;    // Default constructor override    public AutoResizeTextView(Context context) {        this(context, null);    }    // Default constructor when inflating from XML file    public AutoResizeTextView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    // Default constructor override    public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        mTextSize = getTextSize();    }    /**     * When text changes, set the force resize flag to true and reset the text size.     */    @Override    protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {        mNeedsResize = true;        // Since this view may be reused, it is good to reset the text size        resetTextSize();    }    /**     * If the text view size changed, set the force resize flag to true     */    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        if (w != oldw || h != oldh) {            mNeedsResize = true;        }    }    /**     * Register listener to receive resize notifications     * @param listener     */    public void setOnResizeListener(OnTextResizeListener listener) {        mTextResizeListener = listener;    }    /**     * Override the set text size to update our internal reference values     */    @Override    public void setTextSize(float size) {        super.setTextSize(size);        mTextSize = getTextSize();    }    /**     * Override the set text size to update our internal reference values     */    @Override    public void setTextSize(int unit, float size) {        super.setTextSize(unit, size);        mTextSize = getTextSize();    }    /**     * Override the set line spacing to update our internal reference values     */    @Override    public void setLineSpacing(float add, float mult) {        super.setLineSpacing(add, mult);        mSpacingMult = mult;        mSpacingAdd = add;    }    /**     * Set the upper text size limit and invalidate the view     * @param maxTextSize     */    public void setMaxTextSize(float maxTextSize) {        mMaxTextSize = maxTextSize;        requestLayout();        invalidate();    }    /**     * Return upper text size limit     * @return     */    public float getMaxTextSize() {        return mMaxTextSize;    }    /**     * Set the lower text size limit and invalidate the view     * @param minTextSize     */    public void setMinTextSize(float minTextSize) {        mMinTextSize = minTextSize;        requestLayout();        invalidate();    }    /**     * Return lower text size limit     * @return     */    public float getMinTextSize() {        return mMinTextSize;    }    /**     * Set flag to add ellipsis to text that overflows at the smallest text size     * @param addEllipsis     */    public void setAddEllipsis(boolean addEllipsis) {        mAddEllipsis = addEllipsis;    }    /**     * Return flag to add ellipsis to text that overflows at the smallest text size     * @return     */    public boolean getAddEllipsis() {        return mAddEllipsis;    }    /**     * Reset the text to the original size     */    public void resetTextSize() {        if (mTextSize > 0) {            super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);            mMaxTextSize = mTextSize;        }    }    /**     * Resize text after measuring     */    @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);    }    /**     * Resize the text size with default width and height     */    public void resizeText() {        int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();        int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();        resizeText(widthLimit, heightLimit);    }    /**     * Resize the text size with specified width and height     * @param width     * @param height     */    public void resizeText(int width, int height) {        CharSequence text = getText();        // Do not resize if the view does not have dimensions or there is no text        if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {            return;        }        if (getTransformationMethod() != null) {            text = getTransformationMethod().getTransformation(text, this);        }        // Get the text view's paint object        TextPaint textPaint = getPaint();        // Store the current text size        float oldTextSize = textPaint.getTextSize();        // 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;        // Get the required text height        int textHeight = getTextHeight(text, textPaint, width, targetTextSize);        // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes        while (textHeight > height && targetTextSize > mMinTextSize) {            targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);            textHeight = getTextHeight(text, textPaint, width, targetTextSize);        }        // If we had reached our minimum text size and still don't fit, append an ellipsis        if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {            // Draw using a static layout            // modified: use a copy of TextPaint for measuring            TextPaint paint = new TextPaint(textPaint);            // Draw using a static layout            StaticLayout layout = new StaticLayout(text, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);            // Check that we have a least one line of rendered text            if (layout.getLineCount() > 0) {                // Since the line at the specific vertical position would be cut off,                // we must trim up to the previous line                int lastLine = layout.getLineForVertical(height) - 1;                // If the text would not even fit on a single line, clear it                if (lastLine < 0) {                    setText("");                }                // Otherwise, trim to the previous line and add an ellipsis                else {                    int start = layout.getLineStart(lastLine);                    int end = layout.getLineEnd(lastLine);                    float lineWidth = layout.getLineWidth(lastLine);                    float ellipseWidth = textPaint.measureText(mEllipsis);                    // Trim characters off until we have enough room to draw the ellipsis                    while (width < lineWidth + ellipseWidth) {                        lineWidth = textPaint.measureText(text.subSequence(start, --end + 1).toString());                    }                    setText(text.subSequence(0, end) + mEllipsis);                }            }        }        // Some devices try to auto adjust line spacing, so force default line spacing        // and invalidate the layout as a side effect        setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);        setLineSpacing(mSpacingAdd, mSpacingMult);        // Notify the listener if registered        if (mTextResizeListener != null) {            mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);        }        // Reset force resize flag        mNeedsResize = false;    }    // Set the text size of the text paint object and use a static layout to render text off screen before measuring    private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {        // modified: make a copy of the original TextPaint object for measuring        // (apparently the object gets modified while measuring, see also the        // docs for TextView.getPaint() (which states to access it read-only)        TextPaint paintCopy = new TextPaint(paint);        // Update the text paint object        paintCopy.setTextSize(textSize);        // Measure using a static layout        StaticLayout layout = new StaticLayout(source, paintCopy, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);        return layout.getHeight();    }}

Warning. There is an important fixed bug affecting Android 3.1 - 4.04 causing all AutoResizingTextView widgets not to work. Please read: https://stackoverflow.com/a/21851157/2075875


From June 2018 Android officially started supporting this feature for Android 4.0 (API level 14) and higher.
Check it out at: Autosizing TextViews

With Android 8.0 (API level 26) and higher:

<?xml version="1.0" encoding="utf-8"?><TextView    android:layout_width="match_parent"    android:layout_height="200dp"    android:autoSizeTextType="uniform"    android:autoSizeMinTextSize="12sp"    android:autoSizeMaxTextSize="100sp"    android:autoSizeStepGranularity="2sp" />

Programmatically:

setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize,         int autoSizeStepGranularity, int unit)textView.setAutoSizeTextTypeUniformWithConfiguration(                1, 17, 1, TypedValue.COMPLEX_UNIT_DIP);


Android versions prior to Android 8.0 (API level 26):

<?xml version="1.0" encoding="utf-8"?><LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">  <TextView      android:layout_width="match_parent"      android:layout_height="200dp"      app:autoSizeTextType="uniform"      app:autoSizeMinTextSize="12sp"      app:autoSizeMaxTextSize="100sp"      app:autoSizeStepGranularity="2sp" /></LinearLayout>

Programmatically:

TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(TextView textView, int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit) TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(textView, 1, 17, 1,TypedValue.COMPLEX_UNIT_DIP);

Attention: TextView must have layout_width="match_parent" or absolute size!


UPDATE: Following code also fulfills the requirement of an ideal AutoScaleTextView as described here : Auto-fit TextView for Android and is marked as winner.

UPDATE 2: Support of maxlines added, now works fine before API level 16.

Update 3: Support for android:drawableLeft, android:drawableRight, android:drawableTop and android:drawableBottom tags added, thanks to MartinH's simple fix here.


My requirements were little bit different. I needed an efficient way to adjust size because I was animating an integer from, may be 0 to ~4000 in TextView in 2 seconds and I wanted to adjust the size accordingly. My solution works bit differently. Here is what final result looks like:

enter image description here

and the code that produced it:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:orientation="vertical"    android:padding="16dp" >    <com.vj.widgets.AutoResizeTextView        android:layout_width="match_parent"        android:layout_height="100dp"        android:ellipsize="none"        android:maxLines="2"        android:text="Auto Resized Text, max 2 lines"        android:textSize="100sp" /> <!-- maximum size -->    <com.vj.widgets.AutoResizeTextView        android:layout_width="match_parent"        android:layout_height="100dp"        android:ellipsize="none"        android:gravity="center"        android:maxLines="1"        android:text="Auto Resized Text, max 1 line"        android:textSize="100sp" /> <!-- maximum size -->    <com.vj.widgets.AutoResizeTextView        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Auto Resized Text"        android:textSize="500sp" /> <!-- maximum size --></LinearLayout>

And finally the java code:

import android.annotation.TargetApi;import android.content.Context;import android.content.res.Resources;import android.graphics.RectF;import android.os.Build;import android.text.Layout.Alignment;import android.text.StaticLayout;import android.text.TextPaint;import android.util.AttributeSet;import android.util.SparseIntArray;import android.util.TypedValue;import android.widget.TextView;public class AutoResizeTextView extends TextView {private interface SizeTester {    /**     *      * @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);}private RectF mTextRect = new RectF();private RectF mAvailableSpaceRect;private SparseIntArray mTextCachedSizes;private TextPaint mPaint;private float mMaxTextSize;private float mSpacingMult = 1.0f;private float mSpacingAdd = 0.0f;private float mMinTextSize = 20;private int mWidthLimit;private static final int NO_LINE_LIMIT = -1;private int mMaxLines;private boolean mEnableSizeCache = true;private boolean mInitiallized;public AutoResizeTextView(Context context) {    super(context);    initialize();}public AutoResizeTextView(Context context, AttributeSet attrs) {    super(context, attrs);    initialize();}public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {    super(context, attrs, defStyle);    initialize();}private void initialize() {    mPaint = new TextPaint(getPaint());    mMaxTextSize = getTextSize();    mAvailableSpaceRect = new RectF();    mTextCachedSizes = new SparseIntArray();    if (mMaxLines == 0) {        // no value was assigned during construction        mMaxLines = NO_LINE_LIMIT;    }    mInitiallized = true;}@Overridepublic void setText(final CharSequence text, BufferType type) {    super.setText(text, type);    adjustTextSize(text.toString());}@Overridepublic void setTextSize(float size) {    mMaxTextSize = size;    mTextCachedSizes.clear();    adjustTextSize(getText().toString());}@Overridepublic void setMaxLines(int maxlines) {    super.setMaxLines(maxlines);    mMaxLines = maxlines;    reAdjust();}public int getMaxLines() {    return mMaxLines;}@Overridepublic void setSingleLine() {    super.setSingleLine();    mMaxLines = 1;    reAdjust();}@Overridepublic void setSingleLine(boolean singleLine) {    super.setSingleLine(singleLine);    if (singleLine) {        mMaxLines = 1;    } else {        mMaxLines = NO_LINE_LIMIT;    }    reAdjust();}@Overridepublic void setLines(int lines) {    super.setLines(lines);    mMaxLines = lines;    reAdjust();}@Overridepublic void setTextSize(int unit, float size) {    Context c = getContext();    Resources r;    if (c == null)        r = Resources.getSystem();    else        r = c.getResources();    mMaxTextSize = TypedValue.applyDimension(unit, size,            r.getDisplayMetrics());    mTextCachedSizes.clear();    adjustTextSize(getText().toString());}@Overridepublic void setLineSpacing(float add, float mult) {    super.setLineSpacing(add, mult);    mSpacingMult = mult;    mSpacingAdd = add;}/** * Set the lower text size limit and invalidate the view *  * @param minTextSize */public void setMinTextSize(float minTextSize) {    mMinTextSize = minTextSize;    reAdjust();}private void reAdjust() {    adjustTextSize(getText().toString());}private void adjustTextSize(String string) {    if (!mInitiallized) {        return;    }    int startSize = (int) mMinTextSize;    int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom()        - getCompoundPaddingTop();    mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft()        - getCompoundPaddingRight();    mAvailableSpaceRect.right = mWidthLimit;    mAvailableSpaceRect.bottom = heightLimit;    super.setTextSize(            TypedValue.COMPLEX_UNIT_PX,            efficientTextSizeSearch(startSize, (int) mMaxTextSize,                    mSizeTester, mAvailableSpaceRect));}private final SizeTester mSizeTester = new SizeTester() {    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)    @Override    public int onTestSize(int suggestedSize, RectF availableSPace) {        mPaint.setTextSize(suggestedSize);        String text = getText().toString();        boolean singleline = getMaxLines() == 1;        if (singleline) {            mTextRect.bottom = mPaint.getFontSpacing();            mTextRect.right = mPaint.measureText(text);        } else {            StaticLayout layout = new StaticLayout(text, mPaint,                    mWidthLimit, Alignment.ALIGN_NORMAL, mSpacingMult,                    mSpacingAdd, true);            // return early if we have more lines            if (getMaxLines() != NO_LINE_LIMIT                    && layout.getLineCount() > getMaxLines()) {                return 1;            }            mTextRect.bottom = layout.getHeight();            int maxWidth = -1;            for (int i = 0; i < layout.getLineCount(); i++) {                if (maxWidth < layout.getLineWidth(i)) {                    maxWidth = (int) layout.getLineWidth(i);                }            }            mTextRect.right = maxWidth;        }        mTextRect.offsetTo(0, 0);        if (availableSPace.contains(mTextRect)) {            // may be too small, don't worry we will find the best match            return -1;        } else {            // too big            return 1;        }    }};/** * 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 enableSizeCache(boolean enable) {    mEnableSizeCache = enable;    mTextCachedSizes.clear();    adjustTextSize(getText().toString());}private int efficientTextSizeSearch(int start, int end,        SizeTester sizeTester, RectF availableSpace) {    if (!mEnableSizeCache) {        return binarySearch(start, end, sizeTester, availableSpace);    }    String text = getText().toString();    int key = text == null ? 0 : text.length();    int size = mTextCachedSizes.get(key);    if (size != 0) {        return size;    }    size = binarySearch(start, end, sizeTester, availableSpace);    mTextCachedSizes.put(key, size);    return size;}private static int binarySearch(int start, int end, SizeTester sizeTester,        RectF availableSpace) {    int lastBest = start;    int lo = start;    int hi = end - 1;    int mid = 0;    while (lo <= hi) {        mid = (lo + hi) >>> 1;        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;}@Overrideprotected void onTextChanged(final CharSequence text, final int start,        final int before, final int after) {    super.onTextChanged(text, start, before, after);    reAdjust();}@Overrideprotected void onSizeChanged(int width, int height, int oldwidth,        int oldheight) {    mTextCachedSizes.clear();    super.onSizeChanged(width, height, oldwidth, oldheight);    if (width != oldwidth || height != oldheight) {        reAdjust();    }}}