232

Background

Many times we need to auto-fit the font of the TextView to the boundaries given to it.

The problem

Sadly, even though there are many threads and posts (and suggested solutions) talking about this problem (example here, here and here), none of them actually work well.

That's why, I've decided to test each of them till I find the real deal.

I think that the requirements from such a textView should be:

  1. Should allow using any font, typeface, style, and set of characters.

  2. Should handle both width and height

  3. No truncation unless text cannot fit because of the limitation, we've given to it (example: too long text, too small available size). However, we could request for horizontal/vertical scrollbar if we wish, just for those cases.

  4. Should allow multi-line or single-line. In case of multi-line, allow max & min lines.

  5. Should not be slow in computation. Using a loop for finding the best size? At least optimize it and don't increment your sampling by 1 each time.

  6. In case of multi-line, should allow to prefer resizing or using more lines, and/or allow to choose the lines ourselves by using the "\n" character.

What I've tried

I've tried so many samples (including those of the links, I've written about), and I've also tried to modify them to handle the cases, I've talked about, but none really work.

I've made a sample project that allows me to visually see if the TextView auto-fits correctly.

Currently, my sample project only randomize the text (the English alphabet plus digits) and the size of the textView, and let it stay with single line, but even this doesn't work well on any of the samples I've tried.

Here's the code (also available here):

File res/layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  android:layout_height="match_parent" tools:context=".MainActivity">
  <Button android:id="@+id/button1" android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true" android:text="Button" />
  <FrameLayout android:layout_width="match_parent"
    android:layout_height="wrap_content" android:layout_above="@+id/button1"
    android:layout_alignParentLeft="true" android:background="#ffff0000"
    android:layout_alignParentRight="true" android:id="@+id/container"
    android:layout_alignParentTop="true" />

</RelativeLayout>

File src/.../MainActivity.java

public class MainActivity extends Activity
  {
  private final Random        _random            =new Random();
  private static final String ALLOWED_CHARACTERS ="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890";

  @Override
  protected void onCreate(final Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ViewGroup container=(ViewGroup)findViewById(R.id.container);
    findViewById(R.id.button1).setOnClickListener(new OnClickListener()
      {
        @Override
        public void onClick(final View v)
          {
          container.removeAllViews();
          final int maxWidth=container.getWidth();
          final int maxHeight=container.getHeight();
          final FontFitTextView fontFitTextView=new FontFitTextView(MainActivity.this);
          final int width=_random.nextInt(maxWidth)+1;
          final int height=_random.nextInt(maxHeight)+1;
          fontFitTextView.setLayoutParams(new LayoutParams(width,height));
          fontFitTextView.setSingleLine();
          fontFitTextView.setBackgroundColor(0xff00ff00);
          final String text=getRandomText();
          fontFitTextView.setText(text);
          container.addView(fontFitTextView);
          Log.d("DEBUG","width:"+width+" height:"+height+" text:"+text);
          }
      });
    }

  private String getRandomText()
    {
    final int textLength=_random.nextInt(20)+1;
    final StringBuilder builder=new StringBuilder();
    for(int i=0;i<textLength;++i)
      builder.append(ALLOWED_CHARACTERS.charAt(_random.nextInt(ALLOWED_CHARACTERS.length())));
    return builder.toString();
    }
  }

The question

Does anybody know of a solution for this common problem that actually work?

Even a solution that has much less features that what I've written about, for example one that has just a constant number of lines of text, and adjusts its font according to its size, yet never have weird glitches and having the text get too large/small compared to its available space.


GitHub project

Since this is such an important TextView, I've decided to publish a library, so that everyone could easily use it, and contribute to it, here.

android developer
  • 106,412
  • 122
  • 641
  • 1,128
  • Have you tried this one? http://www.androidviews.net/2012/12/autoscale-textview/ – Thrakbad Apr 15 '13 at 14:08
  • @Thrakbad it's one of the links i've mentioned. It also doesn't pass the test. – android developer Apr 15 '13 at 14:11
  • Ah sorry, I missed the last example somehow – Thrakbad Apr 15 '13 at 14:19
  • Yes, please believe me, I've tried a lot of samples and I've also tried to modified them to fix the issues I've found, but never succeeded. If you find something that you think might work, please test it out. I've posted a sample code just for this. – android developer Apr 15 '13 at 14:35
  • what is the problem with the `autoscale-textview`? maybe the easiest way to solve you problem is to fix an existing solution, and I think that `autoscale-textview` is the best approach. – Diogo Bento Apr 15 '13 at 14:37
  • @DiogoBento Have you tried it? Please believe me, I've tried it and many other samples. Here's a sample of what I get from the autoscale-textview as shown on the android-views website, when tested using my test code (of course, the code required me to set the max textsize , so i did it too) : http://tinypic.com/view.php?pic=24q8wol&s=6 what are the parameters? 04-15 width:505 height:446 text:"l4fY77" – android developer Apr 15 '13 at 15:44
  • You can find good solution [here][1] [1]: http://www.stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds – rule Jul 25 '13 at 14:38
  • 1
    @rule this is one of the posts i've already read, and i've tested all of the code samples . also i think you've double-posted. – android developer Jul 25 '13 at 18:13
  • any ideas why this won't work if Auto-fit is extends EditText? – ViksaaSkool Nov 15 '14 at 14:35
  • @ViksaaSkool Well I didn't try it, but what exactly happens when you do it? – android developer Nov 15 '14 at 15:17
  • here is the logcat http://pastebin.com/4VsZexHL I'm just replacing TextView with EditText, in theory it should be sufficient change – ViksaaSkool Nov 15 '14 at 15:29
  • @ViksaaSkool The log shows as if something is wrong with the layout XML file. Sure everything looks ok there? – android developer Nov 15 '14 at 15:47
  • I've seen what I did wrong, now it inflates but it doesn't behave as EditText (it behaves as textView). android:editable="true" doesn't help either – ViksaaSkool Nov 15 '14 at 16:22
  • @ViksaaSkool Sorry for not helping you. You can make a new issue there and maybe I will try to do the same, but since most of the code is not really mine, it will be quite hard for me to solve your problem :( – android developer Nov 15 '14 at 22:01
  • I think I got it working! I'll write a blog post about this in the following hours/days. – ViksaaSkool Nov 16 '14 at 00:50
  • @ViksaaSkool This is great. Can you please post a link here? Would you also like me to add your solution to the Github project? – android developer Nov 16 '14 at 06:24
  • here it is> http://bit.ly/11gNOiE If you think it's worth to be referenced on the github of the lib, please do so – ViksaaSkool Nov 16 '14 at 22:53
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/65048/discussion-between-viksaaskool-and-android-developer). – ViksaaSkool Nov 16 '14 at 23:06
  • @ViksaaSkool You've given me a lot of credit :) . I've only tested the other libraries against a test I've conducted, and also found a few fixes to the best solution I've found, plus I've added things other people suggested here (after testing). I still found some issues and wrote about them on the Github project. I see that you've spend a lot of time in this. Would you mind checking the issues? Also, thank you for sharing your research. – android developer Nov 17 '14 at 06:34
  • Hey, really nice library here. But I'm experiencing a problem using it when willing to zoom the view before writing text in it : seems like the area where the text can go moves its left bound but not its right (at least when typing), ending up with letters invisible. When zooming out after editing, letters appear. Any idea what's causing it? – NSimon Mar 30 '15 at 16:11
  • @NicolasSimon The text will auto-resize its fonts size depending on the text content and view-dimensions . I don't know what happens when you zoom. You'd have to explain what is your definition of zooming of a TextView. – android developer Mar 30 '15 at 18:36
  • @androiddeveloper Here is my question with as much doc as I though would be relevant, don't hesitate to come and stop by! http://stackoverflow.com/questions/29370726/variant-from-android-autofittextview-library-scaling-makes-strange-behaviour – NSimon Mar 31 '15 at 14:15
  • Have you tried the https://github.com/grantland/android-autofittextview library? It's available on MavenCentral. – 13rac1 Jul 14 '15 at 06:57
  • @eosrei Pretty sure I have, and that it had issues. – android developer Jul 14 '15 at 13:33
  • There are open issues on github, did you have any of those problems? It's working perfectly for my use. 1k+ github stars can't be all wrong. :) – 13rac1 Jul 14 '15 at 21:54
  • @eosrei Really wish this was built in on Android, but having third party solutions is also good, as people can fix things and set them – android developer Jul 15 '15 at 07:22
  • Is it possible to adjust this to account for whitespace? For instance, these two strings would be sized differently by this code: `12 45 89` and `123456789`. They both have 10 characters, but I don't think this code is taking whitespace into account. Is it possible to have these two strings set to the same font size? – pez Jan 22 '16 at 04:34
  • This works great, the only thing I am missing is the ellipse at the end if the text is truncated. Is that meant to be showing? – tagy22 Jun 07 '16 at 10:17
  • @tagy22 You mean in case the text got too small? – android developer Jun 07 '16 at 10:25
  • @androiddeveloper Yes, when there is too much text to show it all, I have set android:ellipsize="end" – tagy22 Jun 07 '16 at 10:30
  • @tagy22 I don't remember if it can work. Maybe I didn't try. – android developer Jun 07 '16 at 10:32
  • `android:maxLines="1"` is somehow in conflict with `android:gravity="center_vertical"` (center_vertical is not applied). – matoni Dec 22 '17 at 22:06

16 Answers16

148

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


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

I have modified your test case:

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

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

Final effect:

Enter image description here

Sample Layout file:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp" >

<com.vj.widgets.AutoResizeTextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:ellipsize="none"
    android:maxLines="2"
    android:text="Auto Resized Text, max 2 lines"
    android:textSize="100sp" /> <!-- maximum size -->

<com.vj.widgets.AutoResizeTextView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:ellipsize="none"
    android:gravity="center"
    android:maxLines="1"
    android:text="Auto Resized Text, max 1 line"
    android:textSize="100sp" /> <!-- maximum size -->

<com.vj.widgets.AutoResizeTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Auto Resized Text"
    android:textSize="500sp" /> <!-- maximum size -->

</LinearLayout>

And the Java code:

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.util.TypedValue;
import android.widget.TextView;

public class AutoResizeTextView extends TextView {
    private interface SizeTester {
        /**
         *
         * @param suggestedSize
         *            Size of text to be tested
         * @param availableSpace
         *            available space in which text must fit
         * @return an integer < 0 if after applying {@code suggestedSize} to
         *         text, it takes less space than {@code availableSpace}, > 0
         *         otherwise
         */
        public int onTestSize(int suggestedSize, RectF availableSpace);
    }

    private RectF mTextRect = new RectF();

    private RectF mAvailableSpaceRect;

    private SparseIntArray mTextCachedSizes;

    private TextPaint mPaint;

    private float mMaxTextSize;

    private float mSpacingMult = 1.0f;

    private float mSpacingAdd = 0.0f;

    private float mMinTextSize = 20;

    private int mWidthLimit;

    private static final int NO_LINE_LIMIT = -1;
    private int mMaxLines;

    private boolean mEnableSizeCache = true;
    private boolean mInitializedDimens;

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

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

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

    private void initialize() {
        mPaint = new TextPaint(getPaint());
        mMaxTextSize = getTextSize();
        mAvailableSpaceRect = new RectF();
        mTextCachedSizes = new SparseIntArray();
        if (mMaxLines == 0) {
            // no value was assigned during construction
            mMaxLines = NO_LINE_LIMIT;
        }
    }

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

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

    public int getMaxLines() {
        return mMaxLines;
    }

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

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

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

    @Override
    public void setTextSize(int unit, float size) {
        Context c = getContext();
        Resources r;

        if (c == null)
            r = Resources.getSystem();
        else
            r = c.getResources();
        mMaxTextSize = TypedValue.applyDimension(unit, size,
                r.getDisplayMetrics());
        mTextCachedSizes.clear();
        adjustTextSize();
    }

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

    /**
     * Set the lower text size limit and invalidate the view
     *
     * @param minTextSize
     */
    public void setMinTextSize(float minTextSize) {
        mMinTextSize = minTextSize;
        adjustTextSize();
    }

    private void adjustTextSize() {
        if (!mInitializedDimens) {
            return;
        }
        int startSize = (int) mMinTextSize;
        int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom()
                - getCompoundPaddingTop();
        mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft()
                - getCompoundPaddingRight();
        mAvailableSpaceRect.right = mWidthLimit;
        mAvailableSpaceRect.bottom = heightLimit;
        super.setTextSize(
                TypedValue.COMPLEX_UNIT_PX,
                efficientTextSizeSearch(startSize, (int) mMaxTextSize,
                        mSizeTester, mAvailableSpaceRect));
    }

    private final SizeTester mSizeTester = new SizeTester() {
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public int onTestSize(int suggestedSize, RectF availableSPace) {
            mPaint.setTextSize(suggestedSize);
            String text = getText().toString();
            boolean singleline = getMaxLines() == 1;
            if (singleline) {
                mTextRect.bottom = mPaint.getFontSpacing();
                mTextRect.right = mPaint.measureText(text);
            } else {
                StaticLayout layout = new StaticLayout(text, mPaint,
                        mWidthLimit, Alignment.ALIGN_NORMAL, mSpacingMult,
                        mSpacingAdd, true);

                // Return early if we have more lines
                if (getMaxLines() != NO_LINE_LIMIT
                        && layout.getLineCount() > getMaxLines()) {
                    return 1;
                }
                mTextRect.bottom = layout.getHeight();
                int maxWidth = -1;
                for (int i = 0; i < layout.getLineCount(); i++) {
                    if (maxWidth < layout.getLineWidth(i)) {
                        maxWidth = (int) layout.getLineWidth(i);
                    }
                }
                mTextRect.right = maxWidth;
            }

            mTextRect.offsetTo(0, 0);
            if (availableSPace.contains(mTextRect)) {

                // May be too small, don't worry we will find the best match
                return -1;
            } else {
                // too big
                return 1;
            }
        }
    };

    /**
     * Enables or disables size caching, enabling it will improve performance
     * where you are animating a value inside TextView. This stores the font
     * size against getText().length() Be careful though while enabling it as 0
     * takes more space than 1 on some fonts and so on.
     *
     * @param enable
     *            Enable font size caching
     */
    public void enableSizeCache(boolean enable) {
        mEnableSizeCache = enable;
        mTextCachedSizes.clear();
        adjustTextSize(getText().toString());
    }

    private int efficientTextSizeSearch(int start, int end,
            SizeTester sizeTester, RectF availableSpace) {
        if (!mEnableSizeCache) {
            return binarySearch(start, end, sizeTester, availableSpace);
        }
        int key = getText().toString().length();
        int size = mTextCachedSizes.get(key);
        if (size != 0) {
            return size;
        }
        size = binarySearch(start, end, sizeTester, availableSpace);
        mTextCachedSizes.put(key, size);
        return size;
    }

    private static int binarySearch(int start, int end, SizeTester sizeTester,
            RectF availableSpace) {
        int lastBest = start;
        int lo = start;
        int hi = end - 1;
        int mid = 0;
        while (lo <= hi) {
            mid = (lo + hi) >>> 1;
            int midValCmp = sizeTester.onTestSize(mid, availableSpace);
            if (midValCmp < 0) {
                lastBest = lo;
                lo = mid + 1;
            } else if (midValCmp > 0) {
                hi = mid - 1;
                lastBest = hi;
            } else {
                return mid;
            }
        }
        // Make sure to return the last best.
        // This is what should always be returned.
        return lastBest;

    }

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

    @Override
    protected void onSizeChanged(int width, int height, int oldwidth,
            int oldheight) {
        mInitializedDimens = true;
        mTextCachedSizes.clear();
        super.onSizeChanged(width, height, oldwidth, oldheight);
        if (width != oldwidth || height != oldheight) {
            adjustTextSize();
        }
    }
}

Warning:

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

Community
  • 1
  • 1
M-WaJeEh
  • 16,562
  • 9
  • 59
  • 93
  • ok, i've tested the code, and it seems fine. however, you used getMaxLines() , which is for API 16 , but you can store the maxLines and get it by overriding setMaxLines and store its value.i've changed it and now it works fine. also, since sometimes the text might be even too long for the smallest size, i've tried to use setEllipsize, and that worked too! i think we have a winner. please post your code here so i can mark it as the correct one. – android developer Jul 26 '13 at 22:48
  • you didn't remove the getMaxLines() and used your own. it will still work only from API16... please change it so that everyone could use it. i suggest to override setMaxLines to store the parameter to a field and then access the field instead of using getMaxLines. – android developer Jul 27 '13 at 07:50
  • check now, added support for max lines. – M-WaJeEh Jul 27 '13 at 10:25
  • seems fine. you should add "@TargetApi(Build.VERSION_CODES.JELLY_BEAN)" to the onTestSize() method so that Lint won't give an error/warning about it, and initialize it in the CTOR instead. setting the binary search to static is also a good thing, and you could do the initialization on the largest CTOR and call it from the other CTORs. however, it's really the best answer and you deserve a V . this is finally a good answer for this old question. good job! – android developer Jul 27 '13 at 13:23
  • @M-WaJeEh check the new post in this thread. a user called "MartinH" say that he has a fix for your code. – android developer Oct 28 '13 at 17:37
  • @M-WaJeEh I am using AutoResizeTextView. I have 4.2 (JB) and 4.4 (KK). Till now I was testing it on 4.2 everything worked fine but today when I ran the same code on 4.4 I am getting below error. **Error Message** java.lang.IllegalArgumentException: Layout: -128 < 0 **Where** AutoResizeTextView.java:189 **Short Stacktrace** at android.text.Layout.(Layout.java:138) – user2095470 Apr 20 '14 at 14:36
  • @user2095470 try out this library I've made that is based on this code: https://github.com/AndroidDeveloperLB/AutoFitTextView , and then you could submit an issue. Maybe I could help. – android developer Jun 07 '14 at 08:44
  • @M-WaJeEh I've noticed that on some cases, the last letter of the word goes to the next line. Can you please check it out? I've made a library so that everyone could use and contribute to this special TextView: https://github.com/AndroidDeveloperLB/AutoFitTextView – android developer Jun 07 '14 at 08:50
  • Hi, I'm finding that my text is not growing as large as it could be. I had a fiddle and it seems that getLineRight rather than getLineWidth produces better results... presumably getLineRight-getLineLeft would be the right thing to use. Anyone know why there is a difference? – Sam Feb 25 '15 at 22:02
  • I think the reason is getlinewidth includes spaces at the end of the lines – Sam Feb 25 '15 at 22:03
  • Always autofit.... Not work with "weigth='1'" or with "margins" ... (I know is awesome code, but not perfect), –  Apr 17 '15 at 09:10
  • The size caching can cause one or two characters at the end of the line to not display. The problem is that, for example, the size for string "---" will be cached as the size for all subsequent 3 character strings. But if I then set it to "+++", it will only show "++" because more room is needed for these wider characters. Disabling the caching causes the size to be calculated every time the text is set, so no characters are dropped. There is a warning about this in the code comments, but I think caching should be disabled by default. I don't see any drop in performance for my use cases. – Doug Simonton Aug 05 '15 at 22:56
  • @DougSimonton someone wrote about it and how to fix it, but I didn't understand how: https://github.com/AndroidDeveloperLB/AutoFitTextView/issues/15 . Also, I think there is another similar bug : https://github.com/AndroidDeveloperLB/AutoFitTextView/issues/23 – android developer Jan 13 '16 at 14:04
  • @M-WaJeEh Would you please have a look at my problem http://stackoverflow.com/questions/36265448/objectanimator-pixlated-textview – Muhammad Mar 29 '16 at 12:29
  • @M-WaJeEh I used your code but on one device I got this error- Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.text.TextPaint.setTextSize(float)' on a null object reference at com.mykaishi.xinkaishi.view.AutoResizeTextView$1.onTestSize(AutoResizeTextView.java:73) at com.mykaishi.xinkaishi.view.AutoResizeTextView.binarySearch(AutoResizeTextView.java:234) This is a customer reported crash so i am not able to reproduce the issue . The device is Huawei TAG TL00 and runs 5.1 . Can you help me ? – Shadab Ansari Mar 29 '16 at 23:23
  • This answer does not care about textAllCaps attr! I can sugget to use `String text = getTransformedText();` instead of `String text = getText().toString();` inside onTestSize, where getTransformedText(): private String getTransformedText() { CharSequence text = getText(); if (text != null) { TransformationMethod transformationMethod = getTransformationMethod(); if (transformationMethod != null) { text = transformationMethod.getTransformation(text, this); } } return text == null ? null : text.toString(); } – s.shostko Jun 07 '16 at 13:02
  • there is also TypeFace handling missed. necessary addition into `initialize` and `@Override public void setTypeface(Typeface tf, int style)`: `mPaint.setTypeface(getTypeface());` – s.shostko Jun 07 '16 at 13:40
  • I found that when we `setText()` before view known own bounds then text in first and second iteration (called by `setText()` and `onTextChanged()`) try to fit to 0 width and 0 height and on third iteration called by `onSizeChanged()` (when view was measured) text try to fit to `minTextSize`. So I edited source to fix above issue :) – sosite Jul 08 '16 at 09:44
  • @sosite I'm having a 0-height and 0-width issue. How did you end up modifying the source to resolve it? – stevendesu May 19 '18 at 04:07
13

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

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

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


Updated segment adjustTextSize(String):

private void adjustTextSize(final String text) {
  if (!mInitialized) {
    return;
  }
  int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
  mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();

  mAvailableSpaceRect.right = mWidthLimit;
  mAvailableSpaceRect.bottom = heightLimit;

  int maxTextSplits = text.split(" ").length;
  AutoResizeTextView.super.setMaxLines(Math.min(maxTextSplits, mMaxLines));

  super.setTextSize(
      TypedValue.COMPLEX_UNIT_PX,
      binarySearch((int) mMinTextSize, (int) mMaxTextSize,
                   mSizeTester, mAvailableSpaceRect));
}
MartinH
  • 154
  • 1
  • 4
  • you've updated M-WaJeEh's source. why did you put it as a new answer? i know you have good will, but is it a good thing to put it as an answer? i'm not sure he can see notice your answer... – android developer Oct 25 '13 at 06:48
  • 1
    I just signed up to be able to provide that update. My reputation count is only of 1 which prevents me from commenting on posts that aren't mine (minimum is 15). – MartinH Oct 25 '13 at 19:21
  • No worries, perhaps you'd like to transmit my answer to M-WajeEh as I can't even contact him at the moment :/ – MartinH Oct 28 '13 at 17:21
  • Can you please try to describe better what is it that you've done? did you fix a bug? can you show a screenshot in order to demonstrate the bug? – android developer Oct 28 '13 at 17:35
  • 1
    Hi @MartinH, Welcome to SO. I will surly look into this and will update my answer by mentioning your name and posting the link to your post **after testing**. Just give me few days please. I am stuck somewhere else nowadays. – M-WaJeEh Oct 29 '13 at 06:20
  • Thanks M-WaJeEh. I've also updated my answer with a reason for fixing that issue @androiddeveloper. No screenshots as I don't have access to the source anymore. – MartinH Jan 23 '14 at 18:55
  • @androideveloper: Take a TextView of 100dp, and a left icon (drawableLeft) of 10dp. The initial code was calculating the width available at 100dp, while in reality, because of the icon, we only have 90dp available. When the text was painted on screen, it was overlapping the icon, thinking it had those 100dps. Does that help? – MartinH Feb 03 '14 at 23:00
  • @MartinH I meant I don't understand the chat between you two. Was the issue handled in his post, or should your code replace his? – android developer Jun 06 '14 at 15:19
  • The fix I suggested was indeed incorporated directly within @M-WaJeEh's answer. – MartinH Mar 03 '15 at 21:18
  • Allot of things are missing, such as mMinTextSize,mMaxTextSize and mSizeTester. – tim687 Aug 31 '15 at 15:14
8

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

Converging algorithm looks like this:

for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) {

    // Go to the mean value...
    testSize = (upperTextSize + lowerTextSize) / 2;

    // ... inflate the dummy TextView by setting a scaled textSize and the text...
    mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor);
    mTestView.setText(text);

    // ... call measure to find the current values that the text WANTS to occupy
    mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
    int tempHeight = mTestView.getMeasuredHeight();

    // ... decide whether those values are appropriate.
    if (tempHeight >= targetFieldHeight) {
        upperTextSize = testSize; // Font is too big, decrease upperSize
    }
    else {
        lowerTextSize = testSize; // Font is too small, increase lowerSize
    }
}

And the whole class can be found here.

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

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

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

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

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

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


Please note:

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

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

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

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

Good luck and best regards

Community
  • 1
  • 1
avalancha
  • 1,148
  • 1
  • 19
  • 38
  • there are some issues, but overall it works so good job. the issues: doesn't support multi line (as you wrote) and will break the word in case you try to do it, include API16 function(addOnGlobalLayoutListener) which i think can be totally avoided (or at least use OnPreDrawListener instead), uses binary search so it might test sizes that it doesn't fit at all (and use more memory for no reason,which can cause exceptions if it's too large), doesn't support handling when anything changes (like the text itself). – android developer Apr 23 '13 at 19:13
  • btw, you don't have to use the mTestView variable and do the testing directly on the current view, and this way you won't have to consider all of the special things. also, i think that instead of MAX_TEXT_SIZE you can use the targetFieldHeight . – android developer Apr 23 '13 at 19:25
  • i think that instead of a pure binary search (especially 2 of them), you can get a better guess by the ratio of the target against the measured size. there are also other methods (newton and something else that i don't remember) but i don't think they fit here. the one i suggested now is better since you can start from the min size instead of testing the max size.fact is that you won't need a max size since you keep making a better guess. – android developer Apr 23 '13 at 19:42
  • i also think that the 2 binary searches could be merged to a single line when the condition could simply be : if(getMeasuredWidth()>=targetFieldWidth||getMeasuredHeight()>=targetFieldHeight) – android developer Apr 23 '13 at 19:59
  • 1
    another issue i've found is the text gravity. i think it can't be centered vertically. – android developer Apr 23 '13 at 20:14
  • trolling about what? i think you made a great job, but the solution still misses the way a textview behaves. – android developer Apr 24 '13 at 08:33
  • Well if it's a great job than the least you could do is upvote this answer. And next you could take what you get and try to improve and integrate yourself a bit, realize that this is a community and not a free buffet – avalancha Apr 24 '13 at 12:27
  • I am sorry. you are right. i should be more grateful and only then make my comments. – android developer Apr 24 '13 at 12:33
  • I do not get the expect behaviour using this view, for long words (e.g. email address) a few characters are missing. – ThomasRS Oct 08 '13 at 21:21
  • +1, The most correct solution, what I have found. I am changing runtime the textview layout params: width, height, margin top, bottom, but after that requesting layout. Sometimes I got to small text size: half width and heigh taken, but in other devices is ok –  Nov 02 '13 at 18:28
  • Doesn't work with Android 4.03 - fonts are invisible, while in other Android version they're ok – Malachiasz Feb 14 '14 at 14:57
5

I'll explain how works this attribute lower android versions step by step:

1- Import android support library 26.x.x on your project gradle file. If there is no support library on IDE, they will download automatically.

dependencies {
    compile 'com.android.support:support-v4:26.1.0'
    compile 'com.android.support:appcompat-v7:26.1.0'
    compile 'com.android.support:support-v13:26.1.0' }

allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
        }
    } }

2- Open your layout XML file and refactor like this tag your TextView. This scenario is: when incrased font size on system, fit text to avaliable width, not word wrap.

<android.support.v7.widget.AppCompatTextView
            android:id="@+id/textViewAutoSize"
            android:layout_width="match_parent"
            android:layout_height="25dp"
            android:ellipsize="none"
            android:text="Auto size text with compatible lower android versions."
            android:textSize="12sp"
            app:autoSizeMaxTextSize="14sp"
            app:autoSizeMinTextSize="4sp"
            app:autoSizeStepGranularity="0.5sp"
            app:autoSizeTextType="uniform" />
The Goat
  • 965
  • 10
  • 19
  • Their implementation of auto-fit TextView doesn't work well though. There are times that instead of resizing the font size, the text just wraps to the next line, badly... It's even shown on their videos, as if it's a good thing: https://youtu.be/fjUdJ2aVqE4?t=14 . Notice how the "w" of "TextView" goes to the next line... Anyway, created a new request about it here: https://issuetracker.google.com/issues/68787108 . Please consider starring it. – android developer Nov 02 '17 at 06:25
  • @androiddeveloper You're right. I didn't explain this tag implementation works single line. If you needs wrap lines, you have to change this attribute: android:layout_height="wrap_content". But android.support.v7.widget.AppCompatTextView and gradle implementation guarantee works this attribute. – The Goat Nov 02 '17 at 07:16
  • I don't think you understand. I don't want part-words to wrap to the next line, unless there is no other way to solve it. In the video, you can easily see the fonts could have just gotten smaller instead. That's the issue. Are you saying you know how to fix it? Meaning there won't be word wraps, and only font size changes? – android developer Nov 02 '17 at 07:44
  • @androiddeveloper There are many scenario. My scenario is: autofit text to fixed with and not word wrap. During incrasing system font size, the text have to show not word wrap in textview. If word wrap doesn't matter for you, don't bother set fixed height. This property not using only word wrap, you can autofit the text have fixed width the TextView. – The Goat Nov 02 '17 at 08:06
  • According to my tests, the font sizes don't change correctly, and the word wrap issue can occur. – android developer Nov 02 '17 at 08:25
  • cant you just set max line to 1. – iamsujan Mar 06 '18 at 19:57
4

My requirement is to

  • Click on the ScalableTextView
  • Open a listActivity and display various length string items.
  • Select a text from list.
  • Set the text back to the ScalableTextView in another activity.

I referred the link: Auto Scale TextView Text to Fit within Bounds (including comments) and also the DialogTitle.java

I found that the solution provided is nice and simple but it does not dynamically change the size of the text box. It works great when the selected text length from the list view is greater in size than the existing text lenght in the ScalableTextView. When selected the text having length smaller than the existing text in the ScalableTextView, it do not increase the size of the text, showing the text in the smaller size.

I modified the ScalableTextView.java to readjust the text size based on the text length. Here is my ScalableTextView.java

public class ScalableTextView extends TextView
{
float defaultTextSize = 0.0f;

public ScalableTextView(Context context, AttributeSet attrs, int defStyle)
{
    super(context, attrs, defStyle);
    setSingleLine();
    setEllipsize(TruncateAt.END);
    defaultTextSize = getTextSize();
}

public ScalableTextView(Context context, AttributeSet attrs)
{
    super(context, attrs);
    setSingleLine();
    setEllipsize(TruncateAt.END);
    defaultTextSize = getTextSize();
}

public ScalableTextView(Context context)
{
    super(context);
    setSingleLine();
    setEllipsize(TruncateAt.END);
    defaultTextSize = getTextSize();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    setTextSize(TypedValue.COMPLEX_UNIT_PX, defaultTextSize);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

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

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

                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                ellipsisCount = layout.getEllipsisCount(lineCount - 1);
            }
        }
    }
}
}

Happy Coding....

Community
  • 1
  • 1
Devendra Vaja
  • 3,437
  • 1
  • 16
  • 13
  • Shouldn't you do it using binary search instead of reducing by one for each iteration ? Also, it seems that this code forces the TextView to be with a single line of text, instead of allowing it to have multiple lines. – android developer Feb 09 '16 at 11:38
  • This was the only solution that worked for me. My problem was related to TExtView not fitting the view in 4.1 only – FOliveira Jul 27 '16 at 10:12
  • it seems like it doesn't match parent for 100%. see screenshot: https://s14.postimg.org/93c2xgs75/Screenshot_2.png – user25 Sep 11 '16 at 15:56
3

Warning, bug in Android 3 (Honeycomb) and Android 4.0 (Ice Cream Sandwich)

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

The bug is described in Issue 22493: TextView height bug in Android 4.0 and Issue 17343: button's height and text does not return to its original state after increase and decrease the text size on HoneyComb.

The workaround is to add a newline 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.

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
Malachiasz
  • 6,621
  • 2
  • 31
  • 46
  • can you please show exactly on which lines (before/after which lines) and in which function? – android developer Feb 18 '14 at 11:22
  • ok thanks. i hope this fixes everything, and that whoever read this will find it helpful. currently i can't check it out. maybe next time that i will use it. – android developer Feb 18 '14 at 14:49
  • This code is called from outside of the view's code. Do you know perhaps of a good alternative way to handle this inside the view's code instead? – android developer May 13 '14 at 07:29
  • you could override textView.setText() and put this code inside of TextView class – Malachiasz May 13 '14 at 12:53
  • Wouldn't it also mean that "getText()" would return the text with the extra characters ? – android developer May 13 '14 at 19:55
  • if it's a problem, then override getText too :) – Malachiasz May 14 '14 at 09:20
  • will it have any problem with the rest of the algorithm, if you choose to override getText ? – android developer May 14 '14 at 09:44
  • if you ovverride it properly then no – Malachiasz May 14 '14 at 09:58
  • My idea was to add this character: "\u3000" in overridden setText() method and correspondingly remove this character ""\u3000" in overridden getText(). Just that, nothing more. – Malachiasz Jun 06 '14 at 14:52
  • So for getText, I need to do a substring of super.getText() ? Have you checked this idea? – android developer Jun 06 '14 at 22:14
  • BTW, you can now put your suggestion and issues on a library I've made for this: https://github.com/AndroidDeveloperLB/AutoFitTextView – android developer Jun 07 '14 at 08:54
  • No I haven't checked anything. Feel free to check everything yourself. – Malachiasz Jun 10 '14 at 08:29
  • I've noticed another issue and wrote about it on the library page: on some rare cases (and you now can see it in the sample quite easily), the last character of a line goes to the next line, so, for example, "Hello world" could become : "Hell" on first line , and "o world" on the second line. Could you please check it out? I've tried to fix it and didn't succeed so far. – android developer Jun 10 '14 at 08:49
  • The setText(CharSequence text) method is final in the TextView class, thus you won't be able to override it. You will need to use an custom method for this and call setText(yourText, BufferType.NORMAL) to achieve the override. – Ionut Negru Sep 22 '16 at 07:20
3

There's now an official solution to this problem. Autosizing TextViews introduced with Android O are available in the Support Library 26 and is backwards compatible all the way down to Android 4.0.

https://developer.android.com/preview/features/autosizing-textview.html

I'm not sure why https://stackoverflow.com/a/42940171/47680 which also included this information was deleted by an admin.

Community
  • 1
  • 1
Artem Russakovskii
  • 20,170
  • 17
  • 87
  • 114
  • 1
    Yes, I've heard about it too, but how good is it? Is there any sample of using it? Even in their video, I've noticed a bug: a letter from one word got to the line after (wrapped) : https://youtu.be/1N9KveJ-FU8?t=781 (notice the "W" ) – android developer May 19 '17 at 06:08
3

From June 2018 Android officially support this feature for Android 4.0 (API level 14) and higher.
With Android 8.0 (API level 26) and higher:

setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize, 
        int autoSizeStepGranularity, int unit);

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

TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(TextView textView,
int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)

Check out my detail answer.

Think Twice Code Once
  • 5,273
  • 1
  • 22
  • 32
1

Convert the text view to an image, and the scale the image within the boundaries.

Here's an example on how to convert a view to an Image: Converting a view to Bitmap without displaying it in Android?

The problem is, your text will not be selectable, but it should do the trick. I haven't tried it, so I'm not sure how it would look (because of the scaling).

Community
  • 1
  • 1
Dr NotSoKind
  • 225
  • 1
  • 5
  • 14
1

Since Android O, it's possible to auto resize text in xml:

https://developer.android.com/preview/features/autosizing-textview.html

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

Android O allows you to instruct a TextView to let the text size expand or contract automatically to fill its layout based on the TextView's characteristics and boundaries. This setting makes it easier to optimize the text size on different screens with dynamic content.

The Support Library 26.0 Beta provides full support to the autosizing TextView feature on devices running Android versions prior to Android O. The library provides support to Android 4.0 (API level 14) and higher. The android.support.v4.widget package contains the TextViewCompat class to access features in a backward-compatible fashion.

Javatar
  • 1,913
  • 22
  • 32
  • Sadly, according to my tests, and even in Google IO's video, I've noticed it has issues, such as wrongly wrapping part-words, instead of resizing the font. I've reported about this here: https://issuetracker.google.com/issues/38468964 , and this is why I still don't use it. – android developer Jun 14 '17 at 11:59
1

After i tried Android official Autosizing TextView, i found if your Android version is prior to Android 8.0 (API level 26), you need use android.support.v7.widget.AppCompatTextView, and make sure your support library version is above 26.0.0. Example:

<android.support.v7.widget.AppCompatTextView
    android:layout_width="130dp"
    android:layout_height="32dp"
    android:maxLines="1"
    app:autoSizeMaxTextSize="22sp"
    app:autoSizeMinTextSize="12sp"
    app:autoSizeStepGranularity="2sp"
    app:autoSizeTextType="uniform" />

update:

According to @android-developer's reply, i check the AppCompatActivity source code, and found these two lines in onCreate

final AppCompatDelegate delegate = getDelegate(); delegate.installViewFactory();

and in AppCompatDelegateImpl's createView

    if (mAppCompatViewInflater == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
    }

it use AppCompatViewInflater inflater view, when AppCompatViewInflater createView it will use AppCompatTextView for "TextView".

public final View createView(){
    ...
    View view = null;
    switch (name) {
        case "TextView":
            view = new AppCompatTextView(context, attrs);
            break;
        case "ImageView":
            view = new AppCompatImageView(context, attrs);
            break;
        case "Button":
            view = new AppCompatButton(context, attrs);
            break;
    ...
}

In my project i don't use AppCompatActivity, so i need use <android.support.v7.widget.AppCompatTextView> in xml.

weei.zh
  • 221
  • 2
  • 6
0

Below is avalancha TextView with added functionality for custom Font.

Usage:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:foo="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="match_parent" >

                <de.meinprospekt.androidhd.view.AutoFitText
                android:layout_width="wrap_content"
                android:layout_height="10dp"
                android:text="Small Text"
                android:textColor="#FFFFFF"
                android:textSize="100sp"
                foo:customFont="fonts/Roboto-Light.ttf" />

</FrameLayout>

Don't forget to add: xmlns:foo="http://schemas.android.com/apk/res-auto". Font should be in assets firectory

import java.util.ArrayList;
import java.util.List;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.TextView;
import de.meinprospekt.androidhd.R;
import de.meinprospekt.androidhd.adapter.BrochuresHorizontalAdapter;
import de.meinprospekt.androidhd.util.LOG;

/**
 * https://stackoverflow.com/a/16174468/2075875 This class builds a new android Widget named AutoFitText which can be used instead of a TextView to
 * have the text font size in it automatically fit to match the screen width. Credits go largely to Dunni, gjpc, gregm and speedplane from
 * Stackoverflow, method has been (style-) optimized and rewritten to match android coding standards and our MBC. This version upgrades the original
 * "AutoFitTextView" to now also be adaptable to height and to accept the different TextView types (Button, TextClock etc.)
 * 
 * @author pheuschk
 * @createDate: 18.04.2013
 * 
 * combined with: https://stackoverflow.com/a/7197867/2075875
 */
@SuppressWarnings("unused")
public class AutoFitText extends TextView {

    private static final String TAG = AutoFitText.class.getSimpleName();

    /** Global min and max for text size. Remember: values are in pixels! */
    private final int MIN_TEXT_SIZE = 10;
    private final int MAX_TEXT_SIZE = 400;

    /** Flag for singleLine */
    private boolean mSingleLine = false;

    /**
     * A dummy {@link TextView} to test the text size without actually showing anything to the user
     */
    private TextView mTestView;

    /**
     * A dummy {@link Paint} to test the text size without actually showing anything to the user
     */
    private Paint mTestPaint;

    /**
     * Scaling factor for fonts. It's a method of calculating independently (!) from the actual density of the screen that is used so users have the
     * same experience on different devices. We will use DisplayMetrics in the Constructor to get the value of the factor and then calculate SP from
     * pixel values
     */
    private float mScaledDensityFactor;

    /**
     * Defines how close we want to be to the factual size of the Text-field. Lower values mean higher precision but also exponentially higher
     * computing cost (more loop runs)
     */
    private final float mThreshold = 0.5f;

    /**
     * Constructor for call without attributes --> invoke constructor with AttributeSet null
     * 
     * @param context
     */
    public AutoFitText(Context context) {
        this(context, null);
    }

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

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

    private void init(Context context, AttributeSet attrs) {
        //TextViewPlus part https://stackoverflow.com/a/7197867/2075875
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoFitText);
        String customFont = a.getString(R.styleable.AutoFitText_customFont);
        setCustomFont(context, customFont);
        a.recycle();

        // AutoFitText part
        mScaledDensityFactor = context.getResources().getDisplayMetrics().scaledDensity;
        mTestView = new TextView(context);

        mTestPaint = new Paint();
        mTestPaint.set(this.getPaint());

        this.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                // make an initial call to onSizeChanged to make sure that refitText is triggered
                onSizeChanged(AutoFitText.this.getWidth(), AutoFitText.this.getHeight(), 0, 0);
                // Remove the LayoutListener immediately so we don't run into an infinite loop
                //AutoFitText.this.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                removeOnGlobalLayoutListener(AutoFitText.this, this);
            }
        });
    }

    public boolean setCustomFont(Context ctx, String asset) {
        Typeface tf = null;
        try {
        tf = Typeface.createFromAsset(ctx.getAssets(), asset);  
        } catch (Exception e) {
            LOG.e(TAG, "Could not get typeface: "+e.getMessage());
            return false;
        }

        setTypeface(tf);  
        return true;
    }

    @SuppressLint("NewApi")
    public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener){
        if (Build.VERSION.SDK_INT < 16) {
            v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
        } else {
            v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
        }
    }

    /**
     * Main method of this widget. Resizes the font so the specified text fits in the text box assuming the text box has the specified width. This is
     * done via a dummy text view that is refit until it matches the real target width and height up to a certain threshold factor
     * 
     * @param targetFieldWidth The width that the TextView currently has and wants filled
     * @param targetFieldHeight The width that the TextView currently has and wants filled
     */
    private void refitText(String text, int targetFieldWidth, int targetFieldHeight) {

        // Variables need to be visible outside the loops for later use. Remember size is in pixels
        float lowerTextSize = MIN_TEXT_SIZE;
        float upperTextSize = MAX_TEXT_SIZE;

        // Force the text to wrap. In principle this is not necessary since the dummy TextView
        // already does this for us but in rare cases adding this line can prevent flickering
        this.setMaxWidth(targetFieldWidth);

        // Padding should not be an issue since we never define it programmatically in this app
        // but just to to be sure we cut it off here
        targetFieldWidth = targetFieldWidth - this.getPaddingLeft() - this.getPaddingRight();
        targetFieldHeight = targetFieldHeight - this.getPaddingTop() - this.getPaddingBottom();

        // Initialize the dummy with some params (that are largely ignored anyway, but this is
        // mandatory to not get a NullPointerException)
        mTestView.setLayoutParams(new LayoutParams(targetFieldWidth, targetFieldHeight));

        // maxWidth is crucial! Otherwise the text would never line wrap but blow up the width
        mTestView.setMaxWidth(targetFieldWidth);

        if (mSingleLine) {
            // the user requested a single line. This is very easy to do since we primarily need to
            // respect the width, don't have to break, don't have to measure...

            /*************************** Converging algorithm 1 ***********************************/
            for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) {

                // Go to the mean value...
                testSize = (upperTextSize + lowerTextSize) / 2;

                mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor);
                mTestView.setText(text);
                mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);

                if (mTestView.getMeasuredWidth() >= targetFieldWidth) {
                    upperTextSize = testSize; // Font is too big, decrease upperSize
                } else {
                    lowerTextSize = testSize; // Font is too small, increase lowerSize
                }
            }
            /**************************************************************************************/

            // In rare cases with very little letters and width > height we have vertical overlap!
            mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);

            if (mTestView.getMeasuredHeight() > targetFieldHeight) {
                upperTextSize = lowerTextSize;
                lowerTextSize = MIN_TEXT_SIZE;

                /*************************** Converging algorithm 1.5 *****************************/
                for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) {

                    // Go to the mean value...
                    testSize = (upperTextSize + lowerTextSize) / 2;

                    mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor);
                    mTestView.setText(text);
                    mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);

                    if (mTestView.getMeasuredHeight() >= targetFieldHeight) {
                        upperTextSize = testSize; // Font is too big, decrease upperSize
                    } else {
                        lowerTextSize = testSize; // Font is too small, increase lowerSize
                    }
                }
                /**********************************************************************************/
            }
        } else {

            /*********************** Converging algorithm 2 ***************************************/
            // Upper and lower size converge over time. As soon as they're close enough the loop
            // stops
            // TODO probe the algorithm for cost (ATM possibly O(n^2)) and optimize if possible
            for (float testSize; (upperTextSize - lowerTextSize) > mThreshold;) {

                // Go to the mean value...
                testSize = (upperTextSize + lowerTextSize) / 2;

                // ... inflate the dummy TextView by setting a scaled textSize and the text...
                mTestView.setTextSize(TypedValue.COMPLEX_UNIT_SP, testSize / mScaledDensityFactor);
                mTestView.setText(text);

                // ... call measure to find the current values that the text WANTS to occupy
                mTestView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
                int tempHeight = mTestView.getMeasuredHeight();
                // int tempWidth = mTestView.getMeasuredWidth();

                // LOG.debug("Measured: " + tempWidth + "x" + tempHeight);
                // LOG.debug("TextSize: " + testSize / mScaledDensityFactor);

                // ... decide whether those values are appropriate.
                if (tempHeight >= targetFieldHeight) {
                    upperTextSize = testSize; // Font is too big, decrease upperSize
                } else {
                    lowerTextSize = testSize; // Font is too small, increase lowerSize
                }
            }
            /**************************************************************************************/

            // It is possible that a single word is wider than the box. The Android system would
            // wrap this for us. But if you want to decide fo yourself where exactly to break or to
            // add a hyphen or something than you're going to want to implement something like this:
            mTestPaint.setTextSize(lowerTextSize);
            List<String> words = new ArrayList<String>();

            for (String s : text.split(" ")) {
                Log.i("tag", "Word: " + s);
                words.add(s);
            }
            for (String word : words) {
                if (mTestPaint.measureText(word) >= targetFieldWidth) {
                    List<String> pieces = new ArrayList<String>();
                    // pieces = breakWord(word, mTestPaint.measureText(word), targetFieldWidth);

                    // Add code to handle the pieces here...
                }
            }
        }

        /**
         * We are now at most the value of threshold away from the actual size. To rather undershoot than overshoot use the lower value. To match
         * different screens convert to SP first. See {@link http://developer.android.com/guide/topics/resources/more-resources.html#Dimension} for
         * more details
         */
        this.setTextSize(TypedValue.COMPLEX_UNIT_SP, lowerTextSize / mScaledDensityFactor);
        return;
    }

    /**
     * This method receives a call upon a change in text content of the TextView. Unfortunately it is also called - among others - upon text size
     * change which means that we MUST NEVER CALL {@link #refitText(String)} from this method! Doing so would result in an endless loop that would
     * ultimately result in a stack overflow and termination of the application
     * 
     * So for the time being this method does absolutely nothing. If you want to notify the view of a changed text call {@link #setText(CharSequence)}
     */
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        // Super implementation is also intentionally empty so for now we do absolutely nothing here
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
    }

    @Override
    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
        if (width != oldWidth && height != oldHeight) {
            refitText(this.getText().toString(), width, height);
        }
    }

    /**
     * This method is guaranteed to be called by {@link TextView#setText(CharSequence)} immediately. Therefore we can safely add our modifications
     * here and then have the parent class resume its work. So if text has changed you should always call {@link TextView#setText(CharSequence)} or
     * {@link TextView#setText(CharSequence, BufferType)} if you know whether the {@link BufferType} is normal, editable or spannable. Note: the
     * method will default to {@link BufferType#NORMAL} if you don't pass an argument.
     */
    @Override
    public void setText(CharSequence text, BufferType type) {

        int targetFieldWidth = this.getWidth();
        int targetFieldHeight = this.getHeight();

        if (targetFieldWidth <= 0 || targetFieldHeight <= 0 || text.equals("")) {
            // Log.v("tag", "Some values are empty, AutoFitText was not able to construct properly");
        } else {
            refitText(text.toString(), targetFieldWidth, targetFieldHeight);
        }
        super.setText(text, type);
    }

    /**
     * TODO add sensibility for {@link #setMaxLines(int)} invocations
     */
    @Override
    public void setMaxLines(int maxLines) {
        // TODO Implement support for this. This could be relatively easy. The idea would probably
        // be to manipulate the targetHeight in the refitText-method and then have the algorithm do
        // its job business as usual. Nonetheless, remember the height will have to be lowered
        // dynamically as the font size shrinks so it won't be a walk in the park still
        if (maxLines == 1) {
            this.setSingleLine(true);
        } else {
            throw new UnsupportedOperationException("MaxLines != 1 are not implemented in AutoFitText yet, use TextView instead");
        }
    }

    @Override
    public void setSingleLine(boolean singleLine) {
        // save the requested value in an instance variable to be able to decide later
        mSingleLine = singleLine;
        super.setSingleLine(singleLine);
    }
}

known bugs: Doesn't work with Android 4.03 - fonts are invisible or very small (original avalancha doesn't work too) below is workaround for that bug: https://stackoverflow.com/a/21851239/2075875

Community
  • 1
  • 1
Malachiasz
  • 6,621
  • 2
  • 31
  • 46
0

Try this

TextWatcher changeText = new TextWatcher() {
     @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                tv3.setText(et.getText().toString());
                tv3.post(new Runnable() {           
                    @Override
                    public void run() {
                    while(tv3.getLineCount() >= 3){                     
                            tv3.setTextSize((tv3.getTextSize())-1);                     
                        }
                    }
                });
            }

            @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

            @Override public void afterTextChanged(Editable s) { }
        };
Excommunicated
  • 1,222
  • 8
  • 14
  • I don't think this will work, as each time you change the text size, it will cause the drawing only after you end with the block (meaning it will be added to the events-queue). This means that the loop will only work once at most each time the text changes, and it doesn't guarantee that the number of lines will not exceed 3 lines of text. – android developer Oct 30 '14 at 18:19
0

If you are looking for something easier:

 public MyTextView extends TextView{

    public void resize(String text, float textViewWidth, float textViewHeight) {
       Paint p = new Paint();
       Rect bounds = new Rect();
       p.setTextSize(1);
       p.getTextBounds(text, 0, text.length(), bounds);
       float widthDifference = (textViewWidth)/bounds.width();
       float heightDifference = (textViewHeight);
       textSize = Math.min(widthDifference, heightDifference);
       setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
Sander
  • 636
  • 1
  • 5
  • 16
0

Quick fix for the issue described by @Malachiasz

I've fixed the issue by adding custom support for this in the auto resize class:

public void setTextCompat(final CharSequence text) {
    setTextCompat(text, BufferType.NORMAL);
}

public void setTextCompat(final CharSequence text, BufferType type) {
    // Quick fix for Android Honeycomb and Ice Cream Sandwich which sets the text only on the first call
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 &&
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
        super.setText(DOUBLE_BYTE_WORDJOINER + text + DOUBLE_BYTE_WORDJOINER, type);
    } else {
        super.setText(text, type);
    }
}

@Override
public CharSequence getText() {
    String originalText = super.getText().toString();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 &&
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
        // We try to remove the word joiners we added using compat method - if none found - this will do nothing.
        return originalText.replaceAll(DOUBLE_BYTE_WORDJOINER, "");
    } else {
        return originalText;
    }
}

Just call yourView.setTextCompat(newTextValue) instead of yourView.setText(newTextValue)

Ionut Negru
  • 5,577
  • 4
  • 44
  • 73
  • Why create a new function of setTextCompat, instead of overriding setText ? Also, which issue? – android developer Sep 22 '16 at 11:32
  • setText() is a final method of TextView, thus you won't be able to override it. Another option would be to do this outside of the custom TextView, but this solution is for using it inside the TextView. – Ionut Negru Sep 23 '16 at 05:46
  • Not exactly. Some of the "setText" are private, some are public, and some of the public are not final. It seems most can be handled by overriding public void setText(CharSequence text, BufferType type) . There is a final function that I don't know its purpose a though: public final void setText(char[] text, int start, int len) . Maybe have deeper look at the code. – android developer Sep 24 '16 at 20:06
  • All of those variants of setText() would actually call the setText(CharSequence text) method in the end. If you want to ensure the same behaviour, you would need to override that method, otherwise it would much better to simply add your own custom setText() method. – Ionut Negru Oct 11 '16 at 06:29
  • Yes, but maybe for most cases, it's ok. – android developer Oct 11 '16 at 09:26
  • Never said it was not ok :D Both approaches should work, it just depends on the needs and the developer preference. – Ionut Negru Oct 12 '16 at 16:08
-1

Try adding LayoutParams and MaxWidth and MaxHeight to the TextView. It will force the layout to respect the parent container and not overflow.

textview.setLayoutParams(new LayoutParams(LinearLayout.MATCH_PARENT,LinearLayout.WRAP_CONTENT));

int GeneralApproxWidthOfContainer = 400;
int GeneralApproxHeightOfContainer = 600;
textview.setMaxWidth(400);
textview.setMaxHeight(600);` 
Satan Pandeya
  • 3,460
  • 4
  • 22
  • 47
  • I'm not sure what is it that you are talking about. Do you suggest adding this to the textView sample, so that it won't have the small issue I sometimes see? if so, why didn't you post about it on the github website? – android developer Jul 30 '14 at 13:25