922

I'm looking for an optimal way to resize wrapping text in a TextView so that it will fit within its getHeight and getWidth bounds. I'm not simply looking for a way to wrap the text- I want to make sure it both wraps and is small enough to fit entirely on the screen.

I've seen a few cases on StackOverflow where auto resizing was needed, but they are either very special cases with hack solutions, have no solution, or involve re-drawing the TextView recursively until it is small enough (which is memory intense and forces the user to watch the text shrink step-by-step with every recursion).

But I'm sure somebody out there has found a good solution that doesn't involve what I'm doing: writing several heavy routines that parse and measure the text, resize the text, and repeat until a suitably small size has been found.

What routines does TextView use to wrap the text? Couldn't those be somehow used to predict whether text will be small enough?

tl;dr: is there a best-practice way to auto-resize a TextView to fit, wrapped, in its getHeight and getWidth bounds?

Nathan Fig
  • 14,103
  • 9
  • 40
  • 56
  • I also tried using the getEllipsisCount in StaticLayout to detect when text was going out of bounds, but that wasn't working for me, I had asked about that too: http://stackoverflow.com/questions/5084647/staticlayout-getellipsiscount-returning-0-when-i-know-its-ellipsizing – Nathan Fig Mar 11 '11 at 21:05
  • Why don;t you draw a nine patch textview ? it will auto adjust to its bounds. Am I getting you right brother? – AZ_ Mar 17 '11 at 05:45
  • Nine patch textview? I'm not familiar with nine-patch, it appears to be a image format thing though... I'm looking for documentation on how that could be used with a textview. Know where I might get more info? – Nathan Fig Mar 17 '11 at 14:28
  • Reading up on it a little further I'm getting the impression that nine-patch is just for resizing something that already has a defined format- but I need something that will take a string and find its optimum size and format within certain bounds. – Nathan Fig Mar 17 '11 at 14:45
  • Possible duplicate of: http://stackoverflow.com/a/7875656/435605 – AlikElzin-kilaka Dec 19 '11 at 15:13
  • I found a library that seems to do this automatically: http://ankri.de/autoscale-textview/ Haven't tested it though – Display name Dec 06 '12 at 18:07
  • I had a short look at the library mentioned by Seppl - it lacks the feature of ellipsising the text if it's too long even at the smallest textsize, which Chase's solution provides. So, if the text is too long, the textview's height increases, which is not what the question asked for. – Ridcully Apr 14 '13 at 08:01

35 Answers35

1114

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

Community
  • 1
  • 1
Chase
  • 11,081
  • 7
  • 39
  • 39
  • 10
    This is actually the best solution I've seen yet, very well done. It's not completely suited to my needs because of the way Android likes to break words when wrapping, so I'll be sticking with my custom solution- but your class is much better for general use. – Nathan Fig Apr 05 '11 at 16:11
  • Seeing the following on changing from Portrait to Landscape: java.lang.ArrayIndexOutOfBoundsException at android.text.StaticLayout.getLineStart(StaticLayout.java:1243) at com.groupon.view.AutoResizeTextView.resizeText(AutoResizeTextView.java:248) at com.groupon.view.AutoResizeTextView.onDraw(AutoResizeTextView.java:199) – emmby Jun 16 '11 at 22:57
  • There is still a Bug with padding. You need to add return layout.getHeight()-getPaddingBottom()-getPaddingTop(); at the end of getTextHeight() – passsy Aug 11 '11 at 11:16
  • I don't think that is a bug actually. That method is trying to find the required height for the text. It then compares against the view height which has had its padding subtracted. Have you had any problems with this? – Chase Aug 26 '11 at 07:47
  • 1
    Chase, thanks for posting AutoResizeTextView! I made a minor change and found a bug that causes a problem in the layout editor. Because of the length of my response, I'll have to add it as a new answer to the original question; comments are limited in length... – Alan Jay Weiner Oct 30 '11 at 02:24
  • Alan, thanks for the updates and glad this helped. I went ahead and added the check for `mTextSize == 0`. As for the measuring to fill a specific width, I didn't update my code yet because I want to have a more thorough look but it seems promising. Thanks! – Chase Nov 01 '11 at 08:20
  • A little enhancement that allows not only shrink text but also fit the layout can be found [here on github](https://github.com/serso/android_calculator/blob/master/src/main/java/org/solovyev/android/view/AutoResizeTextView.java#L221). Also I've found that sometimes algorithm works not very well for letters and i always append '|' symbol to the end of text, male calculations and then delete this symbol (can also be found in the sources from the link) – se.solovyev Nov 14 '11 at 06:20
  • 256
    In the spirit of Android, it's all yours! And hopefully when you build something that may be useful, you are able to share that :) – Chase Dec 13 '11 at 00:39
  • This solution isn't working for me on the Galaxy Nexus. It works great on my older Android devices though. Unfortunately, I don't have time to debug this right now, but thought I'd let everyone know I'm seeing issues with Android 4.0.1. – Justin Dec 20 '11 at 04:34
  • Looks like sometimes on the Motorola Xoom I'm running 3.1 or 3.2 there's an ArrayIndexOutOfBoundsException happening on the line containing "int start = layout.getLineStart(lastLine);" seems to be due to int lastLine being set incorrectly. – Andrew Winter Jan 04 '12 at 14:19
  • 1
    Let me look into this in the morning. I don't remember why I was subtracting 1 from the result. That functions seems to be returning a 0 based line index. Could be a bug. I will update. – Chase Jan 04 '12 at 14:57
  • I just took off the -1 and added a line that would only subtract one if > 0. I'm working on fixing a padding issue with 3+ as well, not sure about 4 yet. I'll share the code if it's relevant. – Andrew Winter Jan 05 '12 at 04:36
  • The Padding issue I mentioned in the last comment isn't relative to AutoResizeTextField. Great code by the way, next time you're in Ebisu I owe you a drink! – Andrew Winter Jan 05 '12 at 05:01
  • 7
    Ok, so I made an update to the code. Subtracting 1 from the line index was intentional and I added a comment. The error you got was probably from when there was not enough height to render even one line of text. I added a check and cleared the text in that case. (I work in Shibuya so I'd be up for a beer anytime :) – Chase Jan 06 '12 at 03:26
  • Perfect! As for that beer, Just tack my user name on to my website and shoot me a message with the subject beer. – Andrew Winter Jan 06 '12 at 04:49
  • On another note, what's the reasoning behind using textPaint.setTextSize rather than the native setTextSize and a temporary TextPaint for the size checks? I ask because it seems to have some issues in 3 when doing Gravity.CENTER and some other issues in 4 – Andrew Winter Jan 06 '12 at 05:09
  • I assume by native you mean calling `setTextSize()` on the text view itself. Doing this does almost the exact same thing except also invalidates the layout. The reason you don't want to use some other temp `TextPaint` object is because you want to be sure to consider all the same formatting (font size, family, style, etc.) as the text view itself. Borrowing this text paint object seemed to be the most logically solution. I wonder what kind of issues you are having exactly on the newer platforms... – Chase Jan 06 '12 at 05:54
  • What about using TextPaint textPaint = new TextPaint(); textPaint.set(this.getPaint()); to pass the formatting? – Andrew Winter Jan 13 '12 at 15:19
  • You could, but I don't quite see the advantage. I am modifying the original text paint directly but I will be adjusting the font size any way so what do you gain from creating another object? Of course, if this solves some issues with 3 or 4, it may be worth looking into. – Chase Jan 14 '12 at 02:23
  • Instead of constantly redrawing and measuring the text on a canvas until you find the biggest size that fits, could you just draw it on a canvas with a size that is too big and save it as a .png and then draw the .png to stretch/shrink within in the size you want? You would want an original size that is close or bigger than the final size because shrinking a large image looks better than stretching a small image. Or would that method end up being more work and less efficient than just redrawing and measuring the text until it fits? – Nick Mar 08 '12 at 14:43
  • 3
    I believe that would not work for several reasons. Apart from being hard to implement, the resulting image would only be scaling the height since the width is generally determined and this code re-sizes the font until there is no more vertical overflow. Also, when scaling text images, you will most likely find the text to be blurry if the scaling does not happen on an exact pixel boundary. I find the rendering and remeasuring of text to be fast enough for all of my use cases. Have you experienced performance issues? – Chase Mar 08 '12 at 15:41
  • Has anyone made a fix that works on tablets? This auto resize only seems to work on smart phones. Thanks for all your hard work though! – SpoiledTechie.com Apr 14 '12 at 01:52
  • 1
    I am using this on tablets without any issues. What kind of problems are you having? Keep in mind that certain layouts such as Relative layouts with wrap content settings will not constraint the size and the text resizing will not work. – Chase Apr 16 '12 at 01:49
  • 1
    There's issues with this code on Android 4.1 that I'm currently investigating. – denizmveli Jul 11 '12 at 04:58
  • 7
    Did you try the latest edited version. I made a small change for Jelly Bean about a week ago. This should probably be up on github. – Chase Jul 11 '12 at 05:43
  • 1
    It's a very minor point but you don't need import android.graphics.Canvas any more – Carlos P Jul 15 '12 at 16:34
  • This dont works by default? I dont know how this works. textone = (AutoResizeTextView) findViewById(R.id.textone); If text is AutoResizeTextView must be resized automatically or you have to do something before? help please – Genaut Aug 19 '12 at 16:28
  • I'm having a bit of trouble. In my Android 4.0 Intel emulator, it seems like the text size prior to resizing is being used to calculate the amount of text shown in the TextView, even though I can see in the emulator calls to do the resizing. On my Android 2.3 devices i can see that the text gets resized, but I'm unable to validate on a 4.0 device. Anyone seen this issue? – Nick Campion Aug 21 '12 at 22:05
  • it doesn't seem that this solution makes the font stretch to its maximal size (according to its measured size) . not only that, but the text itself isn't positioned correctly according to the gravity and it get truncated (the fonts,not the text string) . is it possible – android developer Aug 30 '12 at 14:25
  • This class is not intended to increase font sizes. To achieve this behaviour however, you can set a large default size and have the text view resize down to a size that fits. As for the font truncating, and not the text string, I am not sure I follow. – Chase Aug 31 '12 at 03:18
  • @Chase sir I am new to Android dev and I think your solution will work for me. I am trying to create a calculator type but it exceeds from my textview. My question is how ca n I use that java class? – Janwel Aug 31 '12 at 06:07
  • 1
    @Janwel I use this class directly in my XML layout files. They will then be inflated at runtime. You can find more info here http://developer.android.com/guide/topics/ui/custom-components.html – Chase Aug 31 '12 at 07:32
  • @Chase , if i set the font size to be too large (so that it will auto-fit to the max size possible) , it can become multiple lines , so a single word becomes multiple words, each on a different line . – android developer Sep 10 '12 at 11:44
  • 2
    I am having some issues with android 4.0. What is happening is the text is shrunk down to fit the string inside it but only the part of the string that would have fit without shrinking is being drawn. So if I have two words only the first word is shown but it is shown with the smaller text size. This works perfect on devices with pre 4.0. – Bobbake4 Nov 05 '12 at 16:51
  • 4
    Hey @Chase, thanks for the great class! Have you seen issues with text rendering in different sizes in an adapter? I'm using a SimpleCursorAdapter to put text in a GridView. I'm using a ViewBinder to set variable text from the adapter to the AutoResizeTextView, and then immediately resizing it (in the ViewBinder). I'm calling resizeWidth with a width and height that I've checked is consistent. But on different view loads or during scrolling, I get different text sizes for the same string, (e.g. http://i49.tinypic.com/2znwxo2.png vs http://i50.tinypic.com/mrpo35.png), at times overflowing. – annie Nov 14 '12 at 02:30
  • 1
    I really need to move this over to github. android developer, As you know, this class does not scale up. It can be changed to do so but you have to be careful regarding word and character wrapping. Bobbake4, I am using several 4.0+ devices and do not have the same issue. Did you ever find out what the problem was? annie, Keep in mind that an adapter will reuse views so you may have to reset values at some point before measuring. Try debugging the onMeasure and onLayout methods for more insight. – Chase Nov 14 '12 at 05:39
  • 2
    For those of you who want to know how to implement this see here; http://stackoverflow.com/questions/9713669/auto-scale-text-size – Garbit Jan 07 '13 at 17:03
  • I get InflateException: "Binary XML file line #151: Error inflating class AutoResizeTextView" and crash. – Jonny Jan 11 '13 at 07:31
  • Are you sure you have your namespace correct for the class definition in the XML file? – Chase Jan 14 '13 at 09:59
  • @Chase Is there any way or any future intention for the TextView to resize itself, rather than just it's text? It would be very helpful if they could. – RileyE Jan 25 '13 at 16:07
  • @Chase, 1) [this is what happens](http://i.imgur.com/tkvGoze.png) with your auto resizing TextView (middle number). Do you know why? 2) I'm interested in your `getTextHeight()` method. Do you know why in portrait I can use `viewHeight/layout.getHeight()` as a ratio for `setTextSize()`, but in landscape I can't use `viewWidth/layout.getWidth()` because is =1 ? – vault Jan 25 '13 at 21:40
  • I am making a desk clock app (for myself--there are a lot on the market, but all the good ones are paid/ads and I like to learn). I want to use this to scale the clock time to fill the TextView. Do I just set the TextSize to something huge and let this scale it down to fit? Since the time gets updated each minute, the rescaling will happen each minute, right? Is there a way to save the scaling since it will be basically the same for any given time? I also saw that font sizes should be set with sp vs dp. Will this code react appropriately if the user has set a font scaling in their preferences? – MrGibbage Feb 27 '13 at 15:47
  • Just to leave some notes. There is a method of determining text height AND width with only one function call and without StaticLayout. This results in significant performance increase (up to 2 or more times faster). Here is the method: http://stackoverflow.com/a/15398662/1048087 – Prizoff Mar 14 '13 at 01:26
  • @vault see good and quick method for determining width/height in my answer, linked in upper comment. – Prizoff Mar 14 '13 at 01:29
  • You should license this code, because right now nobody is allowed to use it due to copyright. You know. – lhunath May 30 '13 at 16:11
  • 1
    Added WTFPL. Does this mean everything on SO is copyrighted unless otherwise stated? If so, I have a lot code to cut from my projects! :) – Chase May 31 '13 at 01:39
  • 2
    @chase please take a minute and add it to github. I would love to track your changes and have a wiki/issue section. It works on 2.3 but on 4.2 it is wrapping the text in an odd way. I can't quite figure why the textview text is breaking where it is but the code determines a good font size but it falls off the view because of the weird line breaks pushes it down. Has anyone seen this? – Jeff Jun 04 '13 at 04:44
  • 4
    Doesn't work. Tried it on new Huawei Y200 running Android 4.0 – AndroidDev Jul 30 '13 at 10:21
  • I do have ANR's in my project (with lots of users) when using this code. (Although we might have used an old version of it). – Peterdk Aug 20 '13 at 12:42
  • Will the above work for this issue: http://stackoverflow.com/questions/18497760/having-an-issue-with-textsize-within-textview – Si8 Aug 28 '13 at 21:03
  • 7
    does not work for TextView inside ListView Items as well as inside Include other layout – Vladyslav Matviienko Nov 03 '13 at 16:44
  • In the past when creating custom views I have had problems when the second constructor calls the third constructor with 0, and it calls super() with 0 for the style int. What I did was call super() with 2 params in the second constructor, and also call getTextSizeDirectly(). Same for first constructor. – Andrew Mackenzie Nov 22 '13 at 08:25
  • For @annie and @metalurgus and all those that have problems displaying this `TextView` extension in `ListView` items, I found [this implementation](http://stackoverflow.com/a/7875656/22904) to work, albeit with a slight modification to avoid scaling up the text size. – Giulio Piancastelli Dec 06 '13 at 18:17
  • I'm trying to use this, but it doesn't quite seem to work. It shrinks a text that's too big, but not quite enough to fit in the last word. I'm setting the line count to 2. Is that maybe what the issue is? – KairisCharm Dec 09 '13 at 18:38
  • I am seeing the same issue as @Bobbake4 on a 4.1 device. The text scales but only displays what would have fit had it not scaled down. Also never shows ellipses. –  Dec 11 '13 at 20:25
  • 1
    I made two changes to get it to work, removed textHeight > height check and set the text at the end: if (mAddEllipsis && targetTextSize == mMinTextSize) {...} else { setText(text); } –  Dec 11 '13 at 21:13
  • I'm now trying to get the text to center vertically after it resizes and have tried damn near everything. Any suggestions? –  Dec 11 '13 at 22:43
  • For error about ArrayIndexOutOfBoundsException, it seems to happen when maxLine = 1 and ellipsize is activated. To avoid this crash, you have to use setSingleLine(true), though the singleLine constant is deprecated since API Level 3 (see https://code.google.com/p/android/issues/detail?id=33868) – Nicolas Buquet Nov 07 '14 at 10:15
  • Hmm this is not working or me when I set the text programmatically. My view is relatively complex though, any ideas? – Daniel Wilson Nov 24 '14 at 16:35
  • 1
    When I use this it shrinks long text fine. Later, if I change the text to something like two characters long, the text view doesn't grow to the max size. I have min and max size set in sp dimensions. Anyone seen this or have a suggestion? I am simply calling setText() on the component. I'll debug it in the meantime. Thanks! – Lo-Tan Feb 11 '15 at 16:52
  • @Lo-Tan yes this is because you have set the width and height of the TextView, so onLayout won't be called if you change the text. This is a workaround. In onTextChanged write this at the end: ViewGroup.LayoutParams lp = getLayoutParams(); if (lp != null && lp.height != ViewGroup.LayoutParams.WRAP_CONTENT && lp.width != ViewGroup.LayoutParams.WRAP_CONTENT) { mNeedsResize = false; resizeText(); } – DominicM Feb 20 '15 at 18:40
  • This is definitely a bug and should be corrected! Otherwise this is great, thanks alot – DominicM Feb 20 '15 at 18:41
  • 1
    @DominicM thanks for figuring that out! I started using [autofittextview](https://github.com/grantland/android-autofittextview) and it worked out of the gate so I stuck with it :) – Lo-Tan Feb 20 '15 at 19:07
  • Hey, Did you try this text view with list view? Its also changing other row's text. I've tried this view with setting max width. Its not working properly for me. Can you help me, how can i use this using maxWidth attribute? thanks – Jatin Apr 16 '15 at 09:17
  • Exactly == or similar that autofit.... Not work with "weigth='1'" or with "margins" ... (I know is awesome code, but not perfect), –  Apr 17 '15 at 09:28
  • Okay, about the bug already found a solution to run on android version 4 up. – Rodolfo Jun 16 '15 at 17:43
  • 2
    I must say that this answer values $100000, even more – kaitian521 Jan 04 '16 at 07:33
  • Does not work in a RecyclerView. Text gets resized but since I'm wrapping the height of my content initially the height is too big. Then I scroll (and view is re used) and only at that point does the height get updated properly. How to fix? – MobileMon Jul 06 '16 at 12:35
  • After swip to refresh in adapter text size changes randomly. Anyone have any solution? – Yog Guru Jul 09 '16 at 06:22
  • @Chase after re- rotate my devise i can't get original text size. is this bug? or is I have to add some code for it? – Harin Kaklotar Jul 13 '16 at 07:34
  • @Chase my friend before you i was using [android-autofittextview](https://github.com/grantland/android-autofittextview) but this is incredible. i want to say sth for you. If you add this to `Github` i am and future visiters pretty appreciated for. Very very good solution. Thank you and have a good day. – Yasin Kaçmaz Jul 17 '16 at 13:45
  • There is still a Bug in custom adapter. In custom adapter some time small and some time big textview size. How to solve this bug please help me.. – Jignesh Goyani Sep 26 '16 at 08:56
  • Thanks. It works better than the second solution. In the second solution the last phrase was not shown. – CoolMind Dec 09 '16 at 14:37
  • I added a call to requestLayout() at the end of onTextChanged() because for me the resizing would not always trigger when the text was changed. Possibly because the textview was not visible at the time of change? – Nino van Hooff Apr 11 '17 at 08:57
  • I don't see how this textView fit it's parent. It did not working for me – IntoTheDeep Jul 04 '17 at 07:18
241

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!

john-salib
  • 575
  • 11
  • 26
Think Twice Code Once
  • 5,273
  • 1
  • 22
  • 32
  • 4
    Future readers, remember to add `implementation 'com.android.support:support-compat:28.0.0'` in `app/build.gradle` for the `app: ...` attributes to work. – daka Jan 28 '19 at 18:53
  • You write: "From June 2018 Android officially started supporting this feature for Android 4.0 (API level 14) and higher" ??.. Attribute autoSizeTextType is only used in API level 26 and higher. – nnyerges Feb 09 '19 at 23:38
  • 8
    @nnyerges you can use support app compat for older API version ( < 26), use: **app:autoSizeTextType="uniform"** – Think Twice Code Once Feb 09 '19 at 23:53
  • What I stumbled upon is, that you should always test this with different text sizes you can set in the Android settings. Otherwise the `autoSizeMaxTextSize` might be too low and the text is cut off. – L3n95 Jan 13 '20 at 18:52
  • TextView must have `android:lines="1"` and in `layout_width="0dp"` in ConstraintLayout – Black_Zerg Aug 10 '20 at 17:00
  • Is there a way to get the text size that was used after applying setAutoSizeText...? For example: I have a table and I use setAutoSizeText on the first row, how can I get the value used so I can then apply the same text size to all other rows? I try calling textView.textSize, but the value is never changed. – iOS_Mouse Sep 04 '20 at 20:12
  • 1
    @ThinkTwiceCodeOnce, do you know if there are any limitation in developing app widgets? this code is not working for me in an app widget – Gianmarco Nov 23 '20 at 14:45
  • If anyone has `android:singleLine="true"` , it wont work, have to remove it in order to work. – Reejesh PK May 22 '21 at 04:36
148

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;
}

@Override
public void setText(final CharSequence text, BufferType type) {
    super.setText(text, type);
    adjustTextSize(text.toString());
}

@Override
public void setTextSize(float size) {
    mMaxTextSize = size;
    mTextCachedSizes.clear();
    adjustTextSize(getText().toString());
}

@Override
public void setMaxLines(int maxlines) {
    super.setMaxLines(maxlines);
    mMaxLines = maxlines;
    reAdjust();
}

public int getMaxLines() {
    return mMaxLines;
}

@Override
public void setSingleLine() {
    super.setSingleLine();
    mMaxLines = 1;
    reAdjust();
}

@Override
public void setSingleLine(boolean singleLine) {
    super.setSingleLine(singleLine);
    if (singleLine) {
        mMaxLines = 1;
    } else {
        mMaxLines = NO_LINE_LIMIT;
    }
    reAdjust();
}

@Override
public void setLines(int lines) {
    super.setLines(lines);
    mMaxLines = lines;
    reAdjust();
}

@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(getText().toString());
}

@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;
    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;

}

@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(int width, int height, int oldwidth,
        int oldheight) {
    mTextCachedSizes.clear();
    super.onSizeChanged(width, height, oldwidth, oldheight);
    if (width != oldwidth || height != oldheight) {
        reAdjust();
    }
}
}
Community
  • 1
  • 1
M-WaJeEh
  • 16,562
  • 9
  • 59
  • 93
  • well, I have checked your issue. You don't need this AutoResizeTextView your problem is something else. I have commented on your question. This issue has nothing to do with size. – M-WaJeEh Aug 29 '13 at 17:40
  • I noticed that there is @TargetApi(Build.VERSION_CODES.JELLY_BEAN) Can this work on 2.3? – Nick Jian Sep 30 '13 at 08:29
  • @簡子堯 Yes it will work. @TargetApi is just used to tell Lint that don't worry we have got it covered. The issue is `getMaxLines ()` method, it was added in API level 16. We have given our own implementation of `getMaxLines` so it will work. Documentation of @TargetApi says **Indicates that Lint should treat this type as targeting a given API level, no matter what the project target is.** http://stackoverflow.com/questions/14341042/what-is-better-suppresslint-or-targetapi – M-WaJeEh Sep 30 '13 at 09:10
  • Caused by: java.lang.ClassCastException: android.text.SpannableStringBuilder cannot be cast to java.lang.String –  Nov 02 '13 at 17:49
  • @matheszabi The code wrongfully assumed that `getText()` will always contain `String` object so the whole code was embedded with `(String)getText()` which was wrong. I have updated the code and replaced all such occurrences with `getText().toString()`. Hopefully it will solve your problem. – M-WaJeEh Nov 04 '13 at 05:50
  • I love your solution and use it in my app. Do you have an open-source license for it? – Ben Feb 13 '14 at 06:49
  • 11
    Glad you liked it, yeah I have a license check here. https://docs.google.com/document/d/1ELL7FzCsKclSvHu1kKH2XRR7zTC1__uS79s-dGuRYy8/edit?usp=sharing – M-WaJeEh Feb 13 '14 at 06:57
  • 8
    It looks like this won't work with custom Typefaces, as things get cut off vertically. How would you go about addressing that? Has anyone tried with custom fonts? – RealCasually Mar 07 '14 at 21:00
  • Sometime it give wrong result,issue with samsung gallary S-3, it shows small font for short text length. – RobinHood Mar 24 '14 at 11:10
  • 1
    @RobinHood Sometimes `StaticLayout` does not respect `width`. Just replace last returning lines of `onTestSize()` with this 'if (availableSpace.contains(mTextRect)) { // may be too small, don't worry we will find the best match return -1; } else { if (mTextRect.bottom < availableSpace.bottom && mTextRect.right > availableSpace.right) { // hack :O return -1; } // too big return 1; }' – M-WaJeEh Mar 24 '14 at 16:28
  • I initially used the accepted answer to the original question, but using it in a ListAdapter, the text would not be resized correctly until after the view had been scrolled off-screen and back again. This answer works immediately. – randallmeadows Aug 01 '14 at 20:28
  • This one worked for me across all 4.0+ devices. I had problems with Chase's solution on an HTC One S running 4.0.4. – dhaag23 Aug 07 '14 at 20:04
  • @M-WaJeEh Hello, This crashes for me if I have paddingLeft and right in the AutoResizeTextView what seems to be happening is the mWidthLimit comes out negative. – mbwasi Jul 02 '15 at 19:02
  • @M-WaJeEh thank for your effort, Onew quick question, Can use this code for EditText by the just exteding EditText ? – Lavekush Agrawal Jul 28 '15 at 06:18
  • @Lavekush Interesting question, never thought of that. Extend it from `EditText` and then try android:layout_height="100dp" android:maxLines="1" and write something. I guess it should work. – M-WaJeEh Jul 28 '15 at 07:25
  • Thank you, this code works very well. I am having one issue though: For a programmatically added TextView, I notice that the text first appears at its min height. I click on a standard edittext in this layout which triggers the `AutoResizeTextView` to readjust its size, and I notice that the size will get bigger. As I keep messing around with the edittext, the `AutoResizeTextView` adjusts larger and larger until it reaches the point where it fits perfectly. Any idea why the resize isn't working correctly and I need to trigger the onSizeChanged() method repeatedly until i get the right size? – h_k Sep 03 '15 at 15:32
  • Thanks for this. Had to set the text inside onPreDrawListener in order for it to determine the available space. – black Mar 15 '16 at 09:27
41

Actually a solution is in Google's DialogTitle class... though it's not as effective as the accepted one, it's a lot simpler and is easy to adapt.

public class SingleLineTextView extends TextView {

  public SingleLineTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    setSingleLine();
    setEllipsize(TruncateAt.END);
  }

  public SingleLineTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setSingleLine();
    setEllipsize(TruncateAt.END);
  }

  public SingleLineTextView(Context context) {
    super(context);
    setSingleLine();
    setEllipsize(TruncateAt.END);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final Layout layout = getLayout();
    if (layout != null) {
      final int lineCount = layout.getLineCount();
      if (lineCount > 0) {
        final int ellipsisCount = layout.getEllipsisCount(lineCount - 1);
        if (ellipsisCount > 0) {

          final float textSize = getTextSize();

          // textSize is already expressed in pixels
          setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1));

          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
      }
    }
  }

}
Idolon
  • 26,581
  • 12
  • 93
  • 121
crios
  • 579
  • 5
  • 6
  • 2
    Wonder why this hasn't been +1'd more. It's certainly a hack by nature but a very simple solution. For an app that's only laying out a few, non-scrolling SimpleLineTextViews this is the easiest way to go. Great find! – Tom Jan 24 '13 at 19:19
  • This solution doesn't work unless you call `requestLayout` in `onTextChanged`. Even with this fix, the solution here is not applicable in many cases as reducing the textSize by 1 seems not works in all cases: we may need to reduce the textSize further. – Zhi Wang Jul 31 '13 at 03:39
  • @ZhiWang, I don't think you read well the code here. It won't reduce the textSize only by 1, it is a recursive method. It reduces by 1 and measures again, until it gets the ideal size. It is a simple and good solution – Sergio Carneiro Mar 02 '14 at 03:50
  • 10
    @SergioCarneiro nice find, but `super.onMeasure(widthMeasureSpec, heightMeasureSpec);` doesn't cause a recursion, but `measure(widthMeasureSpec, heightMeasureSpec);`. So I changed to 'measure(widthMeasureSpec, heightMeasureSpec);" and it works. I just tested on Android 4.4(Nexus 5) and Android 4.0.4(Samsung SII-LTE) and it worked fine with both of them (I even used custom OTF Japanese font). Grab my fix here: https://gist.github.com/mrleolink/0dfeef749da1b854a44b – Leo Jun 30 '14 at 12:06
  • 2
    best answer. using LeoLink's version, it works well on my 2.3 and 4.0 emulators and actual devices. if used in a listview, remember to call requestLayout() after setting text. – adbie Jan 20 '15 at 11:41
  • 1
    I have forked @LeoLink version, it use `maxLines` and `requestLayout()` on `setText` like @adbie said. FixedLineTextView -> https://gist.github.com/Kevinrob/09742d9069e4e4e4ab66 – Kevin Robatel Apr 24 '15 at 07:57
  • LeoLink's modified version worked fine for me. Original did not. – azdev Nov 18 '15 at 15:52
  • this solution won't work if parent layout having `width=0dp` and `layout_weight` is set. In my case TextView is inside GridView and text is utf-8 encoded. – kiranking Nov 29 '18 at 12:19
36

I started with Chase's solution, but had to adapt two things before it was working as expected on my device (Galaxy Nexus, Android 4.1):

  1. using a copy of TextPaint for measuring layout The documentation for TextView.getPaint() states that it should be used read-only, so I made a copy in both places where we use the paint object for measuring:

    // 1. in resizeText()
    if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {
      // Draw using a static layout
      // modified: use a copy of TextPaint for measuring
      TextPaint paint = new TextPaint(textPaint);
    
    // 2. in getTextHeight()
    private int getTextHeight(CharSequence source, TextPaint originalPaint, 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 paint = new TextPaint(originalPaint);
      // Update the text paint object
      paint.setTextSize(textSize);
      ...
    
  2. adding a unit to setting the text size

    // modified: setting text size via this.setTextSize (instead of textPaint.setTextSize(targetTextSize))
    setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);
    setLineSpacing(mSpacingAdd, mSpacingMult);
    

With these two modifications the solution is working perfectly for me, thanks Chase! I don't know whether it is due to Android 4.x that the original solution was not working. In case you want to see it in action or test whether it really works on your device, you can have a look at my flashcard app Flashcards ToGo where I use this solution to scale the text of a flashcard. The text can have arbitrary length, and the flashcards are displayed in different activities, sometimes smaller sometimes bigger, plus in landscape + portrait mode, and I haven't found any corner case where the solution would not work properly...

onoelle
  • 369
  • 3
  • 2
24

AppcompatTextView now supports auto sizing starting from Support Library 26.0. TextView in Android O also works same way. More info can be found here. A simple demo app can be found here.

<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="wrap_content">

      <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:autoSizeTextType="uniform"
        app:autoSizeMinTextSize="12sp"
        app:autoSizeMaxTextSize="100sp"
        app:autoSizeStepGranularity="2sp"
      />

</LinearLayout>
makata
  • 1,939
  • 2
  • 22
  • 20
  • 3
    I had to set `android:lines="1"` in the TextView in order to resize the text. Without this attribute, the text was wrapped in two lines. – rewgoes May 21 '17 at 14:30
  • what's the import to make this work? I get build errors when I try 26.0.0-beta-1 – Psest328 Jun 01 '17 at 14:29
  • 1
    @Psest328, Are you using maven { url "https://maven.google.com" } ? – makata Jul 11 '17 at 15:27
23

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:

// 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);
    }

to:

// Get the required text height
int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
int textWidth  = getTextWidth(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) || (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.

// Set the text size of the text paint object and use a static layout to render text off screen before measuring
private int getTextWidth(CharSequence source, TextPaint paint, int width, float textSize) {
    // Update the text paint object
    paint.setTextSize(textSize);
    // Draw using a static layout
    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:

//
// from:  http://stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds
//
//

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;

/**
 * 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.
 *
 * 2011-10-29 changes by Alan Jay Weiner
 *              * change to fit both vertically and horizontally  
 *              * test mTextSize for 0 in resizeText() to fix exception in Layout Editor
 *
 * @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);
    }

    // Off screen canvas for text size rendering
    private static final Canvas sTextResizeCanvas = new Canvas();

    // 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() {
        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
        // or if mTextSize has not been initialized
        if (text == null || text.length() == 0 || height <= 0 || width <= 0
                || mTextSize == 0) {
            return;
        }

        // 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);
        int textWidth = getTextWidth(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) || (textWidth > width))
                && targetTextSize > mMinTextSize) {
            targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);
            textHeight = getTextHeight(text, textPaint, width, targetTextSize);
            textWidth = getTextWidth(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
            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);

            // 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
        textPaint.setTextSize(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) {
        // Update the text paint object
        paint.setTextSize(textSize);
        // Draw using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width,
                Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
        layout.draw(sTextResizeCanvas);
        return layout.getHeight();
    }


    // Set the text size of the text paint object and use a static layout to
    // render text off screen before measuring
    private int getTextWidth(CharSequence source, TextPaint paint, int width,
            float textSize) {
        // Update the text paint object
        paint.setTextSize(textSize);
        // Draw using a static layout
        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.

Alan Jay Weiner
  • 1,029
  • 9
  • 8
  • 3
    BUG DETECTED: `getTextWidth()` doesn't work at all as you are passing the desired width in `StaticLayout` constructor. Guess what width would be returned in that case from `getWidth()` method? – se.solovyev Nov 12 '11 at 12:12
  • A note for future people like me who stumble across this: the Paint object has a [measureText](http://developer.android.com/reference/android/graphics/Paint.html) method that can be called to get the width of the text. – Xono Sep 17 '12 at 04:26
  • 2
    Beautiful. This code worked for me on Android 4.1 on my Galaxy Nexus in my KeepScore app (https://github.com/nolanlawson/KeepScore/), whereas Chase's version didn't. Feature request: please put this code on GitHub, guys! StackOverflow is not the place for patches and code reviews. :) – nlawson Dec 28 '12 at 20:56
  • 1
    As mentioned above, layout.getWidth() just returns the width it was created with. What I had to do to make this work was to create the Layout with a width of 4096, and then call getLineWidth() on all the lines and return the max value. – Edward Falk Jan 14 '13 at 05:28
  • And as far as measureText() is concerned, I don't know how well it works on multi-line text, and I'm pretty sure it doesn't handle Spannable text which might contain attributes that would affect the text size. – Edward Falk Jan 14 '13 at 05:29
  • To make this work, I need to call `requestLayout` in `onTextChanged`, anyone met with this kind of problem? – Zhi Wang Jul 31 '13 at 04:36
  • 1
    22 upvote for a non working code/ why?! Igave multiple lines and doesn't fit the space –  Nov 02 '13 at 18:01
  • Hi Alan. We'd like to use this code, but it doesn't have licensing information attached. Can we please use it under the DO WHAT YOU WANT TO license, like the original you based it off? Thanks, Gerv (gerv@mozilla.org) – Gervase Markham Jan 30 '14 at 10:07
  • Hi Gervase. Absolutely, definitely! Use the code wherever you want. Glad it's useful! – Alan Jay Weiner Feb 13 '14 at 00:44
  • doesn't work with Android 4.4 and dynamic layout height (layout_weight) – Malachiasz Feb 18 '14 at 10:21
  • this is just wrong, Chase's code does take the width into account! – DominicM Feb 19 '15 at 02:07
17

A workaround for Android 4.x:

I found AutoResizeTextView and it works great on my Android 2.1 emulator. I loved it so much. But unfortunately it failed on my own 4.0.4 cellphone and 4.1 emulator. After trying I found it could be easily resolved by adding following attributes in AutoResizeTextView class in the xml:

android:ellipsize="none"

android:singleLine="true"

With the 2 lines above, now AutoResizeTextView working perfectly on my 2.1 & 4.1 emulators and my own 4.0.4 cellphone now.

Hope this helps you. :-)

Community
  • 1
  • 1
agudoe2
  • 171
  • 1
  • 4
  • 2
    I certainly recommend more the solution proposed by @onoelle. It works perfectly in all cases. E.g. my case was not a single line text view. – Boris Strandjev Nov 16 '12 at 10:51
16

Warning, bug in Android Honeycomb and Ice Cream Sandwich

Androids versions: 3.1 - 4.04 have a bug, that setTextSize() inside of TextView works only for the 1st time (1st invocation).

Bug is described here: http://code.google.com/p/android/issues/detail?id=22493 http://code.google.com/p/android/issues/detail?id=17343#c9

workaround is to add new line character to text assigned to TextView before changing size:

final String DOUBLE_BYTE_SPACE = "\u3000";
textView.append(DOUBLE_BYTE_SPACE);

I use it in my code as follow:

final String DOUBLE_BYTE_SPACE = "\u3000";
AutoResizeTextView textView = (AutoResizeTextView) view.findViewById(R.id.aTextView);
String fixString = "";
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1
   && android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {  
    fixString = DOUBLE_BYTE_SPACE;
}
textView.setText(fixString + "The text" + fixString);

I add this "\u3000" character on left and right of my text, to keep it centered. If you have it aligned to left then append to the right only. Of course it can be also embedded with AutoResizeTextView widget, but I wanted to keep fix code outside.

Malachiasz
  • 6,621
  • 2
  • 31
  • 46
15

At google IO conference in 2017, google introduced autoSize property of TextView

https://youtu.be/fjUdJ2aVqE4

<android.support.v7.widget.AppCompatTextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/my_text"
        app:autoSizeTextType="uniform"
        app:autoSizeMaxTextSize="10sp"
        app:autoSizeMinTextSize="6sp"
        app:autoSizeStepGranularity="1sp"/>
Milad Ahmadi
  • 571
  • 7
  • 15
  • android.support.v7.widget.AppCompatTextView is important. It doesn't work like app:autoSizeTextType attributes on standart TextView. Congratulations... – The Goat Nov 01 '17 at 20:30
  • works for me! must add the app:autoSizeTextType="uniform" line – avisper Jul 16 '18 at 12:42
14

My need was to resize text in order to perfectly fit view bounds. Chase's solution only reduces text size, this one enlarges also the text if there is enough space.

To make all fast & precise i used a bisection method instead of an iterative while, as you can see in resizeText() method. That's why you have also a MAX_TEXT_SIZE option. I also included onoelle's tips.

Tested on Android 4.4

/**
 *               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 = 26;

    // Maximum text size for this text view
    public static final float MAX_TEXT_SIZE = 128;

    private static final int BISECTION_LOOP_WATCH_DOG = 30;

    // 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 = MAX_TEXT_SIZE;

    // 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;
        }

        // Get the text view's paint object
        TextPaint textPaint = getPaint();

        // Store the current text size
        float oldTextSize = textPaint.getTextSize();

        // Bisection method: fast & precise
        float lower = mMinTextSize;
        float upper = mMaxTextSize;
        int loop_counter=1;
        float targetTextSize = (lower+upper)/2;
        int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
        while(loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) {
            targetTextSize = (lower+upper)/2;
            textHeight = getTextHeight(text, textPaint, width, targetTextSize);
            if(textHeight > height)
                upper = targetTextSize;
            else
                lower = targetTextSize;
            loop_counter++;
        }

        targetTextSize = lower;
        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 paintCopy = new TextPaint(textPaint);
            paintCopy.setTextSize(targetTextSize);
            StaticLayout layout = new StaticLayout(text, paintCopy, 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 = paintCopy.measureText(mEllipsis);

                    // Trim characters off until we have enough room to draw the ellipsis
                    while(width < lineWidth + ellipseWidth) {
                        lineWidth = paintCopy.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 originalPaint, 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 paint = new TextPaint(originalPaint);
        // Update the text paint object
        paint.setTextSize(textSize);
        // Measure using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
        return layout.getHeight();
    }

}
kenorb
  • 118,428
  • 63
  • 588
  • 624
tuxone
  • 386
  • 3
  • 8
11

Since I've been looking for this forever, and I found a solution a while ago which is missing here, I'm gonna write it here, for future reference also.

Note: this code was taken directly from Google Android Lollipop dialer a while back, I don't remember If changes were made at the time. Also, I don't know which license is this under, but I have reason to think it is Apache 2.0.

Class ResizeTextView, the actual View

public class ResizeTextView extends TextView {

private final int mOriginalTextSize;
private final int mMinTextSize;
private final static int sMinSize = 20;
public ResizeTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
    mOriginalTextSize = (int) getTextSize();
    mMinTextSize = (int) sMinSize;
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
    super.onTextChanged(text, start, lengthBefore, lengthAfter);
    ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
}

This ResizeTextView class could extend TextView and all its children as I undestand, so EditText as well.

Class ViewUtil with method resizeText(...)

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.graphics.Paint;
import android.util.TypedValue;
import android.widget.TextView;

public class ViewUtil {

    private ViewUtil() {}

    public static void resizeText(TextView textView, int originalTextSize, int minTextSize) {
        final Paint paint = textView.getPaint();
        final int width = textView.getWidth();
        if (width == 0) return;
        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
        float ratio = width / paint.measureText(textView.getText().toString());
        if (ratio <= 1.0f) {
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                    Math.max(minTextSize, originalTextSize * ratio));
        }
    }
}

You should set your view as

<yourpackage.yourapp.ResizeTextView
            android:layout_width="match_parent"
            android:layout_height="64dp"
            android:gravity="center"
            android:maxLines="1"/>

Hope it helps!

Matteo
  • 1,342
  • 2
  • 16
  • 33
8

Here's a simple solution that uses TextView itself with a TextChangedListened added to it:

expressionView = (TextView) findViewById(R.id.expressionView);
expressionView.addTextChangedListener(textAutoResizeWatcher(expressionView, 25, 55));

private TextWatcher textAutoResizeWatcher(final TextView view, final int MIN_SP, final int MAX_SP) {
    return new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

        @Override
        public void afterTextChanged(Editable editable) {

            final int widthLimitPixels = view.getWidth() - view.getPaddingRight() - view.getPaddingLeft();
            Paint paint = new Paint();
            float fontSizeSP = pixelsToSp(view.getTextSize());
            paint.setTextSize(spToPixels(fontSizeSP));

            String viewText = view.getText().toString();

            float widthPixels = paint.measureText(viewText);

            // Increase font size if necessary.
            if (widthPixels < widthLimitPixels){
                while (widthPixels < widthLimitPixels && fontSizeSP <= MAX_SP){
                    ++fontSizeSP;
                    paint.setTextSize(spToPixels(fontSizeSP));
                    widthPixels = paint.measureText(viewText);
                }
                --fontSizeSP;
            }
            // Decrease font size if necessary.
            else {
                while (widthPixels > widthLimitPixels || fontSizeSP > MAX_SP) {
                    if (fontSizeSP < MIN_SP) {
                        fontSizeSP = MIN_SP;
                        break;
                    }
                    --fontSizeSP;
                    paint.setTextSize(spToPixels(fontSizeSP));
                    widthPixels = paint.measureText(viewText);
                }
            }

            view.setTextSize(fontSizeSP);
        }
    };
}

private float pixelsToSp(float px) {
    float scaledDensity = getResources().getDisplayMetrics().scaledDensity;
    return px/scaledDensity;
}

private float spToPixels(float sp) {
    float scaledDensity = getResources().getDisplayMetrics().scaledDensity;
    return sp * scaledDensity;
}

This approach will increase or decrease the font size as needed to fit the text, respecting the MIN_SP and MAX_SP bounds received as parameters.

ilharco
  • 91
  • 1
  • 2
  • for me, this is best and nice answer. I was using custom font, so I have to just add paint.setTypeface(typeFace); works like magic..thanks – Abhijit Nov 05 '16 at 17:57
8

I hope this helps you

import android.content.Context;
import android.graphics.Rect;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.widget.TextView;

/* Based on 
 * from http://stackoverflow.com/questions/2617266/how-to-adjust-text-font-size-to-fit-textview
 */
public class FontFitTextView extends TextView {

private static float MAX_TEXT_SIZE = 20;

public FontFitTextView(Context context) {
    this(context, null);
}

public FontFitTextView(Context context, AttributeSet attrs) {
    super(context, attrs);

    float size = this.getTextSize();
    if (size > MAX_TEXT_SIZE)
        setTextSize(MAX_TEXT_SIZE);
}

private void refitText(String text, int textWidth) {
    if (textWidth > 0) {
        float availableWidth = textWidth - this.getPaddingLeft()
                - this.getPaddingRight();

        TextPaint tp = getPaint();
        Rect rect = new Rect();
        tp.getTextBounds(text, 0, text.length(), rect);
        float size = rect.width();

        if (size > availableWidth)
            setTextScaleX(availableWidth / size);
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
    int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
    refitText(this.getText().toString(), parentWidth);
    this.setMeasuredDimension(parentWidth, parentHeight);
}

@Override
protected void onTextChanged(final CharSequence text, final int start,
        final int before, final int after) {
    refitText(text.toString(), this.getWidth());
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    if (w != oldw) {
        refitText(this.getText().toString(), w);
    }
}
}

NOTE: I use MAX_TEXT_SIZE in case of text size is bigger than 20 because I don't want to allow big fonts applies to my View, if this is not your case, you can just simply remove it.

vsm
  • 3,335
  • 2
  • 23
  • 35
  • 2
    Hmm- the use of "setTextScaleX" just seems to cause the text to get crunched horizontally, rather than resize the text to a smaller (yet readable) format. Also, it seems text-wrap unfriendly. – Nathan Fig Mar 15 '11 at 20:17
  • Interesting use of the side effect of setTextScaleX. I wonder if that is significantly different from changing the text size. It's possible that the code is the same, but since it's native, I can't see the source. – SMBiggs Jul 24 '18 at 21:47
  • And another comment, why test for `scale > availableWidth`? This will make it resize ONLY if the available size is smaller than it was originally; stretching (or making text bigger) won't happen. – SMBiggs Jul 24 '18 at 22:47
7

I wrote a blog post about this.

I created a component called ResizableButton based on Kirill Grouchnikov's blog post about custom components used in the new android market app. I placed the src code here.

On the other hand, mosabua read my post and told me he was going to open source his implementation which was faster than mine. I hope he release it soon enough :)

Macarse
  • 87,001
  • 42
  • 169
  • 229
  • This works for a single line of text- but doesn't work for wrapping text. Unless there's something I missed? measure() seems to measure width on the assumption that all text will be on the same line. – Nathan Fig Mar 15 '11 at 19:25
  • I noticed your implementation does not center the text by default. It floats left. Thoughts? – Donn Felker May 31 '11 at 21:33
  • noticed i had the same issue with the text not being centered, i added the line android:singleLine="true" and it centered after that – clayton33 Jun 07 '12 at 10:03
  • You could post this piece of code on GitHub, it is quite useful. I really hate Android's layout system for not letting you auto-resize fonts in elements. – Lachezar Oct 21 '15 at 13:24
5

My implementation is a bit more complex, but comes with the following goodies:

  • takes the available width and available height into account
  • works with single line and multiline labels
  • uses ellipsis in case the minimum font size is hit
  • since the internal text representation is changed, remembers the originally set text in a separate variable
  • ensures that the canvas is always only as big as it needs to be, while it uses all the available height of the parent
/**
 * 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.
 * 
 * Based on the original work from Chase Colburn
 * &lt;http://stackoverflow.com/a/5535672/305532>
 *
 * @author Thomas Keller &lt;me@thomaskeller.biz>
 */
public class AutoResizeTextView extends TextView {

    // in dip
    private static final int MIN_TEXT_SIZE = 20;

    private static final boolean SHRINK_TEXT_SIZE = true;

    private static final char ELLIPSIS = '\u2026';

    private static final float LINE_SPACING_MULTIPLIER_MULTILINE = 0.8f;

    private static final float LINE_SPACING_MULTIPLIER_SINGLELINE = 1f;

    private static final float LINE_SPACING_EXTRA = 0.0f;

    private CharSequence mOriginalText;

    // temporary upper bounds on the starting text size
    private float mMaxTextSize;

    // lower bounds for text size
    private float mMinTextSize;

    // determines whether we're currently in the process of measuring ourselves,
    // so we do not enter onMeasure recursively
    private boolean mInMeasure = false;

    // if the text size should be shrinked or if the text size should be kept
    // constant and only characters should be removed to hit the boundaries
    private boolean mShrinkTextSize;

    public AutoResizeTextView(Context context) {
        this(context, null);
        init(context, null);
    }

    public AutoResizeTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        init(context, attrs);
    }

    public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        // the current text size is used as maximum text size we can apply to
        // our widget
        mMaxTextSize = getTextSize();
        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoResizeTextView);
            mMinTextSize = a.getFloat(R.styleable.AutoResizeTextView_minFontSize, MIN_TEXT_SIZE);
            mShrinkTextSize = a.getBoolean(R.styleable.AutoResizeTextView_shrinkTextSize, SHRINK_TEXT_SIZE);
            a.recycle();
        }
    }

    @Override
    public void setTextSize(float size) {
        mMaxTextSize = size;
        super.setTextSize(size);
    }

    /**
     * Returns the original, unmodified text of this widget
     * 
     * @return
     */
    public CharSequence getOriginalText() {
        // text has not been resized yet
        if (mOriginalText == null) {
            return getText();
        }
        return mOriginalText;
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        if (!mInMeasure) {
            mOriginalText = text.toString();
        }
        super.setText(text, type);
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mInMeasure = true;
        try {
            int availableWidth = MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft()
                    - getCompoundPaddingRight();
            int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - getCompoundPaddingTop()
                    - getCompoundPaddingBottom();

            // Do not resize if the view does not have dimensions or there is no
            // text
            if (mOriginalText == null || mOriginalText.length() == 0 || availableWidth <= 0) {
                return;
            }

            TextPaint textPaint = getPaint();

            // start with the recorded max text size
            float targetTextSize = mMaxTextSize;
            String originalText = mOriginalText.toString();
            String finalText = originalText;

            Rect textSize = getTextSize(originalText, textPaint, targetTextSize);
            boolean textExceedsBounds = textSize.height() > availableHeight || textSize.width() > availableWidth;
            if (mShrinkTextSize && textExceedsBounds) {
                // check whether all lines can be rendered in the available
                // width / height without violating the bounds of the parent and
                // without using a text size that is smaller than the minimum
                // text size
                float heightMultiplier = availableHeight / (float) textSize.height();
                float widthMultiplier = availableWidth / (float) textSize.width();
                float multiplier = Math.min(heightMultiplier, widthMultiplier);
                targetTextSize = Math.max(targetTextSize * multiplier, mMinTextSize);

                // measure again
                textSize = getTextSize(finalText, textPaint, targetTextSize);
            }

            // we cannot shrink the height further when we hit the available
            // height, but we can shrink the width by applying an ellipsis on
            // each line
            if (textSize.width() > availableWidth) {
                StringBuilder modifiedText = new StringBuilder();
                String lines[] = originalText.split(System.getProperty("line.separator"));
                for (int i = 0; i < lines.length; i++) {
                    modifiedText.append(resizeLine(textPaint, lines[i], availableWidth));
                    // add the separator back to all but the last processed line
                    if (i != lines.length - 1) {
                        modifiedText.append(System.getProperty("line.separator"));
                    }
                }
                finalText = modifiedText.toString();

                // measure again
                textSize = getTextSize(finalText, textPaint, targetTextSize);
            }

            textPaint.setTextSize(targetTextSize);
            boolean isMultiline = finalText.indexOf('\n') > -1;
            // do not include extra font padding (for accents, ...) for
            // multiline texts, this will prevent proper placement with
            // Gravity.CENTER_VERTICAL
            if (isMultiline) {
                setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_MULTILINE);
                setIncludeFontPadding(false);
            } else {
                setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_SINGLELINE);
                setIncludeFontPadding(true);
            }

            // according to
            // <http://code.google.com/p/android/issues/detail?id=22493>
            // we have to add a unicode character to trigger the text centering
            // in ICS. this particular character is known as "zero-width" and
            // does no harm.
            setText(finalText + "\u200B");

            int measuredWidth = textSize.width() + getCompoundPaddingLeft() + getCompoundPaddingRight();
            int measuredHeight = textSize.height() + getCompoundPaddingTop() + getCompoundPaddingBottom();

            // expand the view to the parent's height in case it is smaller or
            // to the minimum height that has been set
            // FIXME: honor the vertical measure mode (EXACTLY vs AT_MOST) here
            // somehow
            measuredHeight = Math.max(measuredHeight, MeasureSpec.getSize(heightMeasureSpec));
            setMeasuredDimension(measuredWidth, measuredHeight);
        } finally {
            mInMeasure = false;
        }
    }

    private Rect getTextSize(String text, TextPaint textPaint, float textSize) {
        textPaint.setTextSize(textSize);
        // StaticLayout depends on a given width in which it should lay out the
        // text (and optionally also split into separate lines).
        // Therefor we calculate the current text width manually and start with
        // a fake (read: maxmimum) width for the height calculation.
        // We do _not_ use layout.getLineWidth() here since this returns
        // slightly smaller numbers and therefor would lead to exceeded text box
        // drawing.
        StaticLayout layout = new StaticLayout(text, textPaint, Integer.MAX_VALUE, Alignment.ALIGN_NORMAL, 1f, 0f, true);
        int textWidth = 0;
        String lines[] = text.split(System.getProperty("line.separator"));
        for (int i = 0; i < lines.length; ++i) {
            textWidth = Math.max(textWidth, measureTextWidth(textPaint, lines[i]));
        }
        return new Rect(0, 0, textWidth, layout.getHeight());
    }

    private String resizeLine(TextPaint textPaint, String line, int availableWidth) {
        checkArgument(line != null && line.length() > 0, "expected non-empty string");
        int textWidth = measureTextWidth(textPaint, line);
        int lastDeletePos = -1;
        StringBuilder builder = new StringBuilder(line);
        while (textWidth > availableWidth && builder.length() > 0) {
            lastDeletePos = builder.length() / 2;
            builder.deleteCharAt(builder.length() / 2);
            // don't forget to measure the ellipsis character as well; it
            // doesn't matter where it is located in the line, it just has to be
            // there, since there are no (known) ligatures that use this glyph
            String textToMeasure = builder.toString() + ELLIPSIS;
            textWidth = measureTextWidth(textPaint, textToMeasure);
        }
        if (lastDeletePos > -1) {
            builder.insert(lastDeletePos, ELLIPSIS);
        }
        return builder.toString();
    }

    // there are several methods in Android to determine the text width, namely
    // getBounds() and measureText().
    // The latter works for us the best as it gives us the best / nearest
    // results without that our text canvas needs to wrap its text later on
    // again.
    private int measureTextWidth(TextPaint textPaint, String line) {
        return Math.round(textPaint.measureText(line));
    }
}

[revised on 2012-11-21]

  • fixed the placement of the ellipsis (off-by-one error)
  • reworked text size calculation; now always the full text including line breaks is measured, to fix problems when the addition of the height of two single measured lines just didn't lead to the same result as the measurement of the height of the text as a whole
  • instead of looping to find the smallest available text size, just calculate it after the first measurement
Idolon
  • 26,581
  • 12
  • 93
  • 121
Thomas Keller
  • 5,260
  • 3
  • 42
  • 72
5

I found the following to work nicely for me. It doesn't loop and accounts for both height and width. Note that it is important to specify the PX unit when calling setTextSize on the view.

Paint paint = adjustTextSize(getPaint(), numChars, maxWidth, maxHeight);
setTextSize(TypedValue.COMPLEX_UNIT_PX,paint.getTextSize());

Here is the routine I use, passing in the getPaint() from the view. A 10 character string with a 'wide' character is used to estimate the width independent from the actual string.

private static final String text10="OOOOOOOOOO";
public static Paint adjustTextSize(Paint paint, int numCharacters, int widthPixels, int heightPixels) {
    float width = paint.measureText(text10)*numCharacters/text10.length();
    float newSize = (int)((widthPixels/width)*paint.getTextSize());
    paint.setTextSize(newSize);

    // remeasure with font size near our desired result
    width = paint.measureText(text10)*numCharacters/text10.length();
    newSize = (int)((widthPixels/width)*paint.getTextSize());
    paint.setTextSize(newSize);

    // Check height constraints
    FontMetricsInt metrics = paint.getFontMetricsInt();
    float textHeight = metrics.descent-metrics.ascent;
    if (textHeight > heightPixels) {
        newSize = (int)(newSize * (heightPixels/textHeight));
        paint.setTextSize(newSize);
    }

    return paint;
}
Glenn
  • 1,452
  • 19
  • 28
3

Here's an enumeration of what else I've found for anyone still searching:

1) Here's a solution that recursively re-paints the textview until it fits. This means literally watching your text shrink into place, but at least it fits when it's done. The code will need some tweaking to implement, but it's mostly there.

2) You can try hacking together a custom solution like this, or dunni's class in this, which is what I did using the getPaint().measureText(str) to search for the right size, but it got a lot messier since I need it to wrap only on whitespace...

3) You can keep searching- I've tried more alternatives than I can count. Ted's advice on StaticLayout hasn't paid off for me but maybe there's something there; I tried using the StaticLayout.getEllipsis(line) to determine if text was going off screen, to no effect. See my (presently un-answered) post about that here.

Community
  • 1
  • 1
Nathan Fig
  • 14,103
  • 9
  • 40
  • 56
2

I needed a specific solution. I have got an edittext and textview in my layout. The textview is fixed height and width. When the user starts to type in the edittext, the text should immediately appear in the textview. The text in the textfield should auto - resize to fit the textview. So I updated Chase's solution to work for me. So when the text changes in the textview, resizing starts. The difference between mine and Chase's soluton: resizing is done even if the user DELETE some chars. I hope it can help someone.

public class TextFitTextView extends TextView {

// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 10;

// Maximum text size for this text view - if it is 0, then the text acts
// like match_parent
public static final float MAX_TEXT_SIZE = 0;

// Our ellipse string
private static final String mEllipsis = "...";

// Text size that is set from code. This acts as a starting point for
// resizing
private float mTextSize;

// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;

// Max bounds for text size
private float mMaxTextSize = MAX_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;

// Add ellipsis to text that overflows at the smallest text size
private int heightLimit;
private int widthLimit;

// Default constructor override
public TextFitTextView(Context context) {
    this(context, null);
}

// Default constructor when inflating from XML file
public TextFitTextView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

// Default constructor override
public TextFitTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    mTextSize = getTextSize();
}

/**
 * When text changes resize the text size.
 */
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
    // if we are adding new chars to text
    if (before <= after && after != 1) {
        resizeText(true);
        // now we are deleting chars
    } else {
        resizeText(false);
    }
}

/**
 * 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 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;
}

/**
 * Get width and height limits
 */
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    if (widthLimit == 0 && heightLimit == 0) {
        widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
        heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
    }
    super.onLayout(changed, left, top, right, bottom);
}

/**
 * Resize the text size with specified width and height
 * 
 * @param width
 * @param height
 */
public void resizeText(boolean increase) {
    CharSequence text = getText();
    // Do not resize if the view does not have dimensions or there is no
    // text
    if (text == null || text.length() == 0 || heightLimit <= 0 || widthLimit <= 0 || mTextSize == 0) {
        return;
    }

    // Get the text view's paint object
    TextPaint textPaint = getPaint();

    // Get the required text height
    int textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);


    // If the text length is increased 
    // Until we either fit within our text view or we had reached our min
    // text size, incrementally try smaller sizes
    if (increase) {
        while (textHeight > heightLimit && mTextSize > mMinTextSize) {
            mTextSize = Math.max(mTextSize - 2, mMinTextSize);
            textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
        }
    } 
//      text length has been decreased
    else {
//          if max test size is set then add it to while condition
        if (mMaxTextSize != 0) {
            while (textHeight < heightLimit && mTextSize <= mMaxTextSize) {
                mTextSize = mTextSize + 2;
                textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
            }
        } else {
            while (textHeight < heightLimit) {
                mTextSize = mTextSize + 2;
                textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
            }
        }
        mTextSize = textHeight > heightLimit ? mTextSize - 2 : mTextSize;
    }

    // If we had reached our minimum text size and still don't fit, append
    // an ellipsis
    if (mAddEllipsis && mTextSize == mMinTextSize && textHeight > heightLimit) {
        // Draw using a static layout
        TextPaint paint = new TextPaint(textPaint);
        StaticLayout layout = new StaticLayout(text, paint, widthLimit, 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(heightLimit) - 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 = paint.measureText(mEllipsis);

                // Trim characters off until we have enough room to draw the
                // ellipsis
                while (widthLimit < lineWidth + ellipseWidth) {
                    lineWidth = paint.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, mTextSize);
    setLineSpacing(mSpacingAdd, mSpacingMult);

}

// 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 originalPaint, int width, float textSize) {
    // Update the text paint object
    TextPaint paint = new TextPaint(originalPaint);
    paint.setTextSize(textSize);
    // Measure using a static layout
    StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd,
            true);
    return layout.getHeight();
}

}
granko87
  • 201
  • 2
  • 5
2

Providing this version of top answer rewritten on C# for those who codes on Xamarin.Android. Worked for me well.

 /**
 *               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.
 */


using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Util;
using Android.Widget;
using Java.Lang;

namespace App.GuestGuide.Droid.Controls
{
    public class OnTextResizeEventArgs : EventArgs
    {
        public TextView TextView { get; set; }
        public float OldSize { get; set; }
        public float NewSize { get; set; }
    }

    /// <inheritdoc />
    /// <summary>
    /// 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.
    /// </summary>
    public class AutoResizeTextView : TextView
    {
        /// <summary>
        /// Minimum text size for this text view
        /// </summary>
        public static float MIN_TEXT_SIZE = 10;

        /// <summary>
        /// Our ellipse string
        /// </summary>
        private const string Ellipsis = "...";


        private float _mMaxTextSize;

        private float _mMinTextSize = MIN_TEXT_SIZE;

        /// <summary>
        /// Register subscriber to receive resize notifications
        /// </summary>
        public event EventHandler<OnTextResizeEventArgs> OnTextResize;

        /// <summary>
        /// Flag for text and/or size changes to force a resize
        /// </summary>
        private bool _needsResize;

        /// <summary>
        /// Text size that is set from code. This acts as a starting point for resizing
        /// </summary>
        private float _textSize;

        /// <summary>
        /// Text view line spacing multiplier
        /// </summary>
        private float _spacingMult = 1.0f;

        /// <summary>
        /// Text view additional line spacing
        /// </summary>
        private float _spacingAdd;

        /// <summary>
        /// Add ellipsis to text that overflows at the smallest text size
        /// </summary>
        public bool ShouldAddEllipsis { get; set; }

        /// <inheritdoc />
        /// <summary>
        /// Override the set text size to update our internal reference values
        /// </summary>
        public override float TextSize
        {
            get => base.TextSize;
            set
            {
                base.TextSize = value;
                _textSize = TextSize;
            }
        }

        /// <summary>
        /// Temporary upper bounds on the starting text size
        /// </summary>
        public float MaxTextSize
        {
            get => _mMaxTextSize;
            // Set the upper text size limit and invalidate the view
            set
            {
                _mMaxTextSize = value;
                RequestLayout();
                Invalidate();
            }
        }

        /// <summary>
        /// Lower bounds for text size
        /// </summary>
        public float MinTextSize
        {
            get => _mMinTextSize;
            //Set the lower text size limit and invalidate the view
            set
            {
                _mMinTextSize = value;
                RequestLayout();
                Invalidate();
            }
        }

        public AutoResizeTextView(Context context) : this(context, null)
        {
        }

        public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0)
        {
        }

        public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
        {
            _textSize = TextSize;
        }

        public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
        {
            _textSize = TextSize;
        }

        protected AutoResizeTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
        {
            _textSize = TextSize;
        }

        /// <inheritdoc />
        /// <summary>
        /// When text changes, set the force resize flag to true and reset the text size.
        /// </summary>
        /// <param name="text"></param>
        /// <param name="start"></param>
        /// <param name="lengthBefore"></param>
        /// <param name="lengthAfter"></param>
        protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
        {
            _needsResize = true;
            // Since this view may be reused, it is good to reset the text size
            ResetTextSize();
        }

        /// <inheritdoc />
        /// <summary>
        /// If the text view size changed, set the force resize flag to true
        /// </summary>
        /// <param name="w"></param>
        /// <param name="h"></param>
        /// <param name="oldw"></param>
        /// <param name="oldh"></param>
        protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
        {
            if (w != oldw || h != oldh)
            {
                _needsResize = true;
            }
        }

        public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
        {
            base.SetTextSize(unit, size);
            _textSize = TextSize;
        }

        /// <inheritdoc />
        /// <summary>
        /// Override the set line spacing to update our internal reference values
        /// </summary>
        /// <param name="add"></param>
        /// <param name="mult"></param>
        public override void SetLineSpacing(float add, float mult)
        {
            base.SetLineSpacing(add, mult);
            _spacingMult = mult;
            _spacingAdd = add;
        }

        /// <summary>
        /// Reset the text to the original size
        /// </summary>
        public void ResetTextSize()
        {
            if (_textSize > 0)
            {
                base.SetTextSize(ComplexUnitType.Px, _textSize);
                _mMaxTextSize = _textSize;
            }
        }

        /// <inheritdoc />
        /// <summary>
        /// Resize text after measuring
        /// </summary>
        /// <param name="changed"></param>
        /// <param name="left"></param>
        /// <param name="top"></param>
        /// <param name="right"></param>
        /// <param name="bottom"></param>
        protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
        {
            if (changed || _needsResize)
            {
                var widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
                var heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
                ResizeText(widthLimit, heightLimit);
            }

            base.OnLayout(changed, left, top, right, bottom);
        }

        /// <summary>
        /// Resize the text size with default width and height
        /// </summary>
        public void ResizeText()
        {
            var heightLimit = Height - PaddingBottom - PaddingTop;
            var widthLimit = Width - PaddingLeft - PaddingRight;
            ResizeText(widthLimit, heightLimit);
        }

        /// <summary>
        /// Resize the text size with specified width and height
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public void ResizeText(int width, int height)
        {
            ICharSequence text = null;

            if (!string.IsNullOrEmpty(Text))
            {
                text = new Java.Lang.String(Text);
            }

            // 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 || _textSize == 0)
            {
                return;
            }

            if (TransformationMethod != null)
            {
                text = TransformationMethod.GetTransformationFormatted(text, this);
            }

            // Get the text view's paint object
            var textPaint = Paint;
            // Store the current text size
            var oldTextSize = textPaint.TextSize;
            // If there is a max text size set, use the lesser of that and the default text size
            var targetTextSize = _mMaxTextSize > 0 ? System.Math.Min(_textSize, _mMaxTextSize) : _textSize;

            // Get the required text height
            var 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 = System.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 (ShouldAddEllipsis && targetTextSize == _mMinTextSize && textHeight > height)
            {
                // Draw using a static layout
                // modified: use a copy of TextPaint for measuring
                var paint = new TextPaint(textPaint);
                // Draw using a static layout
                var layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, false);

                // Check that we have a least one line of rendered text
                if (layout.LineCount > 0)
                {
                    // Since the line at the specific vertical position would be cut off,
                    // we must trim up to the previous line
                    var lastLine = layout.GetLineForVertical(height) - 1;
                    // If the text would not even fit on a single line, clear it
                    if (lastLine < 0)
                    {
                        Text = string.Empty;
                    }
                    // Otherwise, trim to the previous line and add an ellipsis
                    else
                    {
                        var start = layout.GetLineStart(lastLine);
                        var end = layout.GetLineEnd(lastLine);
                        var lineWidth = layout.GetLineWidth(lastLine);
                        var ellipseWidth = textPaint.MeasureText(Ellipsis);

                        // Trim characters off until we have enough room to draw the ellipsis
                        while (width < lineWidth + ellipseWidth)
                        {
                            lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1));
                        }

                        Text = text.SubSequence(0, end) + Ellipsis;
                    }
                }
            }

            // Some devices try to auto adjust line spacing, so force default line spacing
            // and invalidate the layout as a side effect
            SetTextSize(ComplexUnitType.Px, targetTextSize);
            SetLineSpacing(_spacingAdd, _spacingMult);

            var notifyArgs = new OnTextResizeEventArgs
            {
                TextView = this,
                NewSize = targetTextSize,
                OldSize = oldTextSize
            };

            // Notify the listener if registered
            OnTextResize?.Invoke(this, notifyArgs);

            // Reset force resize flag
            _needsResize = false;
        }

        /// <summary>
        /// Set the text size of the text paint object and use a static layout to render text off screen before measuring
        /// </summary>
        /// <param name="source"></param>
        /// <param name="paint"></param>
        /// <param name="width"></param>
        /// <param name="textSize"></param>
        /// <returns></returns>
        private int GetTextHeight(ICharSequence 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)
            // Update the text paint object
            var paintCopy = new TextPaint(paint)
            {
                TextSize = textSize
            };

            // Measure using a static layout
            var layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true);

            return layout.Height;
        }
    }
}
Oleg Kosuakiv
  • 134
  • 13
2

My method is:

public void changeTextSize(int initialSize, TextView tv) {

    DisplayMetrics displayMetrics = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    double width = displayMetrics.widthPixels / displayMetrics.xdpi;
    double height = displayMetrics.heightPixels / displayMetrics.ydpi;

    Log.i("LOG", "The width of the tested emulator is: " + width);
    Log.i("LOG", "The height of the tested emulator is: " + height);

    double scale = Math.min(width / 2.25, height / 4.0); //See the logcat >>> width = 2.25 and heigt = 4.0
    tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, (int) (initialSize * scale));

}

For example:

changeTextSize(16, findViewById(R.id.myTextView));
changeTextSize(12, findViewById(R.id.myEditText));
gadolf
  • 735
  • 8
  • 15
1

I just created the following method (based on the ideas of Chase) which might help you if you want to draw text to any canvas:

private static void drawText(Canvas canvas, int xStart, int yStart,
        int xWidth, int yHeigth, String textToDisplay,
        TextPaint paintToUse, float startTextSizeInPixels,
        float stepSizeForTextSizeSteps) {

    // Text view line spacing multiplier
    float mSpacingMult = 1.0f;
    // Text view additional line spacing
    float mSpacingAdd = 0.0f;
    StaticLayout l = null;
    do {
        paintToUse.setTextSize(startTextSizeInPixels);
        startTextSizeInPixels -= stepSizeForTextSizeSteps;
        l = new StaticLayout(textToDisplay, paintToUse, xWidth,
                Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
    } while (l.getHeight() > yHeigth);

    int textCenterX = xStart + (xWidth / 2);
    int textCenterY = (yHeigth - l.getHeight()) / 2;

    canvas.save();
    canvas.translate(textCenterX, textCenterY);
    l.draw(canvas);
    canvas.restore();
}

This could be used e.g. in any onDraw() method of any custom view.

Simon
  • 12,485
  • 14
  • 63
  • 84
1

Here's yet another solution, just for kicks. It's probably not very efficient, but it does cope with both height and width of the text, and with marked-up text.

@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)
{
    if ((MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED)
            && (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED)) {

        final float desiredWidth = MeasureSpec.getSize(widthMeasureSpec);
        final float desiredHeight = MeasureSpec.getSize(heightMeasureSpec);

        float textSize = getTextSize();
        float lastScale = Float.NEGATIVE_INFINITY;
        while (textSize > MINIMUM_AUTO_TEXT_SIZE_PX) {
            // Measure how big the textview would like to be with the current text size.
            super.onMeasure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);

            // Calculate how much we'd need to scale it to fit the desired size, and
            // apply that scaling to the text size as an estimate of what we need.
            final float widthScale = desiredWidth / getMeasuredWidth();
            final float heightScale = desiredHeight / getMeasuredHeight();
            final float scale = Math.min(widthScale, heightScale);

            // If we don't need to shrink the text, or we don't seem to be converging, we're done.
            if ((scale >= 1f) || (scale <= lastScale)) {
                break;
            }

            // Shrink the text size and keep trying.
            textSize = Math.max((float) Math.floor(scale * textSize), MINIMUM_AUTO_TEXT_SIZE_PX);
            setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
            lastScale = scale;
        }
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Squatting Bear
  • 1,484
  • 13
  • 12
  • I've tried 6 other answers, but this one gets me the best results. Still is it not perfect because the text size ends up slightly smaller than it needs to be in some cases, but better than the other answers. – ThomasW Oct 16 '17 at 09:05
1

I have use code from chase and M-WaJeEh and I found some advantage & disadvantage here

from chase

Advantage:

  • it's perfect for 1 line TextView

Disadvantage:

  • if it's more than 1 line with custom font some of text will disappear

  • if it's enable ellipse, it didn't prepare space for ellipse

  • if it's custom font (typeface), it didn't support

from M-WaJeEh

Advantage:

  • it's perfect for multi-line

Disadvantage:

  • if set height as wrap-content, this code will start from minimum size and it will reduce to smallest as it can, not from the setSize and reduce by the limited width

  • if it's custom font (typeface), it didn't support

Sruit A.Suk
  • 6,247
  • 7
  • 55
  • 66
  • 1
    Crashes in `getTextHeight()` when setting text or text size. Android 4.0.4 emulator. `java.lang.IllegalArgumentException: Layout: -40 < 0 at android.text.Layout.(Layout.java:140) at android.text.StaticLayout.(StaticLayout.java:104) at android.text.StaticLayout.(StaticLayout.java:90) at android.text.StaticLayout.(StaticLayout.java:68) at android.text.StaticLayout.(StaticLayout.java:48)` – TalkLittle Jul 04 '14 at 00:45
1

I combined some of the above suggestions to make one that scales up and down, with bisection method. It also scales within the width.

/**
 *               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 = 10;

    // Minimum text size for this text view
    public static final float MAX_TEXT_SIZE = 128;

    private static final int BISECTION_LOOP_WATCH_DOG = 30;

    // 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 = MAX_TEXT_SIZE;

    // 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() {

        // Height and width with a padding as a percentage of height
        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;
        }

        // Get the text view's paint object
        TextPaint textPaint = getPaint();

        // Store the current text size
        float oldTextSize = textPaint.getTextSize();

        // Bisection method: fast & precise
        float lower = mMinTextSize;
        float upper = mMaxTextSize;
        int loop_counter = 1;
        float targetTextSize = (lower + upper) / 2;
        int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
        int textWidth = getTextWidth(text, textPaint, width, targetTextSize);

        while (loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) {
            targetTextSize = (lower + upper) / 2;
            textHeight = getTextHeight(text, textPaint, width, targetTextSize);
            textWidth = getTextWidth(text, textPaint, width, targetTextSize);
            if (textHeight > (height) || textWidth > (width))
                upper = targetTextSize;
            else
                lower = targetTextSize;
            loop_counter++;
        }

        targetTextSize = lower;
        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 paintCopy = new TextPaint(textPaint);
            paintCopy.setTextSize(targetTextSize);
            StaticLayout layout = new StaticLayout(text, paintCopy, 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 = paintCopy.measureText(mEllipsis);

                    // Trim characters off until we have enough room to draw the
                    // ellipsis
                    while (width < lineWidth + ellipseWidth) {
                        lineWidth = paintCopy.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 originalPaint,
            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 paint = new TextPaint(originalPaint);
        // Update the text paint object
        paint.setTextSize(textSize);
        // Measure using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width,
                Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
        return layout.getHeight();
    }

    // Set the text size of the text paint object and use a static layout to
    // render text off screen before measuring
    private int getTextWidth(CharSequence source, TextPaint originalPaint,
            int width, float textSize) {
        // Update the text paint object
        TextPaint paint = new TextPaint(originalPaint);
        // Draw using a static layout
        paint.setTextSize(textSize);

        StaticLayout layout = new StaticLayout(source, paint, width,
                Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);

        return (int) layout.getLineWidth(0);
    }
}
Toxid
  • 607
  • 10
  • 18
1

You can use the android.text.StaticLayout class for this. That's what TextView uses internally.

Ted Hopp
  • 222,293
  • 47
  • 371
  • 489
  • 4
    Could you elaborate? I see the dev doc on StaticLayout, it has some interesting methods, but I'm not sure I see how this could be used to create what I'm asking for? – Nathan Fig Feb 17 '11 at 20:34
0

Thanks to Chase and onoelle, for the lazy programmers, let me post here a working version of their fantastic merged code, adapted on a Button, instead of a TextView.

Substitute all your Buttons (not ImageButtons) with AutoResizeTextButtons and the same boring problem is fixed for them too.

Here is the code. I just removed the imports.

/**
 *            DO WHAT THE FUCK 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 THE FUCK YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 * 
 *  0. You just DO WHAT THE FUCK YOU WANT TO.
 *  made better by onoelle
 *  adapted for button by beppi
 */

/**
 * Text Button 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 AutoResizeTextButton extends Button {

    // 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(Button 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 AutoResizeTextButton(Context context) {
        this(context, null);
    }

    // Default constructor when inflating from XML file
    public AutoResizeTextButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    // Default constructor override
    public AutoResizeTextButton(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;
        }

        // 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);
            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
//      textPaint.setTextSize(targetTextSize);
     // modified: setting text size via this.setTextSize (instead of textPaint.setTextSize(targetTextSize))
        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 originalPaint, 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)
        // Update the text paint object
          TextPaint paint = new TextPaint(originalPaint);
        paint.setTextSize(textSize);
        // Measure using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
        return layout.getHeight();
    }

}

Usage:

put a AutoResizeTextButton inside your xml in replace of a normal Button, without changing anything else. Inside the onCreate() put (for example):

    myButton = (AutoResizeTextButton)getView().findViewById(id.myButton);
    myButton.setMinTextSize(8f);
    myButton.resizeText();
Beppi's
  • 1,937
  • 1
  • 18
  • 36
  • Button background is invisible. You need to include the following in your button view xml: style="?android:attr/buttonBarButtonStyle" – Kristy Welsh Dec 09 '15 at 15:42
0

Here is the approach I take. It's very simple. It uses successive approximation to zero in on the fontsize and can generally have it figured out in less than 10 iterations. Just replace "activityWidth" with the width of whatever view you are using to display the text in. In my example, it's set as a private field to the screen's width. The inital fontsize of 198 is only set in the event the method generates an exception (which really should never happen):

  private float GetFontSizeForScreenWidth(String text)
  {
    float fontsize = 198;

    try
    {
      Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
      paint.setColor(Color.RED);
      Typeface typeface = Typeface.create("Helvetica", Typeface.BOLD);
      paint.setTypeface(typeface);
      paint.setTextAlign(Align.CENTER);

      int lowVal = 0;
      int highVal = 2000;
      int currentVal = highVal;

      /*
       * Successively approximate the screen size until it is 
       * within 2 pixels of the maximum screen width. Generally
       * this will get you to the closest font size within about 10
       * iterations.
       */

      do
      {
        paint.setTextSize(currentVal);
        float textWidth = paint.measureText(text);

        float diff = activityWidth - textWidth;

        if ((diff >= 0) && (diff <= 2))
        {
          fontsize = paint.getTextSize();
          return fontsize;
        }

        if (textWidth > activityWidth)
          highVal = currentVal;
        else if (textWidth < activityWidth)
          lowVal = currentVal;
        else
        {
          fontsize = paint.getTextSize();
          return fontsize;
        }

        currentVal = (highVal - lowVal) / 2 + lowVal;

      } while (true);      
    }
    catch (Exception ex)
    {
      return fontsize;
    }
  }
AndroidDev
  • 20,063
  • 26
  • 131
  • 216
0

Extend TextView and override onDraw with the code below. It will keep text aspect ratio but size it to fill the space. You could easily modify code to stretch if necessary.

  @Override
  protected void onDraw(@NonNull Canvas canvas) {
    TextPaint textPaint = getPaint();
    textPaint.setColor(getCurrentTextColor());
    textPaint.setTextAlign(Paint.Align.CENTER);
    textPaint.drawableState = getDrawableState();

    String text = getText().toString();
    float desiredWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2;
    float desiredHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2;
    float textSize = textPaint.getTextSize();

    for (int i = 0; i < 10; i++) {
      textPaint.getTextBounds(text, 0, text.length(), rect);
      float width = rect.width();
      float height = rect.height();

      float deltaWidth = width - desiredWidth;
      float deltaHeight = height - desiredHeight;

      boolean fitsWidth = deltaWidth <= 0;
      boolean fitsHeight = deltaHeight <= 0;

      if ((fitsWidth && Math.abs(deltaHeight) < 1.0)
          || (fitsHeight && Math.abs(deltaWidth) < 1.0)) {
        // close enough
        break;
      }

      float adjustX = desiredWidth / width;
      float adjustY = desiredHeight / height;

      textSize = textSize * (adjustY < adjustX ? adjustY : adjustX);

      // adjust text size
      textPaint.setTextSize(textSize);
    }
    float x = desiredWidth / 2f;
    float y = desiredHeight / 2f - rect.top - rect.height() / 2f;
    canvas.drawText(text, x, y, textPaint);
  }
Greg Bacchus
  • 1,709
  • 2
  • 19
  • 24
0

Refer to the ScalableTextView.java here Auto-fit TextView for Android. I have added the code to shrink and expand the TextView based on the text length

Community
  • 1
  • 1
Devendra Vaja
  • 3,437
  • 1
  • 16
  • 13
0

If anyone needs it, here is the same code snippet but for Xamarin.Android.

/**
 *               DO WHAT YOU WANT TO PUBLIC LICENSE
 *                    Version 1, December 2017
 * 
 * Copyright (C) 2017 Nathan Westfall
 * 
 * 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.
 */

using Android.Content;
using Android.Runtime;
using Android.Widget;
using Android.Util;
using Android.Text;

using Java.Lang;

namespace My.Text
{
    public class AutoResizeTextView : TextView
    {
        public const float MIN_TEXT_SIZE = 20;

        public interface OnTextResizeListener
        {
            void OnTextResize(TextView textView, float oldSize, float newSize);
        }

        private const string mEllipsis = "...";

        private OnTextResizeListener mTextResizeListener;

        private bool 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;

        public bool AddEllipsis { get; set; } = true;

        public AutoResizeTextView(Context context) : this(context, null) { }

        public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0) { }

        public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyle): base(context, attrs, defStyle)
        {
            mTextSize = TextSize;
        }

        protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
        {
            base.OnTextChanged(text, start, lengthBefore, lengthAfter);

            mNeedsResize = true;

            ResetTextSize();
        }

        protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
        {
            base.OnSizeChanged(w, h, oldw, oldh);

            if (w != oldw || h != oldh)
                mNeedsResize = true;
        }

        public void SetOnResizeListener(OnTextResizeListener listener)
        {
            mTextResizeListener = listener;
        }

        public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
        {
            base.SetTextSize(unit, size);

            mTextSize = TextSize;
        }

        public override void SetLineSpacing(float add, float mult)
        {
            base.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 ResetTextSize()
        {
            if(mTextSize > 0)
            {
                base.SetTextSize(ComplexUnitType.Px, mTextSize);
                mMaxTextSize = mTextSize;
            }
        }

        protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
        {
            if(changed || mNeedsResize)
            {
                int widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
                int heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
                ResizeText(widthLimit, heightLimit);
            }
            base.OnLayout(changed, left, top, right, bottom);

            base.OnLayout(changed, left, top, right, bottom);
        }

        public void ResizeText()
        {
            int heightLimit = Height - PaddingBottom - PaddingTop;
            int widthLimit = Width - PaddingLeft - PaddingRight;
            ResizeText(widthLimit, heightLimit);
        }

        public void ResizeText(int width, int height)
        {
            var text = TextFormatted;
            if (text == null || text.Length() == 0 || height <= 0 || width <= 0 || mTextSize == 0)
                return;

            if (TransformationMethod != null)
                text = TransformationMethod.GetTransformationFormatted(TextFormatted, this);

            TextPaint textPaint = Paint;

            float oldTextSize = textPaint.TextSize;
            float targetTextSize = mMaxTextSize > 0 ? System.Math.Min(mTextSize, mMaxTextSize) : mTextSize;

            int textHeight = GetTextHeight(text, textPaint, width, targetTextSize);

            while(textHeight > height && targetTextSize > mMinTextSize)
            {
                targetTextSize = System.Math.Max(targetTextSize - 2, mMinTextSize);
                textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
            }

            if(AddEllipsis && targetTextSize == mMinTextSize && textHeight > height)
            {
                TextPaint paint = new TextPaint(textPaint);

                StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false);
                if(layout.LineCount > 0)
                {
                    int lastLine = layout.GetLineForVertical(height) - 1;
                    if (lastLine < 0)
                        SetText("", BufferType.Normal);
                    else
                    {
                        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, BufferType.Normal);
                    }
                }
            }

            SetTextSize(ComplexUnitType.Px, targetTextSize);
            SetLineSpacing(mSpacingAdd, mSpacingMult);

            mTextResizeListener?.OnTextResize(this, oldTextSize, targetTextSize);

            mNeedsResize = false;
        }

        private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize)
        {
            TextPaint paintCopy = new TextPaint(paint);
            paintCopy.TextSize = textSize;
            StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false);
            return layout.Height;
        }
    }
}
nwestfall
  • 209
  • 2
  • 4
  • 12
0

The problem is about how to have this feature on Button; For TextView it's easy and works very well by following the official document here.

Style.xml:

    <style name="Widget.Button.CustomStyle" parent="Widget.MaterialComponents.Button">
        <item name="android:minHeight">50dp</item>
        <item name="android:maxWidth">300dp</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textSize">16sp</item>
        <item name="backgroundTint">@color/white</item>
        <item name="cornerRadius">25dp</item>
        <item name="autoSizeTextType">uniform</item>
        <item name="autoSizeMinTextSize">10sp</item>
        <item name="autoSizeMaxTextSize">16sp</item>
        <item name="autoSizeStepGranularity">2sp</item>
        <item name="android:maxLines">1</item>
        <item name="android:textColor">@color/colorPrimary</item>
        <item name="android:insetTop">0dp</item>
        <item name="android:insetBottom">0dp</item>
        <item name="android:lineSpacingExtra">4sp</item>
        <item name="android:gravity">center</item>
    </style>

Usage:

<com.google.android.material.button.MaterialButton
            android:id="@+id/blah"
            style="@style/Widget.Button.CustomStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:text="Your long text, to the infinity and beyond!!! Why not :)" />

Result:
result

Dr.jacky
  • 2,499
  • 6
  • 41
  • 76
0

Text to Fit Bounds (1 line)

To have the text shrink to fit bounds for one line:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:autoSizeTextType="uniform"
    android:lines:"1"
/>
SwiftiSwift
  • 4,046
  • 5
  • 27
  • 57
0

This solutions works for us:

public class CustomFontButtonTextFit extends CustomFontButton
{
    private final float DECREMENT_FACTOR = .1f;

    public CustomFontButtonTextFit(Context context) {
        super(context);
    }

    public CustomFontButtonTextFit(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomFontButtonTextFit(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    private synchronized void refitText(String text, int textWidth) {
        if (textWidth > 0) 
        {
            float availableWidth = textWidth - this.getPaddingLeft()
                    - this.getPaddingRight();

            TextPaint tp = getPaint();
            Rect rect = new Rect();
            tp.getTextBounds(text, 0, text.length(), rect);
            float size = rect.width();

            while(size > availableWidth)
            {
                setTextSize( getTextSize() - DECREMENT_FACTOR );
                tp = getPaint();

                tp.getTextBounds(text, 0, text.length(), rect);
                size = rect.width();
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);

        refitText(this.getText().toString(), parentWidth);

        if(parentWidth < getSuggestedMinimumWidth())
            parentWidth = getSuggestedMinimumWidth();

        if(parentHeight < getSuggestedMinimumHeight())
            parentHeight = getSuggestedMinimumHeight();

        this.setMeasuredDimension(parentWidth, parentHeight);
    }

    @Override
    protected void onTextChanged(final CharSequence text, final int start,
            final int before, final int after) 
    {
        super.onTextChanged(text, start, before, after);

        refitText(text.toString(), this.getWidth());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh)
    {
        super.onSizeChanged(w, h, oldw, oldh);

        if (w != oldw) 
            refitText(this.getText().toString(), w);
    }
}
Springfeed
  • 145
  • 1
  • 2
-3

I just borrow some other guys' idea and write some code below that may be helpful.

import android.content.Context;
import android.graphics.Canvas;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;

public class AutoResizeTextView extends TextView {
    private static final int MAX_SIZE = 1000;

    private static final int MIN_SIZE = 5;

    private TextPaint mTextPaint;

    private float mSpacingMult = 1.0f;

    private float mSpacingAdd = 0.0f;

    private boolean needAdapt = false;

    private boolean adapting = false;

    public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public AutoResizeTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public AutoResizeTextView(Context context) {
        super(context);
        init();
    }

    private void init() {
        mTextPaint = new TextPaint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (adapting) {
            return;
        }
        if (needAdapt) {
            adaptTextSize();
        } else {
            super.onDraw(canvas);
        }
    }

    private void adaptTextSize() {
        CharSequence text = getText();
        int viewWidth = getMeasuredWidth();
        int viewHeight = getMeasuredHeight();

        if (viewWidth==0 || viewHeight==0
                || TextUtils.isEmpty(text)) {
            return;
        }

        adapting = true;
        /* binary search */
        int bottom=MIN_SIZE, top=MAX_SIZE, mid = 0;
        while (bottom <= top) {
            mid = (bottom + top)/2;
            mTextPaint.setTextSize(mid);
            int textWidth = (int) mTextPaint.measureText(text, 0, text.length());
            int textHeight = getTextHeight(text, viewWidth);
            if (textWidth<viewWidth && textHeight<viewHeight) {
                bottom = mid+1;
            } else {
                top = mid-1;
            }
        }

        int newSize = mid-1;
        setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);

        adapting=false;
        needAdapt = false;

        invalidate();
    }

    private int getTextHeight(CharSequence text, int targetWidth) {
        StaticLayout layout = new StaticLayout(text, mTextPaint, targetWidth,
                Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
        return layout.getHeight();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        needAdapt = true;
    }

    @Override
    protected void onTextChanged(CharSequence text, int start,
            int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        needAdapt = true;
    }

    @Override
    public void setLineSpacing(float add, float mult) {
        super.setLineSpacing(add, mult);
        mSpacingMult = mult;
        mSpacingAdd = add;
    }
}