Auto-fit TextView for Android Auto-fit TextView for Android android android

Auto-fit TextView for Android


Thanks to MartinH's simple fix here, this code also takes care of android:drawableLeft, android:drawableRight, android:drawableTop and android:drawableBottom tags.


My answer here should make you happy Auto Scale TextView Text to Fit within Bounds

I have modified your test case:

@Overrideprotected void onCreate(final Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    final ViewGroup container = (ViewGroup) findViewById(R.id.container);    findViewById(R.id.button1).setOnClickListener(new OnClickListener() {        @Override        public void onClick(final View v) {            container.removeAllViews();            final int maxWidth = container.getWidth();            final int maxHeight = container.getHeight();            final AutoResizeTextView fontFitTextView = new AutoResizeTextView(MainActivity.this);            final int width = _random.nextInt(maxWidth) + 1;            final int height = _random.nextInt(maxHeight) + 1;            fontFitTextView.setLayoutParams(new FrameLayout.LayoutParams(                    width, height));            int maxLines = _random.nextInt(4) + 1;            fontFitTextView.setMaxLines(maxLines);            fontFitTextView.setTextSize(500);// max size            fontFitTextView.enableSizeCache(false);            fontFitTextView.setBackgroundColor(0xff00ff00);            final String text = getRandomText();            fontFitTextView.setText(text);            container.addView(fontFitTextView);            Log.d("DEBUG", "width:" + width + " height:" + height                    + " text:" + text + " maxLines:" + maxLines);        }    });}

I am posting code here at per android developer's request:

Final effect:

Enter image description here

Sample Layout file:

<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 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 mInitializedDimens;    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;        }    }    @Override    public void setTextSize(float size) {        mMaxTextSize = size;        mTextCachedSizes.clear();        adjustTextSize();    }    @Override    public void setMaxLines(int maxlines) {        super.setMaxLines(maxlines);        mMaxLines = maxlines;        adjustTextSize();    }    public int getMaxLines() {        return mMaxLines;    }    @Override    public void setSingleLine() {        super.setSingleLine();        mMaxLines = 1;        adjustTextSize();    }    @Override    public void setSingleLine(boolean singleLine) {        super.setSingleLine(singleLine);        if (singleLine) {            mMaxLines = 1;        } else {            mMaxLines = NO_LINE_LIMIT;        }        adjustTextSize();    }    @Override    public void setLines(int lines) {        super.setLines(lines);        mMaxLines = lines;        adjustTextSize();    }    @Override    public 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();    }    @Override    public 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;        adjustTextSize();    }    private void adjustTextSize() {        if (!mInitializedDimens) {            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);        }        int key = getText().toString().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 the 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);        adjustTextSize();    }    @Override    protected void onSizeChanged(int width, int height, int oldwidth,            int oldheight) {        mInitializedDimens = true;        mTextCachedSizes.clear();        super.onSizeChanged(width, height, oldwidth, oldheight);        if (width != oldwidth || height != oldheight) {            adjustTextSize();        }    }}

Warning:

Beware of this resolved bug in Android 3.1 (Honeycomb) though.


I've modified M-WaJeEh's answer a bit to take into account compound drawables on the sides.

The getCompoundPaddingXXXX() methods return padding of the view + drawable space. See for example: TextView.getCompoundPaddingLeft()

Issue:This fixes the measurement of the width and height of the TextView space available for the text. If we don't take the drawable size into account, it is ignored and the text will end up overlapping the drawable.


Updated segment adjustTextSize(String):

private void adjustTextSize(final String text) {  if (!mInitialized) {    return;  }  int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();  mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();  mAvailableSpaceRect.right = mWidthLimit;  mAvailableSpaceRect.bottom = heightLimit;  int maxTextSplits = text.split(" ").length;  AutoResizeTextView.super.setMaxLines(Math.min(maxTextSplits, mMaxLines));  super.setTextSize(      TypedValue.COMPLEX_UNIT_PX,      binarySearch((int) mMinTextSize, (int) mMaxTextSize,                   mSizeTester, mAvailableSpaceRect));}


Ok I have used the last week to massively rewrite my code to precisely fit your test. You can now copy this 1:1 and it will immediately work - including setSingleLine(). Please remember to adjust MIN_TEXT_SIZE and MAX_TEXT_SIZE if you're going for extreme values.

Converging algorithm looks like this:

for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) {    // Go to the mean value...    testSize = (upperTextSize + lowerTextSize) / 2;    // ... inflate the dummy TextView by setting a scaled textSize and the text...    mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor);    mTestView.setText(text);    // ... call measure to find the current values that the text WANTS to occupy    mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);    int tempHeight = mTestView.getMeasuredHeight();    // ... decide whether those values are appropriate.    if (tempHeight >= targetFieldHeight) {        upperTextSize = testSize; // Font is too big, decrease upperSize    }    else {        lowerTextSize = testSize; // Font is too small, increase lowerSize    }}

And the whole class can be found here.

The result is very flexible now. This works the same declared in xml like so:

<com.example.myProject.AutoFitText    android:id="@+id/textView"    android:layout_width="match_parent"    android:layout_height="0dp"    android:layout_weight="4"    android:text="@string/LoremIpsum" />

... as well as built programmatically like in your test.

I really hope you can use this now. You can call setText(CharSequence text) now to use it by the way. The class takes care of stupendously rare exceptions and should be rock-solid. The only thing that the algorithm does not support yet is:

  • Calls to setMaxLines(x) where x >= 2

But I have added extensive comments to help you build this if you wish to!


Please note:

If you just use this normally without limiting it to a single line then there might be word-breaking as you mentioned before. This is an Android feature, not the fault of the AutoFitText. Android will always break words that are too long for a TextView and it's actually quite a convenience. If you want to intervene here than please see my comments and code starting at line 203. I have already written an adequate split and the recognition for you, all you'd need to do henceforth is to devide the words and then modify as you wish.

In conclusion: You should highly consider rewriting your test to also support space chars, like so:

final Random _random = new Random();final String ALLOWED_CHARACTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890";final int textLength = _random.nextInt(80) + 20;final StringBuilder builder = new StringBuilder();for (int i = 0; i < textLength; ++i) {    if (i % 7 == 0 && i != 0) {        builder.append(" ");    }    builder.append(ALLOWED_CHARACTERS.charAt(_random.nextInt(ALLOWED_CHARACTERS.length())));}((AutoFitText) findViewById(R.id.textViewMessage)).setText(builder.toString());

This will produce very beutiful (and more realistic) results.
You will find commenting to get you started in this matter as well.

Good luck and best regards