I\'ve been searching for a way of auto fit a text inside a textview. Through my search I\'ve found many solutions like:
Using the idea from nunofmendes, I wrote a derived class of TextView that auto resizes text and supports multiple lines.
import android.content.Context;
import android.os.Handler;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.TypedValue;
public class AutoResizeTextView extends TextView {
private Handler measureHandler = new Handler();
private Runnable requestLayout = new Runnable() {
@Override
public void run() {
requestLayout();
}
};
public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public AutoResizeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AutoResizeTextView(Context context) {
super(context);
}
@Override
protected void onMeasure(final int widthMeasureSpec,
final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final float maxWidth = getWidth();
final float maxHeight = getHeight();
if (maxWidth < 1.0f || maxHeight < 1.0f) {
return;
}
int index = 0;
int lineCount = 0;
CharSequence text = getText();
final TextPaint paint = getPaint();
while (index < text.length()) {
index += paint.breakText(text, index, text.length(), true, maxWidth, null);
lineCount++;
}
final float height = lineCount * getLineHeight() + (lineCount > 0 ?
(lineCount - 1) * paint.getFontSpacing() : 0);
if (height > maxHeight) {
final float textSize = getTextSize();
setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1));
measureHandler.post(requestLayout);
}
}
}
thanks for your solution you are superman! I have implemented your code in derived class of TextView. The code is converted to C# code for Xamarin android. You can easily convert to Java if you need(remove the first constructor for Java).
public class AutoFitTextView : TextView
{
public AutoFitTextView(System.IntPtr javaReference, Android.Runtime.JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
public AutoFitTextView(Context context)
: base(context)
{
}
public AutoFitTextView(Context context, IAttributeSet attrs)
: base(context, attrs)
{
}
public void ResizeFontSize()
{
int textSize = 50;
int maxHeight = this.Height;
while (GetHeightOfMultiLineText(this.Text, textSize, this.Width) > maxHeight)
{
textSize--;
}
float scaleFactor = Context.Resources.DisplayMetrics.ScaledDensity;
float additionalFactor = 1.2f;
TextSize = ((float)(textSize / (additionalFactor * scaleFactor)));
}
private int GetHeightOfMultiLineText(string text, int textSize, int maxWidth)
{
TextPaint paint = new TextPaint();
paint.TextSize = textSize;
int index = 0;
int lineCount = 0;
while (index < text.Length)
{
index += paint.BreakText(text, index, text.Length, true, maxWidth, null);
lineCount++;
}
Rect bounds = new Rect();
paint.GetTextBounds("Yy", 0, 2, bounds);
// obtain space between lines
double lineSpacing = Math.Max(0, ((lineCount - 1) * bounds.Height() * 0.25));
return (int)Math.Floor(lineSpacing + lineCount * bounds.Height());
}
}
Here is AXML code
<touchtest.AutoFitTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/tvLabel2"
android:singleLine="false"
android:inputType="textMultiLine"
android:text="I am here To test this text" />
The compilable, fixed code: copy-paste this one, not the accepted one, because you need to fix it:)
public static int getHeightOfMultiLineText(String text, int textSize, int maxWidth) {
TextPaint paint = new TextPaint();
paint.setTextSize(textSize);
int index = 0;
int lineCount = 0;
while (index < text.length()) {
index += paint.breakText(text, index, text.length(), true, maxWidth, null);
lineCount++;
}
Rect bounds = new Rect();
paint.getTextBounds("Yy", 0, 2, bounds);
// obtain space between lines
double lineSpacing = Math.max(0, ((lineCount - 1) * bounds.height() * 0.25));
return (int) Math.floor(lineSpacing + lineCount * bounds.height());
}
Need to use, thanks.
Important: You need to consider the scale factor
int textSize = 50;
int maxHeight = boundsTv.height();
while (getHeightOfMultiLineText(text, textSize, params.width) > maxHeight) {
textSize--;
}
float scaleFactor = mainActivity.getResources().getDisplayMetrics().scaledDensity;
float temp = 1.2f;
tvText.setTextSize((float) (textSize / (temp*scaleFactor)));
A little improved answer of phamducgiam. It shows text with already fitted size, not skretchs after showing. Looks ugly in editor because of starting textSize 100, but works as it should in run. Here's code:
import android.content.Context;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
public class FontFitTextView extends TextView {
public FontFitTextView(Context context) {
super(context);
}
public FontFitTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FontFitTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// start decreasing font size from 100
setTextSize(TypedValue.COMPLEX_UNIT_PX, 100);
// calculate dimensions. don't forget about padding
final float maxWidth = getWidth()-(getPaddingLeft()+getPaddingRight());
final float maxHeight = getHeight()-(getPaddingTop()+getPaddingBottom());
if (maxWidth < 1.0f || maxHeight < 1.0f) {
return;
}
CharSequence text = getText();
int lineCount = getLineCount(maxWidth, text);
float height = getHeight(lineCount);
// keep decreasing font size until it fits
while (height > maxHeight) {
final float textSize = getTextSize();
setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1));
height = getHeight(getLineCount(maxWidth, getText()));
}
// show fitted textView
requestLayout();
}
private float getHeight(int lineCount) {
return lineCount * getLineHeight() + (lineCount > 0 ? (lineCount - 1) * getPaint().getFontSpacing() : 0);
}
private int getLineCount(float maxWidth, CharSequence text) {
int lineCount = 0;
int index = 0;
final TextPaint paint = getPaint();
while (index < text.length()) {
index += paint.breakText(text, index, text.length(), true, maxWidth, null);
lineCount++;
}
return lineCount;
}
}
While I waited for a possible solution to this question, I have been experimenting and trying to figure it out.
The closest solution to this problem was based in a method from paint.
Basically paint has a method called 'breaktext' which:
public int breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)
Added in API level 1
Measure the text, stopping early if the measured width exceeds maxWidth. Return the number of chars that were measured, and if measuredWidth is not null, return in it the actual width measured.
I combine that with paint 'getTextBounds' which:
public void getTextBounds (String text, int start, int end, Rect bounds)
Added in API level 1
Return in bounds (allocated by the caller) the smallest rectangle that encloses all of the >characters, with an implied origin at (0,0).
So now I can obtain the number of chars that fit in a given width and the height of those chars.
Using a while you can keep moving the removing chars from the string you want to measure and obtain the number of lines (by using a while(index < string.length)) and multiply that by the height obtained in the getTextBounds.
Additionally you will have to add a variable height for each two lines that represent the space between the lines (which is not counted in the getTextBounds).
As an example code, the function to know the height of a multiple line text is something like this:
public int getHeightOfMultiLineText(String text,int textSize, int maxWidth) {
paint = new TextPaint();
paint.setTextSize(textSize);
int index = 0;
int linecount = 0;
while(index < text.length()) {
index += paint.breakText(text,index,text.length,true,maxWidth,null);
linecount++;
}
Rect bounds = new Rect();
paint.getTextBounds("Yy", 0, 2, bounds);
// obtain space between lines
double lineSpacing = Math.max(0,((lineCount - 1) * bounds.height()*0.25));
return (int)Math.floor(lineSpacing + lineCount * bounds.height());
Note: maxWidth variable is in pixels
Then you will have to call this method inside a while to determine what is the max font size for that height. An example code would be:
textSize = 100;
int maxHeight = 50;
while(getHeightOfMultiLineText(text,textSize,maxWidth) > maxHeight)
textSize--;
Unfortunately this was the only (as far as I know) way I was able to achieve the aspect from the images above.
Hope this can be of help to anyone trying to overcome this obstacle.
You have a pretty good solution on this problem but i took a look at this from a different point of view. I think that the logic is simpler and more understandable at least for the new android developers. The idea is based by inserting the '\n' character between the spaces of the big string. Then, i set the edited string at the textView as the displaying text. So, here is the code..
private String insertNewLineCharAtString(String str, int step){
StringBuilder sb = new StringBuilder();
int spaceCounter = 0;
String[] strArray = str.split(" ");
for(int i = 0; i < strArray.length; i++){
if(spaceCounter == step){
sb.append('\n');
sb.append(strArray[i]);
spaceCounter = 0;
}else{
sb.append(" "+strArray[i]);
}
spaceCounter++;
}
return sb.toString();
}
The parameters are simple. Str is the string that we are going to edit and the variable step defines in how many spaces the method will insert the '/n' character. Then , you just call something like this :
myTextView.setText(insertNewLineCharactersAtString(yourDisplayingStr);
hope this helps many of you that you don't want to go deeper with TextPaint , Rects, and math functions..