How to adjust text font size to fit textview
Solution 1
The solution below incorporates all of the suggestions here. It starts with what was originally posted by Dunni. It uses a binary search like gjpc's, but it is a bit more readable. It also include's gregm's bug fixes and a bug-fix of my own.
import android.content.Context;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
public class FontFitTextView extends TextView {
public FontFitTextView(Context context) {
super(context);
initialise();
}
public FontFitTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialise();
}
private void initialise() {
mTestPaint = new Paint();
mTestPaint.set(this.getPaint());
//max size defaults to the initially specified text size unless it is too small
}
/* Re size the font so the specified text fits in the text box
* assuming the text box is the specified width.
*/
private void refitText(String text, int textWidth)
{
if (textWidth <= 0)
return;
int targetWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();
float hi = 100;
float lo = 2;
final float threshold = 0.5f; // How close we have to be
mTestPaint.set(this.getPaint());
while((hi - lo) > threshold) {
float size = (hi+lo)/2;
mTestPaint.setTextSize(size);
if(mTestPaint.measureText(text) >= targetWidth)
hi = size; // too big
else
lo = size; // too small
}
// Use lo so that we undershoot rather than overshoot
this.setTextSize(TypedValue.COMPLEX_UNIT_PX, lo);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int height = getMeasuredHeight();
refitText(this.getText().toString(), parentWidth);
this.setMeasuredDimension(parentWidth, height);
}
@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);
}
}
//Attributes
private Paint mTestPaint;
}
Solution 2
I've written a class that extends TextView and does this. It just uses measureText as you suggest. Basically it has a maximum text size and minimum text size (which can be changed) and it just runs through the sizes between them in decrements of 1 until it finds the biggest one that will fit. Not particularly elegant, but I don't know of any other way.
Here is the code:
import android.content.Context;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.TextView;
public class FontFitTextView extends TextView {
public FontFitTextView(Context context) {
super(context);
initialise();
}
public FontFitTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialise();
}
private void initialise() {
testPaint = new Paint();
testPaint.set(this.getPaint());
//max size defaults to the intially specified text size unless it is too small
maxTextSize = this.getTextSize();
if (maxTextSize < 11) {
maxTextSize = 20;
}
minTextSize = 10;
}
/* Re size the font so the specified text fits in the text box
* assuming the text box is the specified width.
*/
private void refitText(String text, int textWidth) {
if (textWidth > 0) {
int availableWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();
float trySize = maxTextSize;
testPaint.setTextSize(trySize);
while ((trySize > minTextSize) && (testPaint.measureText(text) > availableWidth)) {
trySize -= 1;
if (trySize <= minTextSize) {
trySize = minTextSize;
break;
}
testPaint.setTextSize(trySize);
}
this.setTextSize(trySize);
}
}
@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);
}
}
//Getters and Setters
public float getMinTextSize() {
return minTextSize;
}
public void setMinTextSize(int minTextSize) {
this.minTextSize = minTextSize;
}
public float getMaxTextSize() {
return maxTextSize;
}
public void setMaxTextSize(int minTextSize) {
this.maxTextSize = minTextSize;
}
//Attributes
private Paint testPaint;
private float minTextSize;
private float maxTextSize;
}
Solution 3
This is speedplane's FontFitTextView
, but it only decreases font size if needed to make the text fit, and keeps its font size otherwise. It does not increase the font size to fit height.
public class FontFitTextView extends TextView {
// Attributes
private Paint mTestPaint;
private float defaultTextSize;
public FontFitTextView(Context context) {
super(context);
initialize();
}
public FontFitTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
private void initialize() {
mTestPaint = new Paint();
mTestPaint.set(this.getPaint());
defaultTextSize = getTextSize();
}
/* Re size the font so the specified text fits in the text box
* assuming the text box is the specified width.
*/
private void refitText(String text, int textWidth) {
if (textWidth <= 0 || text.isEmpty())
return;
int targetWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();
// this is most likely a non-relevant call
if( targetWidth<=2 )
return;
// text already fits with the xml-defined font size?
mTestPaint.set(this.getPaint());
mTestPaint.setTextSize(defaultTextSize);
if(mTestPaint.measureText(text) <= targetWidth) {
this.setTextSize(TypedValue.COMPLEX_UNIT_PX, defaultTextSize);
return;
}
// adjust text size using binary search for efficiency
float hi = defaultTextSize;
float lo = 2;
final float threshold = 0.5f; // How close we have to be
while (hi - lo > threshold) {
float size = (hi + lo) / 2;
mTestPaint.setTextSize(size);
if(mTestPaint.measureText(text) >= targetWidth )
hi = size; // too big
else
lo = size; // too small
}
// Use lo so that we undershoot rather than overshoot
this.setTextSize(TypedValue.COMPLEX_UNIT_PX, lo);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int height = getMeasuredHeight();
refitText(this.getText().toString(), parentWidth);
this.setMeasuredDimension(parentWidth, height);
}
@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 || h != oldh) {
refitText(this.getText().toString(), w);
}
}
}
Here is an example how it could be used in xml:
<com.your.package.activity.widget.FontFitTextView
android:id="@+id/my_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="My Text"
android:textSize="60sp" />
This would keep the font size to 60sp as long as the text fits in width. If the text is longer, it will decrease font size. In this case, the TextView
s height will also change because of height=wrap_content
.
If you find any bugs, feel free to edit.
Solution 4
Here is my solution which works on emulator and phones but not very well on Eclipse layout editor. It's inspired from kilaka's code but the size of the text is not obtained from the Paint but from measuring the TextView itself calling measure(0, 0)
.
The Java class :
public class FontFitTextView extends TextView
{
private static final float THRESHOLD = 0.5f;
private enum Mode { Width, Height, Both, None }
private int minTextSize = 1;
private int maxTextSize = 1000;
private Mode mode = Mode.None;
private boolean inComputation;
private int widthMeasureSpec;
private int heightMeasureSpec;
public FontFitTextView(Context context) {
super(context);
}
public FontFitTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FontFitTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray tAttrs = context.obtainStyledAttributes(attrs, R.styleable.FontFitTextView, defStyle, 0);
maxTextSize = tAttrs.getDimensionPixelSize(R.styleable.FontFitTextView_maxTextSize, maxTextSize);
minTextSize = tAttrs.getDimensionPixelSize(R.styleable.FontFitTextView_minTextSize, minTextSize);
tAttrs.recycle();
}
private void resizeText() {
if (getWidth() <= 0 || getHeight() <= 0)
return;
if(mode == Mode.None)
return;
final int targetWidth = getWidth();
final int targetHeight = getHeight();
inComputation = true;
float higherSize = maxTextSize;
float lowerSize = minTextSize;
float textSize = getTextSize();
while(higherSize - lowerSize > THRESHOLD) {
textSize = (higherSize + lowerSize) / 2;
if (isTooBig(textSize, targetWidth, targetHeight)) {
higherSize = textSize;
} else {
lowerSize = textSize;
}
}
setTextSize(TypedValue.COMPLEX_UNIT_PX, lowerSize);
measure(widthMeasureSpec, heightMeasureSpec);
inComputation = false;
}
private boolean isTooBig(float textSize, int targetWidth, int targetHeight) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
measure(0, 0);
if(mode == Mode.Both)
return getMeasuredWidth() >= targetWidth || getMeasuredHeight() >= targetHeight;
if(mode == Mode.Width)
return getMeasuredWidth() >= targetWidth;
else
return getMeasuredHeight() >= targetHeight;
}
private Mode getMode(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY)
return Mode.Both;
if(widthMode == MeasureSpec.EXACTLY)
return Mode.Width;
if(heightMode == MeasureSpec.EXACTLY)
return Mode.Height;
return Mode.None;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if(!inComputation) {
this.widthMeasureSpec = widthMeasureSpec;
this.heightMeasureSpec = heightMeasureSpec;
mode = getMode(widthMeasureSpec, heightMeasureSpec);
resizeText();
}
}
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
resizeText();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w != oldw || h != oldh)
resizeText();
}
public int getMinTextSize() {
return minTextSize;
}
public void setMinTextSize(int minTextSize) {
this.minTextSize = minTextSize;
resizeText();
}
public int getMaxTextSize() {
return maxTextSize;
}
public void setMaxTextSize(int maxTextSize) {
this.maxTextSize = maxTextSize;
resizeText();
}
}
The XML attribute file :
<resources>
<declare-styleable name="FontFitTextView">
<attr name="minTextSize" format="dimension" />
<attr name="maxTextSize" format="dimension" />
</declare-styleable>
</resources>
Check my github for the latest version of this class. I hope it can be useful for someone. If a bug is found or if the code needs explaination, feel free to open an issue on Github.
Solution 5
Thanks a lot to https://stackoverflow.com/users/234270/speedplane. Great answer!
Here is an improved version of his response that also take care of height and comes with a maxFontSize attribute to limit font size (was useful in my case, so I wanted to share it) :
package com.<your_package>;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
public class FontFitTextView extends TextView
{
private Paint mTestPaint;
private float maxFontSize;
private static final float MAX_FONT_SIZE_DEFAULT_VALUE = 20f;
public FontFitTextView(Context context)
{
super(context);
initialise(context, null);
}
public FontFitTextView(Context context, AttributeSet attributeSet)
{
super(context, attributeSet);
initialise(context, attributeSet);
}
public FontFitTextView(Context context, AttributeSet attributeSet, int defStyle)
{
super(context, attributeSet, defStyle);
initialise(context, attributeSet);
}
private void initialise(Context context, AttributeSet attributeSet)
{
if(attributeSet!=null)
{
TypedArray styledAttributes = context.obtainStyledAttributes(attributeSet, R.styleable.FontFitTextView);
maxFontSize = styledAttributes.getDimension(R.styleable.FontFitTextView_maxFontSize, MAX_FONT_SIZE_DEFAULT_VALUE);
styledAttributes.recycle();
}
else
{
maxFontSize = MAX_FONT_SIZE_DEFAULT_VALUE;
}
mTestPaint = new Paint();
mTestPaint.set(this.getPaint());
//max size defaults to the initially specified text size unless it is too small
}
/* Re size the font so the specified text fits in the text box
* assuming the text box is the specified width.
*/
private void refitText(String text, int textWidth, int textHeight)
{
if (textWidth <= 0)
return;
int targetWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();
int targetHeight = textHeight - this.getPaddingTop() - this.getPaddingBottom();
float hi = maxFontSize;
float lo = 2;
// final float threshold = 0.5f; // How close we have to be
final float threshold = 1f; // How close we have to be
mTestPaint.set(this.getPaint());
Rect bounds = new Rect();
while ((hi - lo) > threshold)
{
float size = (hi + lo) / 2;
mTestPaint.setTextSize(size);
mTestPaint.getTextBounds(text, 0, text.length(), bounds);
if (bounds.width() >= targetWidth || bounds.height() >= targetHeight)
hi = size; // too big
else
lo = size; // too small
// if (mTestPaint.measureText(text) >= targetWidth)
// hi = size; // too big
// else
// lo = size; // too small
}
// Use lo so that we undershoot rather than overshoot
this.setTextSize(TypedValue.COMPLEX_UNIT_PX, lo);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int height = getMeasuredHeight();
refitText(this.getText().toString(), parentWidth, height);
this.setMeasuredDimension(parentWidth, height);
}
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after)
{
refitText(text.toString(), this.getWidth(), this.getHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
if (w != oldw)
{
refitText(this.getText().toString(), w, h);
}
}
}
Corresponding /res/values/attr.xml file:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FontFitTextView">
<attr name="maxFontSize" format="dimension" />
</declare-styleable>
</resources>
Example:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:res-auto="http://schemas.android.com/apk/res-auto"
android:id="@+id/home_Layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background"
tools:ignore="ContentDescription" >
...
<com.<your_package>.FontFitTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="Sample Text"
android:textSize="28sp"
res-auto:maxFontSize="35sp"/>
...
</RelativeLayout>
To use the new maxFontSize
attribute, don't forget to add xmlns:res-auto="http://schemas.android.com/apk/res-auto"
as show in the example.
Related videos on Youtube
rudas
Updated on January 14, 2021Comments
-
rudas over 3 years
Is there any way in android to adjust the textsize in a textview to fit the space it occupies?
E.g. I'm using a
TableLayout
and adding severalTextView
s to each row. Since I don't want theTextView
s to wrap the text I rather see that it lowers the font size of the content.Any ideas?
I have tried
measureText
, but since I don't know the size of the column it seems troublesome to use. This is the code where I want to change the font size to something that fitsTableRow row = new TableRow(this); for (int i=0; i < ColumnNames.length; i++) { TextView textColumn = new TextView(this); textColumn.setText(ColumnNames[i]); textColumn.setPadding(0, 0, 1, 0); textColumn.setTextColor(getResources().getColor(R.drawable.text_default)); row.addView(textColumn, new TableRow.LayoutParams()); } table.addView(row, new TableLayout.LayoutParams());
-
vsm over 13 yearsCheck my solution based on dunni's code here stackoverflow.com/questions/5033012/… Note: I didn't implement it with a loop. PS: thanks dunni
-
-
Ted Hopp over 13 yearsA binary search would generally be faster than a linear search.
-
Ted Hopp over 13 yearsInstead of a binary search, a simple scaling works pretty well:
trySize *= availableWidth / measured_width
(then clamped to minTextSize). -
AlikElzin-kilaka over 12 yearsThanks for combining all the feedback. I see that the solution takes only width into consideration. My problem is that the fond exceeds the height.
-
AlikElzin-kilaka over 12 yearsOh, it seems to partially work at runtime. On a device ot emulator, the text is cut half way up. On the Eclipse layout editor it looks fine. Any ideas?
-
AlikElzin-kilaka over 12 yearsHow do you incorporate it in a layout xml?
-
Glenn over 12 yearsThis code is useful for placing text within an existing view into a constrained size area or you can create your own derived class from TextView and override the onMeasure as shown in other posts. By itself it cannot be used in a layout.
-
Ed Sinek about 12 yearsWorks great for me. Thx for posting. How do I introduce an XML attribute to specify hi/lo - and use the attribute value (if it exists - keeping existing values as defaults) in the refitText method?
-
Ed Sinek about 12 yearsthis post looks like it will do the trick (adding custom xml attributes for hi/lo): stackoverflow.com/a/8090772/156611
-
RoflcoptrException almost 12 years@speedplane Is there any way to also center this text?
-
android developer almost 12 years@kilaka , the code you've set on ppl website doesn't exist. please post it here ...
-
sulai over 11 yearsHow did you use it? I have added an example of how I use it to my answer. I have tested it with various android versions, emulators and ADT Graphical Layout Editor.
-
PearsonArtPhoto over 11 yearsEssentially, I wasn't specifying a specific height. Your onMeasure is allowing the view to take over if it wants. I managed to come up with a solution, changing the last line in the routine to
this.setMeasuredDimension(parentWidth, height);
-
sulai over 11 yearsGreat input, I corrected the code :) Now it works with
match_parent
andwrap_content
. -
Toni Alvarez over 11 yearsIt's works prefect on my Galaxy Nexus, but I've problems on devices with small screens.
-
yDelouis over 11 yearsWhat's your problem on devices with small screens ?
-
Toni Alvarez over 11 yearsSorry, the problem is not with small screens, your view is not working in all devices with Android 4.0, emulator included. I opened new issue in your GitHub
-
vault over 11 yearsI have the same problem as Hilaka, text is cut. See here
-
sulai over 11 yearsBy simply reading your code, I don't think this will work. Remember, the question was about automatically adjusting the text view's font size to fit into the view. You set a fixed font size.
-
android developer about 11 years@kilaka This still doesn't work well because it doesn't handle the height of the text well enough. Also, I think that instead of binary search, you can simply use "trySize *= availableWidth / measureTextWidth;" same for height.
-
android developer about 11 yearsSorry, but it still doesn't work on all cases. I also think it doesn't handle multi lines correctly.
-
Pascal about 11 yearsOh.. Could you give me a case for which it doesn't work? (Multi line is not supported, you are right)
-
android developer about 11 yearsMany cases. I've created a random tester just to prove that it's correct. Here's one sample: width of the view:317px, height: 137px , text: "q7Lr" . What I see is this: tinypic.com/view.php?pic=2dv5yf9&s=6 . here's the sample project i've made: mega.co.nz/… . I think that such a view should handle multi line, support any size of fonts, handle the height and not just the width, ... Sadly, none of the samples i've found has worked well.
-
Pascal about 11 yearsok, definitely not a complete solution. sorry. No time to improve it for the time being.. If you can improve it, do not hesitate to post your solution.
-
android developer about 11 yearsI've created a new thread that shows my tests, hoping that someone would be able to make a good solution : stackoverflow.com/questions/16017165/…
-
Zordid about 11 yearsWhy do you test for empty string writing "".equals(s) instead of simply s.isEmpty() ?? Or s.length()==0? Don't understand why I can see these equality tests sometimes.
-
sulai about 11 yearsGood point @Zordid, just didn't know about
string.isEmpty()
until now :) -
Casey Murray almost 11 yearsThis was a great solution! In case anyone else is new to android development and doesn't quite know how to implement an extended view in XML, it looks like this:
<com.example.zengame1.FontFitTextView android:paddingTop="5dip" android:id="@+id/childs_name" android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:layout_gravity="center" android:textSize="@dimen/text_size"/>
-
Andrew Mackenzie over 10 yearsGood answer (but not good enough to get selected?). Should also implement the third constructor for TextView as shown here: file:///Users/andrew/android-sdk-mac_x86/docs/reference/android/widget/TextView.html
-
Pratik Butani over 10 years
text.isEmpty()
Call requires API level 9 (current min is 8): java.lang.String#isEmpty change it totext.equals(null)
-
sulai over 10 years@PratikButani thanks for pointing this out. the equivalent to
text.isEmpty()
would be"".equals(text)
. -
Ricardo over 10 yearsGood solution. If you need really big text size for very large screens, increase the initial value of
float hi
. -
StuStirling over 10 yearsAny way to only resize if the text exceeds the parent width? For example I don't want to increase the size of the text if it fits in ok.
-
Rahul Rastogi almost 10 yearsNot working in my android 4.4. Text size is not working.
-
AlexGuti almost 10 yearsGreat work! Got it working also with buttons. To take care of the textview height just set
float hi = this.getHeight() - this.getPaddingBottom() - this.getPaddingTop();
-
Dino about 9 yearsI am trying to implement this on my widget, but I keep getting no widget? I have posted a help here link I know it is the
view.setTextViewText()
that is the issue here -
SMBiggs almost 9 yearsMake sure that you check the view's dimensions after it has been drawn. Doing this during onCreate() is too early. I used ViewTreeObserver to make sure the measurements were taken at the right time.
-
LiangWang over 8 yearsi think it would be more reasonable to do refitText at "onLayout" rather than "onSizeChanged"
-
LiangWang over 8 yearsin the real world, believe it or not, when resizing is triggered, onMeasure will be called a lot of times (>20). I think it's enough to put refitText into onSizeChanged and onTextChanged. (I tested it on EditText)
-
Foobar over 8 yearsDoes this support multiple lines of text?
-
Muhammad about 8 yearsCan you please have a look at my problem stackoverflow.com/questions/36265448/…
-
user3690202 over 7 yearsI can't believe this has so many up votes when it clearly doesn't work. It completely ignores the height of the text - so for cases where the text is only a couple of characters it is possible for the text to become cut in half because it is too high for the text view.
-
AlexGuti over 7 years@user3690202, now i use github.com/grantland/android-autofittextview library to achieve this behavior. Good luck!
-
Rafael Lima almost 6 yearsI know is a long time ago and probably nobody will read it but: "why do you need to do binary search if you can very acurately estimate the value?" you can very accurately estimate the optimal size by
mTestPaint.set(this.getPaint()); float v = mTestPaint.measureText(text); if (v > targetWidth) hi = hi * targetWidth / v;
-
chari sharma almost 6 yearsgravity function is not working like : android:gravity="center_vertical|center_horizontal" How can i achieve this?
-
chari sharma almost 6 yearsall text support only one line , is it possibility to add multiple line.
-
Atul Bhardwaj almost 4 yearsUse app:autoSizeTextType="uniform" for backward compatibility because android:autoSizeTextType="uniform" only work in API Level 26 and higher.
-
Atul Bhardwaj almost 4 yearsThanks Suraj. This is the best solution
-
Georgiy Chebotarev almost 4 yearsI checked it on SDK 24 and it doesn't work. Are you sure, that
app:..
will works for SDK low 26? -
Suraj Vaishnav almost 4 yearsYes, It works as an app namespace provides backward compatibility. There is a detailed article about it, check this maybe it would solve your problem. medium.com/over-engineering/…