29
votes

I'm trying to create a method for resizing multi-line text in a TextView such that it fits within the bounds (both the X and Y dimensions) of the TextView.

At present, I have something, but all it does is resize the text such that just the first letter/character of the text fills the dimensions of the TextView (i.e. only the first letter is viewable, and it's huge). I need it to fit all the lines of the text within the bounds of the TextView.

Here is what I have so far:

public static void autoScaleTextViewTextToHeight(TextView tv)
{
    final float initSize = tv.getTextSize();
    //get the width of the view's back image (unscaled).... 
    float minViewHeight;
    if(tv.getBackground()!=null)
    {
      minViewHeight = tv.getBackground().getIntrinsicHeight();
    }
    else
    {
      minViewHeight = 10f;//some min.
    }
    final float maxViewHeight = tv.getHeight() - (tv.getPaddingBottom()+tv.getPaddingTop())-12;// -12 just to be sure
    final String s = tv.getText().toString();

    //System.out.println(""+tv.getPaddingTop()+"/"+tv.getPaddingBottom());

    if(minViewHeight >0 && maxViewHeight >2)
    {
      Rect currentBounds = new Rect();
      tv.getPaint().getTextBounds(s, 0, s.length(), currentBounds);
      //System.out.println(""+initSize);
      //System.out.println(""+maxViewHeight);
      //System.out.println(""+(currentBounds.height()));

      float resultingSize = 1;
      while(currentBounds.height() < maxViewHeight)
      {
        resultingSize ++;
        tv.setTextSize(resultingSize);

        tv.getPaint().getTextBounds(s, 0, s.length(), currentBounds);
        //System.out.println(""+(currentBounds.height()+tv.getPaddingBottom()+tv.getPaddingTop()));
        //System.out.println("Resulting: "+resultingSize);
      }
      if(currentBounds.height()>=maxViewHeight)
      {
        //just to be sure, reduce the value
        tv.setTextSize(resultingSize-1);
      }
    }
}

I think the problem is in the use of tv.getPaint().getTextBounds(...). It always returns small numbers for the text bounds... small relative to the tv.getWidth() and tv.getHeight() values... even if the text size is far larger than the width or height of the TextView.

14
This solution is neat: [Scale text in a view to fit?][1] [1]: stackoverflow.com/a/7259136/3857562Bob bobbington

14 Answers

22
votes

The AutofitTextView library from MavenCentral handles this nicely. The source hosted on Github(1k+ stars) at https://github.com/grantland/android-autofittextview

Add the following to your app/build.gradle

repositories {
    mavenCentral()
}

dependencies {
    compile 'me.grantland:autofittextview:0.2.+'
}

Enable any View extending TextView in code:

AutofitHelper.create(textView);

Enable any View extending TextView in XML:

<me.grantland.widget.AutofitLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:singleLine="true"
        />
</me.grantland.widget.AutofitLayout>

Use the built in Widget in code or XML:

<me.grantland.widget.AutofitTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:singleLine="true"
    />
10
votes

New since Android O:

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

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

I have played with this for quite some time, trying to get my font sizes correct on a wide variety of 7" tablets (kindle fire, Nexus7, and some inexpensive ones in China with low-res screens) and devices.

The approach that finally worked for me is as follows. The "32" is an arbitrary factor that basically gives about 70+ characters across a 7" tablet horizontal line, which is a font size I was looking for. Adjust accordingly.

textView.setTextSize(getFontSize(activity));


public static int getFontSize (Activity activity) { 

    DisplayMetrics dMetrics = new DisplayMetrics();
    activity.getWindowManager().getDefaultDisplay().getMetrics(dMetrics);

    // lets try to get them back a font size realtive to the pixel width of the screen
    final float WIDE = activity.getResources().getDisplayMetrics().widthPixels;
    int valueWide = (int)(WIDE / 32.0f / (dMetrics.scaledDensity));
    return valueWide;
}
3
votes

I was able to answer my own question using the following code (see below), but my solution was very specific to the application. For instance, this will probably only look good and/or work for a TextView sized to approx. 1/2 the screen (with also a 40px top margin and 20px side margins... no bottom margin).

The using this approach though, you can create your own similar implementation. The static method basically just looks at the number of characters and determines a scaling factor to apply to the TextView's text size, and then incrementally increases the text size until the overall height (an estimated height -- using the width of the text, the text height, and the width of the TextView) is just below that of the TextView. The parameters necessary to determine the scaling factor (i.e. the if/else if statements) were set by guess-and-check. You'll likely have to play around with the numbers to make it work for your particular application.

This isn't the most elegant solution, though it was easy to code and it works for me. Does anyone have a better approach?

public static void autoScaleTextViewTextToHeight(final TextView tv, String s)
    {       
        float currentWidth=tv.getPaint().measureText(s);
        int scalingFactor = 0;
        final int characters = s.length();
        //scale based on # of characters in the string
        if(characters<5)
        {
            scalingFactor = 1;
        }
        else if(characters>=5 && characters<10)
        {
            scalingFactor = 2;
        }
        else if(characters>=10 && characters<15)
        {
            scalingFactor = 3;
        }
        else if(characters>=15 && characters<20)
        {
            scalingFactor = 3;
        }
        else if(characters>=20 && characters<25)
        {
            scalingFactor = 3;
        }
        else if(characters>=25 && characters<30)
        {
            scalingFactor = 3;
        }
        else if(characters>=30 && characters<35)
        {
            scalingFactor = 3;
        }
        else if(characters>=35 && characters<40)
        {
            scalingFactor = 3;
        }
        else if(characters>=40 && characters<45)
        {
            scalingFactor = 3;
        }
        else if(characters>=45 && characters<50)
        {
            scalingFactor = 3;
        }
        else if(characters>=50 && characters<55)
        {
            scalingFactor = 3;
        }
        else if(characters>=55 && characters<60)
        {
            scalingFactor = 3;
        }
        else if(characters>=60 && characters<65)
        {
            scalingFactor = 3;
        }
        else if(characters>=65 && characters<70)
        {
            scalingFactor = 3;
        }
        else if(characters>=70 && characters<75)
        {
            scalingFactor = 3;
        }
        else if(characters>=75)
        {
            scalingFactor = 5;
        }

        //System.out.println(((int)Math.ceil(currentWidth)/tv.getWidth()+scalingFactor));
        //the +scalingFactor is important... increase this if nec. later
        while((((int)Math.ceil(currentWidth)/tv.getWidth()+scalingFactor)*tv.getTextSize())<tv.getHeight())
        {
            tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, tv.getTextSize()+0.25f);
            currentWidth=tv.getPaint().measureText(s);
            //System.out.println(((int)Math.ceil(currentWidth)/tv.getWidth()+scalingFactor));
        }

        tv.setText(s);
    }

Thanks.

3
votes

I had the same problem and wrote a class that seems to work for me. Basically, I used a static layout to draw the text in a separate canvas and remeasure until I find a font size that fits. You can see the class posted in the topic below. I hope it helps.

Auto Scale TextView Text to Fit within Bounds

3
votes

Stumbled upon this whilst looking for a solution myself... I'd tried all the other solutions out there that I could see on stack overflow etc but none really worked so I wrote my own.

Basically by wrapping the text view in a custom linear layout I've been able to successfully measure the text properly by ensuring it is measured with a fixed width.

<!-- TextView wrapped in the custom LinearLayout that expects one child TextView -->
<!-- This view should specify the size you would want the text view to be displayed at -->
<com.custom.ResizeView
    android:layout_width="fill_parent"
    android:layout_height="0dp"
    android:layout_margin="10dp"
    android:layout_weight="1"
    android:orientation="horizontal" >

    <TextView
        android:id="@+id/CustomTextView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
</com.custom.ResizeView>

Then the linear layout code

public class ResizeView extends LinearLayout {

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

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        // oldWidth used as a fixed width when measuring the size of the text
        // view at different font sizes
        final int oldWidth = getMeasuredWidth() - getPaddingBottom() - getPaddingTop();
        final int oldHeight = getMeasuredHeight() - getPaddingLeft() - getPaddingRight();

        // Assume we only have one child and it is the text view to scale
        TextView textView = (TextView) getChildAt(0);

        // This is the maximum font size... we iterate down from this
        // I've specified the sizes in pixels, but sp can be used, just modify
        // the call to setTextSize

        float size = getResources().getDimensionPixelSize(R.dimen.solutions_view_max_font_size);

        for (int textViewHeight = Integer.MAX_VALUE; textViewHeight > oldHeight; size -= 0.1f) {
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);

            // measure the text views size using a fixed width and an
            // unspecified height - the unspecified height means measure
            // returns the textviews ideal height
            textView.measure(MeasureSpec.makeMeasureSpec(oldWidth, MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED);

            textViewHeight = textView.getMeasuredHeight();
        }
    }
}

Hope this helps someone.

2
votes

maybe try setting setHoriztonallyScrolling() to true before taking text measurements so that the textView doesn't try to layout your text on multiple lines

2
votes

One way would be to specify different sp dimensions for each of the generalized screen sizes. For instance, provide 8sp for small screens, 12sp for normal screens, 16 sp for large and 20 sp for xlarge. Then just have your layouts refer to @dimen text_size or whatever and you can rest assured, as density is taken care of via the sp unit. See the following link for more info on this approach.

http://www.developer.android.com/guide/topics/resources/more-resources.html#Dimension

I must note, however, that supporting more languages means more work during the testing phase, especially if you're interested in keeping text on one line, as some languages have much longer words. In that case, make a dimens.xml file in the values-de-large folder, for example, and tweak the value manually. Hope this helps.

1
votes

Here is a solution that I created based on some other feedback. This solution allows you to set the size of the text in XML which will be the max size and it will adjust itself to fit the view height. Size Adjusting TextView

 private float findNewTextSize(int width, int height, CharSequence text) {
            TextPaint textPaint = new TextPaint(getPaint());

            float targetTextSize = textPaint.getTextSize();

            int textHeight = getTextHeight(text, textPaint, width, targetTextSize);
            while(textHeight > height && targetTextSize > mMinTextSize) {
                    targetTextSize = Math.max(targetTextSize - 1, mMinTextSize);
                    textHeight = getTextHeight(text, textPaint, width, targetTextSize);
            }
            return targetTextSize;
    }
private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {
            paint.setTextSize(textSize);
            StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
            return layout.getHeight();
    }
0
votes

If your only requirement is to have the text automatically split and continue in the next line and the height is not important then just have it like this.

<TextView
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:maxEms="integer"
    android:width="integer"/>

This will have your TextView wrap to it's content vertically depending on your maxEms value.

0
votes
0
votes

This is based on mattmook's answer. It worked well on some devices, but not on all. I moved the resizing to the measuring step, made the maximum font size a custom attribute, took margins into account, and extended FrameLayout instead of LineairLayout.

 public class ResizeView extends FrameLayout {
    protected float max_font_size;

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

        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.ResizeView,
                0, 0);
        max_font_size = a.getDimension(R.styleable.ResizeView_maxFontSize, 30.0f);
    }

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

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        // Use the parent's code for the first measure
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Assume we only have one child and it is the text view to scale
        final TextView textView = (TextView) getChildAt(0);

        // Check if the default measure resulted in a fitting textView
        LayoutParams childLayout = (LayoutParams) textView.getLayoutParams();
        final int textHeightAvailable = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - childLayout.topMargin - childLayout.bottomMargin;
        int textViewHeight = textView.getMeasuredHeight();
        if (textViewHeight < textHeightAvailable) {
            return;
        }

        final int textWidthSpec = MeasureSpec.makeMeasureSpec(
                MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight() - childLayout.leftMargin - childLayout.rightMargin, 
                MeasureSpec.EXACTLY);
        final int textHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

        for (float size = max_font_size; size >= 1.05f; size-=0.1f) {
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
            textView.measure(textWidthSpec, textHeightSpec);

            textViewHeight = textView.getMeasuredHeight();
            if (textViewHeight <= textHeightAvailable) {
                break;
            }
        }
    }
}

And this in attrs.xml:

<declare-styleable name="ResizeView">
    <attr name="maxFontSize" format="reference|dimension"/>
</declare-styleable>

And finally used like this:

<PACKAGE_NAME.ui.ResizeView xmlns:custom="http://schemas.android.com/apk/res/PACKAGE_NAME"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="start|center_vertical"
    android:padding="5dp"
    custom:maxFontSize="@dimen/normal_text">

    <TextView android:id="@+id/tabTitle2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</PACKAGE_NAME.ui.ResizeView>
0
votes

Try this...

tv.setText("Give a very large text anc check , this xample is very usefull");
    countLine=tv.getLineHeight();
    System.out.println("LineCount " + countLine);
    if (countLine>=40){
        tv.setTextSize(15);
    }