android full screen textview [duplicate] - android
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 several TextViews to each row. Since I don't want the TextViews 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 fits
TableRow 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());
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;
}
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;
}
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 TextViews height will also change because of height=wrap_content.
If you find any bugs, feel free to edit.
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.
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.
You can now do this without a third party library or a widget. It's built into TextView in API level 26. Add android:autoSizeTextType="uniform" to your TextView and set height to it. That's all.
https://developer.android.com/guide/topics/ui/look-and-feel/autosizing-textview.html
<?xml version="1.0" encoding="utf-8"?>
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:autoSizeTextType="uniform" />
You can also use TextViewCompat or app:autoSizeTextType="uniform" for backward compatibility.
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
Use app:autoSizeTextType="uniform" for backward compatibility because android:autoSizeTextType="uniform" only work in API Level 26 and higher.
Slight modification to onMeasure:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
refitText(this.getText().toString(), parentWidth);
this.setMeasuredDimension(parentWidth, parentHeight);
}
And binary search on refitText:
private void refitText(String text, int textWidth)
{
if (textWidth > 0)
{
int availableWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight();
int trySize = (int)maxTextSize;
int increment = ~( trySize - (int)minTextSize ) / 2;
testPaint.setTextSize(trySize);
while ((trySize > minTextSize) && (testPaint.measureText(text) > availableWidth))
{
trySize += increment;
increment = ( increment == 0 ) ? -1 : ~increment / 2;
if (trySize <= minTextSize)
{
trySize = (int)minTextSize;
break;
}
testPaint.setTextSize(trySize);
}
this.setTextSize( TypedValue.COMPLEX_UNIT_PX, trySize);
}
}
I found the following to work nicely for me. It doesn't loop and accounts for both height and width. Note that it is important to specify the PX unit when calling setTextSize on the view. Thanks to the tip in a previous post for this!
Paint paint = adjustTextSize(getPaint(), numChars, maxWidth, maxHeight);
setTextSize(TypedValue.COMPLEX_UNIT_PX,paint.getTextSize());
Here is the routine I use, passing in the getPaint() from the view. A 10 character string with a 'wide' character is used to estimate the width independent from the actual string.
private static final String text10="OOOOOOOOOO";
public static Paint adjustTextSize(Paint paint, int numCharacters, int widthPixels, int heightPixels) {
float width = paint.measureText(text10)*numCharacters/text10.length();
float newSize = (int)((widthPixels/width)*paint.getTextSize());
paint.setTextSize(newSize);
// remeasure with font size near our desired result
width = paint.measureText(text10)*numCharacters/text10.length();
newSize = (int)((widthPixels/width)*paint.getTextSize());
paint.setTextSize(newSize);
// Check height constraints
FontMetricsInt metrics = paint.getFontMetricsInt();
float textHeight = metrics.descent-metrics.ascent;
if (textHeight > heightPixels) {
newSize = (int)(newSize * (heightPixels/textHeight));
paint.setTextSize(newSize);
}
return paint;
}
Works with modification
You need to set the text view size like this because otherwise setTextSize assumes the value is in SP units:
setTextSize(TypedValue.COMPLEX_UNIT_PX, trySize);
And you needed to explicitly add this code.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
refitText(this.getText().toString(), parentWidth);
}
I had this pain in my projects for soooo long until I found this library:
compile 'me.grantland:autofittextview:0.2.+'
You just need to add the xml by your needs and it's done. For example:
<me.grantland.widget.AutofitTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:maxLines="2"
android:textSize="40sp"
autofit:minTextSize="16sp"
/>
I used a variation of Dunni solution above, but that particular code didn't work for me. In particular, when trying to use the Paint object set to have the traits of the view's Paint object, and then calling measureText(), it doesn't return the same value as directly calling the view's Paint object. Perhaps there are some differences in the way my views are set up that make the behavior different.
My solution was to directly use the view's Paint, even though there might be some performance penalties in changing the font size for the view multiple times.
I've been working on improving the excellent solution from speedplane, and came up with this. It manages the height, including setting the margin such that the text should be centered correctly vertically.
This uses the same function to get the width, as it seems to work the best, but it uses a different function to get the height, as the height isn't provided anywhere. There are some corrections that need to be made, but I figured out a way to do that, while looking pleasing to the eye.
import android.content.Context;
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 {
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());
//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 = Math.min(targetHeight,100);
float lo = 2;
final float threshold = 0.5f; // How close we have to be
Rect bounds = new Rect();
mTestPaint.set(this.getPaint());
while((hi - lo) > threshold) {
float size = (hi+lo)/2;
mTestPaint.setTextSize(size);
mTestPaint.getTextBounds(text, 0, text.length(), bounds);
if((mTestPaint.measureText(text)) >= targetWidth || (1+(2*(size+(float)bounds.top)-bounds.bottom)) >=targetHeight)
hi = size; // too big
else
lo = size; // too small
}
// Use lo so that we undershoot rather than overshoot
this.setTextSize(TypedValue.COMPLEX_UNIT_PX,(float) lo);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
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);
}
}
//Attributes
private Paint mTestPaint;
}
Google already made this feature.
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:autoSizeTextType="uniform" />
https://developer.android.com/guide/topics/ui/look-and-feel/autosizing-textview.html
Inspired by the previous posters I wanted to share my solution. It works with a scale factor which is applied to the previous font size to make it fit the available space. In addition to prevent unexpected behaviour of TextViews onDraw method, it simply draws the text on its own.
public class FontFitTextView extends TextView {
// How much of the available space should be used in percent.
private static final float MARGINHEIGHT = 0.8f;
private static final float MARGINWIDTH = 0.8f;
private Paint paint;
private int viewWidth;
private int viewHeight;
private float textHeight;
private float textWidth;
public FontFitTextView(Context c) {
this(c, null);
}
public FontFitTextView(Context c, AttributeSet attrs) {
super(c, attrs);
initComponent();
}
// Default constructor override
public FontFitTextView(Context c, AttributeSet attrs, int defStyle) {
super(c, attrs, defStyle);
initComponent();
}
private void initComponent() {
paint = new Paint();
paint.setTextSize(30);
paint.setTextAlign(Align.CENTER);
paint.setAntiAlias(true);
}
public void setFontColor(int c) {
paint.setColor(c);
}
private void calcTextSize(String s, Canvas c) {
float availableHeight = viewHeight;
float availableWidth = viewWidth;
// This value scales the old font up or down to match the available
// space.
float scale = 1.0f;
// Rectangle for measuring the text dimensions
Rect rect = new Rect();
float oldFontSize = paint.getTextSize();
// Calculate the space used with old font size
paint.getTextBounds(s, 0, s.length(), rect);
textWidth = rect.width();
textHeight = rect.height();
// find scale-value to fit the text horizontally
float scaleWidth = 1f;
if (textWidth > 0.0f) {
scaleWidth = (availableWidth) / textWidth * MARGINWIDTH;
}
// find scale-value to fit the text vertically
float scaleHeight = 1f;
if (textHeight > 0.0f) {
scaleHeight = (availableHeight) / textHeight * MARGINHEIGHT;
}
// We are always limited by the smaller one
if (scaleWidth < scaleHeight) {
scale = scaleWidth;
} else {
scale = scaleHeight;
}
// We apply the scale to the old font size to make it bigger or smaller
float newFontSize = (oldFontSize * scale);
paint.setTextSize(newFontSize);
}
/**
* Calculates the origin on the Y-Axis (width) for the text in this view.
*
* #return
*/
private float calcStartDrawingPosX() {
float left = getMeasuredWidth();
float centerY = left - (viewWidth / 2);
return centerY;
}
/**
* Calculates the origin on the Y-Axis (height) for the text in this view.
*
* #return
*/
private float calcStartDrawingPosY() {
float bottom = getMeasuredHeight();
// The paint only centers horizontally, origin on the Y-Axis stays at
// the bottom, thus we have to lift the origin additionally by the
// height of the font.
float centerX = bottom - (viewHeight / 2) + (textHeight / 2);
return centerX;
}
#Override
protected void onDraw(Canvas canvas) {
String text = getText().toString();
if (text.length() > 0) {
calcTextSize(text, canvas);
canvas.drawText(text, calcStartDrawingPosX(),
calcStartDrawingPosY(), paint);
}
};
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
viewWidth = w;
viewHeight = h;
super.onSizeChanged(w, h, oldw, oldh);
}
}
/* get your context */
Context c = getActivity().getApplicationContext();
LinearLayout l = new LinearLayout(c);
l.setOrientation(LinearLayout.VERTICAL);
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 0);
l.setLayoutParams(params);
l.setBackgroundResource(R.drawable.border);
TextView tv=new TextView(c);
tv.setText(" your text here");
/* set typeface if needed */
Typeface tf = Typeface.createFromAsset(c.getAssets(),"fonts/VERDANA.TTF");
tv.setTypeface(tf);
// LayoutParams lp = new LayoutParams();
tv.setTextColor(Color.parseColor("#282828"));
tv.setGravity(Gravity.CENTER | Gravity.BOTTOM);
// tv.setLayoutParams(lp);
tv.setTextSize(20);
l.addView(tv);
return l;
This should be a simple solution:
public void correctWidth(TextView textView, int desiredWidth)
{
Paint paint = new Paint();
Rect bounds = new Rect();
paint.setTypeface(textView.getTypeface());
float textSize = textView.getTextSize();
paint.setTextSize(textSize);
String text = textView.getText().toString();
paint.getTextBounds(text, 0, text.length(), bounds);
while (bounds.width() > desiredWidth)
{
textSize--;
paint.setTextSize(textSize);
paint.getTextBounds(text, 0, text.length(), bounds);
}
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
Extend TextView and override onDraw with the code below. It will keep text aspect ratio but size it to fill the space. You could easily modify code to stretch if necessary.
#Override
protected void onDraw(#NonNull Canvas canvas) {
TextPaint textPaint = getPaint();
textPaint.setColor(getCurrentTextColor());
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.drawableState = getDrawableState();
String text = getText().toString();
float desiredWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2;
float desiredHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2;
float textSize = textPaint.getTextSize();
for (int i = 0; i < 10; i++) {
textPaint.getTextBounds(text, 0, text.length(), rect);
float width = rect.width();
float height = rect.height();
float deltaWidth = width - desiredWidth;
float deltaHeight = height - desiredHeight;
boolean fitsWidth = deltaWidth <= 0;
boolean fitsHeight = deltaHeight <= 0;
if ((fitsWidth && Math.abs(deltaHeight) < 1.0)
|| (fitsHeight && Math.abs(deltaWidth) < 1.0)) {
// close enough
break;
}
float adjustX = desiredWidth / width;
float adjustY = desiredHeight / height;
textSize = textSize * (adjustY < adjustX ? adjustY : adjustX);
// adjust text size
textPaint.setTextSize(textSize);
}
float x = desiredWidth / 2f;
float y = desiredHeight / 2f - rect.top - rect.height() / 2f;
canvas.drawText(text, x, y, textPaint);
}
I wrote a short helper class that makes a textview fit within a certain width and adds ellipsize "..." at the end if the minimum textsize cannot be achieved.
Keep in mind that it only makes the text smaller until it fits or until the minimum text size is reached. To test with large sizes, set the textsize to a large number before calling the help method.
It takes Pixels, so if you are using values from dimen, you can call it like this:
float minTextSizePx = getResources().getDimensionPixelSize(R.dimen.min_text_size);
float maxTextWidthPx = getResources().getDimensionPixelSize(R.dimen.max_text_width);
WidgetUtils.fitText(textView, text, minTextSizePx, maxTextWidthPx);
This is the class I use:
public class WidgetUtils {
public static void fitText(TextView textView, String text, float minTextSizePx, float maxWidthPx) {
textView.setEllipsize(null);
int size = (int)textView.getTextSize();
while (true) {
Rect bounds = new Rect();
Paint textPaint = textView.getPaint();
textPaint.getTextBounds(text, 0, text.length(), bounds);
if(bounds.width() < maxWidthPx){
break;
}
if (size <= minTextSizePx) {
textView.setEllipsize(TextUtils.TruncateAt.END);
break;
}
size -= 1;
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
}
}
}
If a tranformation like allCaps is set, speedplane's approach is buggy. I fixed it, resulting in the following code (sorry, my reputation does not allow me to add this as a comment to speedplane's solution):
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 (getTransformationMethod() != null) {
text = getTransformationMethod().getTransformation(text, this).toString();
}
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;
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;
}
I don't known this is correct way or not bt its working ...take your view and check OnGlobalLayoutListener() and get textview linecount then set textSize.
yourView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (textView.getLineCount()>=3) {
textView.setTextSize(20);
}else{
//add somthing
}
}
});
Its very simple few line code..
In my case using app:autoSize was not solving all cases, for example it doesn't prevent word breaking
This is what I ended up using, it will resize down the text so that there are no word breaks on multiple lines
/**
* Resizes down the text size so that there are no word breaks
*/
class AutoFitTextView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private val paint = Paint()
private val bounds = Rect()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var shouldResize = false
paint.typeface = typeface
var textSize = textSize
paint.textSize = textSize
val biggestWord: String = text.split(" ").maxByOrNull { it.count() } ?: return
// Set bounds equal to the biggest word bounds
paint.getTextBounds(biggestWord, 0, biggestWord.length, bounds)
// Iterate to reduce the text size so that it makes the biggest word fit the line
while ((bounds.width() + paddingStart + paddingEnd + paint.fontSpacing) > measuredWidth) {
textSize--
paint.textSize = textSize
paint.getTextBounds(biggestWord, 0, biggestWord.length, bounds)
shouldResize = true
}
if (shouldResize) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
}
}
}
Related
Overlay TextureView with circle border View
Android newbie here but I'm trying to overlay a transparent circle view over a TextureView stream from the device camera, however no matter what I do I cannot get the overlay view to appear on top of the TextureView. I have the TextureView implemented as such: package org.tensorflow.demo; import android.content.Context; import android.util.AttributeSet; import android.view.TextureView; /** * A {#link TextureView} that can be adjusted to a specified aspect ratio. */ public class AutoFitTextureView extends TextureView { private int ratioWidth = 0; private int ratioHeight = 0; public AutoFitTextureView(final Context context) { this(context, null); } public AutoFitTextureView(final Context context, final AttributeSet attrs) { this(context, attrs, 0); } public AutoFitTextureView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); } /** * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio * calculated from the parameters. Note that the actual sizes of parameters don't matter, that * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. * * #param width Relative horizontal size * #param height Relative vertical size */ public void setAspectRatio(final int width, final int height) { if (width < 0 || height < 0) { throw new IllegalArgumentException("Size cannot be negative."); } ratioWidth = width; ratioHeight = height; requestLayout(); } #Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = MeasureSpec.getSize(widthMeasureSpec); final int height = MeasureSpec.getSize(heightMeasureSpec); if (0 == ratioWidth || 0 == ratioHeight) { setMeasuredDimension(width, height); } else { if (width < height * ratioWidth / ratioHeight) { setMeasuredDimension(width, width * ratioHeight / ratioWidth); } else { setMeasuredDimension(height * ratioWidth / ratioHeight, height); } } } } And my overlay currently looks like this (circle colour current set to black for debugging: package org.tensorflow.demo; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; public class OverlayView extends View { private final Paint transparentPaint; public OverlayView(final Context context, final AttributeSet attrs) { super(context, attrs); transparentPaint = new Paint(); transparentPaint.setColor(getResources().getColor(android.R.color.black)); } #Override public void onDraw(final Canvas canvas) { int w = getWidth(); int h = getHeight(); int pl = getPaddingLeft(); int pr = getPaddingRight(); int pt = getPaddingTop(); int pb = getPaddingBottom(); int usableWidth = w - (pl + pr); int usableHeight = h - (pt + pb); int radius = Math.min(usableWidth, usableHeight) / 2; int cx = pl + (usableWidth / 2); int cy = pt + (usableHeight / 2); canvas.drawCircle(cx, cy, radius, transparentPaint); } } Lastly I've got my layout xml as such: <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <org.tensorflow.demo.AutoFitTextureView android:id="#+id/texture" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <org.tensorflow.demo.OverlayView android:id="#+id/overlay" android:layout_width="match_parent" android:layout_height="match_parent" /> <org.tensorflow.demo.RecognitionScoreView android:id="#+id/results" android:layout_width="match_parent" android:layout_height="112dp" /> </FrameLayout>
Ended up solving this using a PorterDuff.Mode.SRC_OUT to cut out a circle from a full screen rectangle: protected void createWindowFrame() { int w = getWidth(); int h = getHeight(); windowFrame = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas osCanvas = new Canvas(windowFrame); RectF outerRectangle = new RectF(0, 0, getWidth(), getHeight()); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.parseColor("#4885ed")); osCanvas.drawRect(outerRectangle, paint); paint.setColor(Color.TRANSPARENT); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)); int pl = getPaddingLeft(); int pr = getPaddingRight(); int pt = getPaddingTop(); int pb = getPaddingBottom(); int usableWidth = w - (pl + pr); int usableHeight = h - (pt + pb); int radius = Math.min(usableWidth, usableHeight) / 2; int cx = pl + (usableWidth / 2); int cy = pt + (usableHeight / 2); osCanvas.drawCircle(cx, cy, radius, paint); }
strange behavior in custom view
I want to make a view that it's width and height is always equal. when user define a specfied height and width in xml, it chooses a smaller one. for example: <com.mmmmar.ControlView android:layout_width="100dp" android:layout_height="200dp" android:background="#f00" /> it run well. However, when I make width lager than height, It runs unexpectedly. green block is something I draw on this view, red block is the background I set in xml to indicate the area of view. <com.mmmmar.ControlView android:layout_width="200dp" android:layout_height="100dp" android:background="#f00" /> here is my code public class ControlView extends View { private final static int DEFAULT_SIZE = 200; private int mDrawRadius; private Paint mPaint; public ControlView(Context context, #Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // configure paint Paint paint = new Paint(); paint.setAntiAlias(true); paint.setDither(true); mPaint = paint; } #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int size = dp2px(DEFAULT_SIZE, getContext()); int width = calculateSpec(widthMeasureSpec, size); int height = calculateSpec(heightMeasureSpec, size); int radius = Math.min(width, height); // width should equal height. setMeasuredDimension(radius, radius); mDrawRadius = radius; } #Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = mPaint; paint.setColor(Color.GREEN); paint.setStyle(Paint.Style.FILL); canvas.drawRect(0, 0, mDrawRadius, mDrawRadius, paint); } public int dp2px(float dp, Context context) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } public int calculateSpec(int measureSpec, int defaultSize) { int mode = View.MeasureSpec.getMode(measureSpec); int size = View.MeasureSpec.getSize(measureSpec); int realSize; if (mode == View.MeasureSpec.EXACTLY) { realSize = size; } else { // For Mode :UNSPECIFIED and AT_MOST realSize = Math.min(size, defaultSize); } return realSize; } }
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int size = dp2px(DEFAULT_SIZE, getContext()); int width = calculateSpec(widthMeasureSpec, size); int height = calculateSpec(heightMeasureSpec, size); int radius = Math.min(width, height); // width should equal height. setMeasuredDimension(radius, radius); mDrawRadius = radius; } You do not need to set measured dimension after calculating. #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Your logic comes here to pick smallest border super.onMeasure(smallest_border_that_you_pick,smallest_border_that_you_pick) } Would be fix your problem.
How to set min text size in custom edit text?
I am using this custom EditText which resizes the text inside my EditText automatically. It works fine, however, the text gets way too tiny before it changes line. how do i set my min text size? so that it changes line only when text size reaches 12sp public class FontFitTextView extends EditText { // 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); } } } <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" />
You only need to change the value from lo // adjust text size using binary search for efficiency float hi = defaultTextSize; float lo = 2; For e.g.: // adjust text size using binary search for efficiency float minTextSize = defaultTextSize / 2; float hi = defaultTextSize; float lo = minTextSize;
Customize a ProgressBar to become a Thermometer
How to customize a ProgressBar to look like a Thermometer ? with the possibility to change color. My suggestion was to rotate the progressBar 90° to become vertical then have it overlay an image of an empty Thermometer but it's bad and messy solution. I Think the best will be to either to extends View or ProgressBar class and customize the draw method but I have no idea how to draw Thermometer, any Help would be appreciated.
I created something like this for a project package com.janslab.thermometer.widgets; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.Scroller; import com.janslab.thermometer.R; public class DummyThermometer extends View { private Paint mInnerCirclePaint; private Paint mOuterCirclePaint; private Paint mFirstOuterCirclePaint; //thermometer arc paint private Paint mFirstOuterArcPaint; //thermometer lines paints private Paint mInnerLinePaint; private Paint mOuterLinePaint; private Paint mFirstOuterLinePaint; //thermometer radii private int mOuterRadius; private int mInnerRadius; private int mFirstOuterRadius; //thermometer colors private int mThermometerColor = Color.rgb(200, 115, 205); //circles and lines variables private float mLastCellWidth; private int mStageHeight; private float mCellWidth; private float mStartCenterY; //center of first cell private float mEndCenterY; //center of last cell private float mStageCenterX; private float mXOffset; private float mYOffset; // I 1st Cell I 2nd Cell I 3rd Cell I private static final int NUMBER_OF_CELLS = 3; //three cells in all ie.stageHeight divided into 3 equal cells //animation variables private float mIncrementalTempValue; private boolean mIsAnimating; private Animator mAnimator; public DummyThermometer(Context context) { this(context, null); } public DummyThermometer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DummyThermometer(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); if (attrs != null) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Thermometer, defStyle, 0); mThermometerColor = a.getColor(R.styleable.Thermometer_therm_color, mThermometerColor); a.recycle(); } init(); } private void init() { mInnerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mInnerCirclePaint.setColor(mThermometerColor); mInnerCirclePaint.setStyle(Paint.Style.FILL); mInnerCirclePaint.setStrokeWidth(17f); mOuterCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mOuterCirclePaint.setColor(Color.WHITE); mOuterCirclePaint.setStyle(Paint.Style.FILL); mOuterCirclePaint.setStrokeWidth(32f); mFirstOuterCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFirstOuterCirclePaint.setColor(mThermometerColor); mFirstOuterCirclePaint.setStyle(Paint.Style.FILL); mFirstOuterCirclePaint.setStrokeWidth(60f); mFirstOuterArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFirstOuterArcPaint.setColor(mThermometerColor); mFirstOuterArcPaint.setStyle(Paint.Style.STROKE); mFirstOuterArcPaint.setStrokeWidth(30f); mInnerLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mInnerLinePaint.setColor(mThermometerColor); mInnerLinePaint.setStyle(Paint.Style.FILL); mInnerLinePaint.setStrokeWidth(17f); mOuterLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mOuterLinePaint.setColor(Color.WHITE); mOuterLinePaint.setStyle(Paint.Style.FILL); mFirstOuterLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFirstOuterLinePaint.setColor(mThermometerColor); mFirstOuterLinePaint.setStyle(Paint.Style.FILL); } #Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mStageCenterX = getWidth() / 2; mStageHeight = getHeight(); mCellWidth = mStageHeight / NUMBER_OF_CELLS; //center of first cell mStartCenterY = mCellWidth / 2; //move to 3rd cell mLastCellWidth = (NUMBER_OF_CELLS * mCellWidth); //center of last(3rd) cell mEndCenterY = mLastCellWidth - (mCellWidth / 2); // mOuterRadius is 1/4 of mCellWidth mOuterRadius = (int) (0.25 * mCellWidth); mInnerRadius = (int) (0.656 * mOuterRadius); mFirstOuterRadius = (int) (1.344 * mOuterRadius); mFirstOuterLinePaint.setStrokeWidth(mFirstOuterRadius); mOuterLinePaint.setStrokeWidth(mFirstOuterRadius / 2); mFirstOuterArcPaint.setStrokeWidth(mFirstOuterRadius / 4); mXOffset = mFirstOuterRadius / 4; mXOffset = mXOffset / 2; //get the d/f btn firstOuterLine and innerAnimatedline mYOffset = (mStartCenterY + (float) 0.875 * mOuterRadius) - (mStartCenterY + mInnerRadius); mYOffset = mYOffset / 2; } #Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawFirstOuterCircle(canvas); drawOuterCircle(canvas); drawInnerCircle(canvas); drawFirstOuterLine(canvas); drawOuterLine(canvas); animateInnerLine(canvas); drawFirstOuterCornerArc(canvas); } #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //take care of paddingTop and paddingBottom int paddingY = getPaddingBottom() + getPaddingTop(); //get height and width int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); height += paddingY; setMeasuredDimension(width, height); } private void drawInnerCircle(Canvas canvas) { drawCircle(canvas, mInnerRadius, mInnerCirclePaint); } private void drawOuterCircle(Canvas canvas) { drawCircle(canvas, mOuterRadius, mOuterCirclePaint); } private void drawFirstOuterCircle(Canvas canvas) { drawCircle(canvas, mFirstOuterRadius, mFirstOuterCirclePaint); } private void drawCircle(Canvas canvas, float radius, Paint paint) { canvas.drawCircle(mStageCenterX, mEndCenterY, radius, paint); } private void drawOuterLine(Canvas canvas) { float startY = mEndCenterY - (float) (0.875 * mOuterRadius); float stopY = mStartCenterY + (float) (0.875 * mOuterRadius); drawLine(canvas, startY, stopY, mOuterLinePaint); } private void drawFirstOuterLine(Canvas canvas) { float startY = mEndCenterY - (float) (0.875 * mFirstOuterRadius); float stopY = mStartCenterY + (float) (0.875 * mOuterRadius); drawLine(canvas, startY, stopY, mFirstOuterLinePaint); } private void drawLine(Canvas canvas, float startY, float stopY, Paint paint) { canvas.drawLine(mStageCenterX, startY, mStageCenterX, stopY, paint); } //simulate temperature measurement for now private void animateInnerLine(Canvas canvas) { if (mAnimator == null) measureTemperature(); if (!mIsAnimating) { mIncrementalTempValue = mEndCenterY + (float) (0.875 * mInnerRadius); mIsAnimating = true; } else { mIncrementalTempValue = mEndCenterY + (float) (0.875 * mInnerRadius) - mIncrementalTempValue; } if (mIncrementalTempValue > mStartCenterY + mInnerRadius) { float startY = mEndCenterY + (float) (0.875 * mInnerRadius); drawLine(canvas, startY, mIncrementalTempValue, mInnerCirclePaint); } else { float startY = mEndCenterY + (float) (0.875 * mInnerRadius); float stopY = mStartCenterY + mInnerRadius; drawLine(canvas, startY, stopY, mInnerCirclePaint); mIsAnimating = false; stopMeasurement(); } } private void drawFirstOuterCornerArc(Canvas canvas) { float y = mStartCenterY - (float) (0.875 * mFirstOuterRadius); RectF rectF = new RectF(mStageCenterX - mFirstOuterRadius / 2 + mXOffset, y + mFirstOuterRadius, mStageCenterX + mFirstOuterRadius / 2 - mXOffset, y + (2 * mFirstOuterRadius) + mYOffset); canvas.drawArc(rectF, -180, 180, false, mFirstOuterArcPaint); } public void setThermometerColor(int thermometerColor) { this.mThermometerColor = thermometerColor; mInnerCirclePaint.setColor(mThermometerColor); mFirstOuterCirclePaint.setColor(mThermometerColor); mFirstOuterArcPaint.setColor(mThermometerColor); mInnerLinePaint.setColor(mThermometerColor); mFirstOuterLinePaint.setColor(mThermometerColor); invalidate(); } //simulate temperature measurement for now private void measureTemperature() { mAnimator = new Animator(); mAnimator.start(); } private class Animator implements Runnable { private Scroller mScroller; private final static int ANIM_START_DELAY = 1000; private final static int ANIM_DURATION = 4000; private boolean mRestartAnimation = false; public Animator() { mScroller = new Scroller(getContext(), new AccelerateDecelerateInterpolator()); } public void run() { if (mAnimator != this) return; if (mRestartAnimation) { int startY = (int) (mStartCenterY - (float) (0.875 * mInnerRadius)); int dy = (int) (mEndCenterY + mInnerRadius); mScroller.startScroll(0, startY, 0, dy, ANIM_DURATION); mRestartAnimation = false; } boolean isScrolling = mScroller.computeScrollOffset(); mIncrementalTempValue = mScroller.getCurrY(); if (isScrolling) { invalidate(); post(this); } else { stop(); } } public void start() { mRestartAnimation = true; postDelayed(this, ANIM_START_DELAY); } public void stop() { removeCallbacks(this); mAnimator = null; } } private void stopMeasurement() { if (mAnimator != null) mAnimator.stop(); } #Override protected void onAttachedToWindow() { super.onAttachedToWindow(); measureTemperature(); } #Override protected void onDetachedFromWindow() { stopMeasurement(); super.onDetachedFromWindow(); } #Override public void setVisibility(int visibility) { super.setVisibility(visibility); switch (visibility) { case View.VISIBLE: measureTemperature(); break; default: stopMeasurement(); break; } } } attrs.xml file <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="Thermometer"> <attr name="therm_color" format="color" /> </declare-styleable> </resources>
First I would provide 2 setters, one for color and one for the temperature value, normalized from 0 ... 1, where 0 means no visible bar, and 1 means a fully visible bar. public void setColor(int color) { mColor = color; invalidate(); // important, this triggers onDraw } public void setValue(float value) { mValue = -(value - 1); invalidate(); // important, this triggers onDraw } Notice for value, I reverse the value, since we draw the bar from bottom up, instead from top down. It makes sense in the canvas.drawRect method. If your CustomView may have custom sizes, set your size of the progressBar (I refer to the inner bar as progressBar) in onSizeChanged, as this gets called when the View has changed it's size. If it is a fixed size, you can just provide those values statically in an init function or the constructor. #Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mProgressRect = new Rect( /*your bar left offset relative to base bitmap*/, /*your bar top offset relative to base bitmap*/, /*your bar total width*/, /*your max bar height*/ ); } Then in ondraw, take these values into account and draw accordingly. First draw the Bitmap, depending on your selected color (I would provide the thermometer base as a Bitmap, as long as it does not have to be completely dynamically drawn (special requirements) Then draw the progress bar, with an height based on mValue * totalHeight of the bar, using the color provided in the setter. For example: #Override protected void onDraw(Canvas canvas) { // draw your thermometer base, bitmap based on color value canvas.drawBitmap( /*your base thermometer bitmap here*/ ); // draw the "progress" canvas.drawRect(mProgressRect.left, mProgressRect.top + (mValue * mProgressRect.bottom - mProgressRect.top), mProgressRect.right, mProgressRect.bottom, mPaint); } Hope that helps. P.S.: If you want to have the thermometer base image also dynamically drawn, it's a slightly different story, it would involve creating a path first and draw it with a Paint object, instead of drawing the bitmap. EDIT: Even better, if you want a simple solution for the "roundness" of the bar, draw a line instead a rect. Define a line paint object like this: mPaint = new Paint(); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeWidth(20); // thickness of your bar then in onDraw, instead drawRect: // draw the "progress" canvas.drawLine(mProgressRect.left, mProgressRect.top + (mValue * mProgressRect.bottom - mProgressRect.top), mProgressRect.left, mProgressRect.bottom, mPaint); Be sure to adjust your mProgressRectaccordingly.
How to adjust text font size to fit textview
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 several TextViews to each row. Since I don't want the TextViews 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 fits TableRow 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());
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; }
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; }
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 TextViews height will also change because of height=wrap_content. If you find any bugs, feel free to edit.
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.
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.
You can now do this without a third party library or a widget. It's built into TextView in API level 26. Add android:autoSizeTextType="uniform" to your TextView and set height to it. That's all. https://developer.android.com/guide/topics/ui/look-and-feel/autosizing-textview.html <?xml version="1.0" encoding="utf-8"?> <TextView android:layout_width="match_parent" android:layout_height="200dp" android:autoSizeTextType="uniform" /> You can also use TextViewCompat or app:autoSizeTextType="uniform" for backward compatibility.
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
Use app:autoSizeTextType="uniform" for backward compatibility because android:autoSizeTextType="uniform" only work in API Level 26 and higher.
Slight modification to onMeasure: #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); refitText(this.getText().toString(), parentWidth); this.setMeasuredDimension(parentWidth, parentHeight); } And binary search on refitText: private void refitText(String text, int textWidth) { if (textWidth > 0) { int availableWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight(); int trySize = (int)maxTextSize; int increment = ~( trySize - (int)minTextSize ) / 2; testPaint.setTextSize(trySize); while ((trySize > minTextSize) && (testPaint.measureText(text) > availableWidth)) { trySize += increment; increment = ( increment == 0 ) ? -1 : ~increment / 2; if (trySize <= minTextSize) { trySize = (int)minTextSize; break; } testPaint.setTextSize(trySize); } this.setTextSize( TypedValue.COMPLEX_UNIT_PX, trySize); } }
I found the following to work nicely for me. It doesn't loop and accounts for both height and width. Note that it is important to specify the PX unit when calling setTextSize on the view. Thanks to the tip in a previous post for this! Paint paint = adjustTextSize(getPaint(), numChars, maxWidth, maxHeight); setTextSize(TypedValue.COMPLEX_UNIT_PX,paint.getTextSize()); Here is the routine I use, passing in the getPaint() from the view. A 10 character string with a 'wide' character is used to estimate the width independent from the actual string. private static final String text10="OOOOOOOOOO"; public static Paint adjustTextSize(Paint paint, int numCharacters, int widthPixels, int heightPixels) { float width = paint.measureText(text10)*numCharacters/text10.length(); float newSize = (int)((widthPixels/width)*paint.getTextSize()); paint.setTextSize(newSize); // remeasure with font size near our desired result width = paint.measureText(text10)*numCharacters/text10.length(); newSize = (int)((widthPixels/width)*paint.getTextSize()); paint.setTextSize(newSize); // Check height constraints FontMetricsInt metrics = paint.getFontMetricsInt(); float textHeight = metrics.descent-metrics.ascent; if (textHeight > heightPixels) { newSize = (int)(newSize * (heightPixels/textHeight)); paint.setTextSize(newSize); } return paint; }
Works with modification You need to set the text view size like this because otherwise setTextSize assumes the value is in SP units: setTextSize(TypedValue.COMPLEX_UNIT_PX, trySize); And you needed to explicitly add this code. #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); refitText(this.getText().toString(), parentWidth); }
I had this pain in my projects for soooo long until I found this library: compile 'me.grantland:autofittextview:0.2.+' You just need to add the xml by your needs and it's done. For example: <me.grantland.widget.AutofitTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true" android:maxLines="2" android:textSize="40sp" autofit:minTextSize="16sp" />
I used a variation of Dunni solution above, but that particular code didn't work for me. In particular, when trying to use the Paint object set to have the traits of the view's Paint object, and then calling measureText(), it doesn't return the same value as directly calling the view's Paint object. Perhaps there are some differences in the way my views are set up that make the behavior different. My solution was to directly use the view's Paint, even though there might be some performance penalties in changing the font size for the view multiple times.
I've been working on improving the excellent solution from speedplane, and came up with this. It manages the height, including setting the margin such that the text should be centered correctly vertically. This uses the same function to get the width, as it seems to work the best, but it uses a different function to get the height, as the height isn't provided anywhere. There are some corrections that need to be made, but I figured out a way to do that, while looking pleasing to the eye. import android.content.Context; 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 { 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()); //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 = Math.min(targetHeight,100); float lo = 2; final float threshold = 0.5f; // How close we have to be Rect bounds = new Rect(); mTestPaint.set(this.getPaint()); while((hi - lo) > threshold) { float size = (hi+lo)/2; mTestPaint.setTextSize(size); mTestPaint.getTextBounds(text, 0, text.length(), bounds); if((mTestPaint.measureText(text)) >= targetWidth || (1+(2*(size+(float)bounds.top)-bounds.bottom)) >=targetHeight) hi = size; // too big else lo = size; // too small } // Use lo so that we undershoot rather than overshoot this.setTextSize(TypedValue.COMPLEX_UNIT_PX,(float) lo); } #Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); 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); } } //Attributes private Paint mTestPaint; }
Google already made this feature. <TextView android:layout_width="match_parent" android:layout_height="200dp" android:autoSizeTextType="uniform" /> https://developer.android.com/guide/topics/ui/look-and-feel/autosizing-textview.html
Inspired by the previous posters I wanted to share my solution. It works with a scale factor which is applied to the previous font size to make it fit the available space. In addition to prevent unexpected behaviour of TextViews onDraw method, it simply draws the text on its own. public class FontFitTextView extends TextView { // How much of the available space should be used in percent. private static final float MARGINHEIGHT = 0.8f; private static final float MARGINWIDTH = 0.8f; private Paint paint; private int viewWidth; private int viewHeight; private float textHeight; private float textWidth; public FontFitTextView(Context c) { this(c, null); } public FontFitTextView(Context c, AttributeSet attrs) { super(c, attrs); initComponent(); } // Default constructor override public FontFitTextView(Context c, AttributeSet attrs, int defStyle) { super(c, attrs, defStyle); initComponent(); } private void initComponent() { paint = new Paint(); paint.setTextSize(30); paint.setTextAlign(Align.CENTER); paint.setAntiAlias(true); } public void setFontColor(int c) { paint.setColor(c); } private void calcTextSize(String s, Canvas c) { float availableHeight = viewHeight; float availableWidth = viewWidth; // This value scales the old font up or down to match the available // space. float scale = 1.0f; // Rectangle for measuring the text dimensions Rect rect = new Rect(); float oldFontSize = paint.getTextSize(); // Calculate the space used with old font size paint.getTextBounds(s, 0, s.length(), rect); textWidth = rect.width(); textHeight = rect.height(); // find scale-value to fit the text horizontally float scaleWidth = 1f; if (textWidth > 0.0f) { scaleWidth = (availableWidth) / textWidth * MARGINWIDTH; } // find scale-value to fit the text vertically float scaleHeight = 1f; if (textHeight > 0.0f) { scaleHeight = (availableHeight) / textHeight * MARGINHEIGHT; } // We are always limited by the smaller one if (scaleWidth < scaleHeight) { scale = scaleWidth; } else { scale = scaleHeight; } // We apply the scale to the old font size to make it bigger or smaller float newFontSize = (oldFontSize * scale); paint.setTextSize(newFontSize); } /** * Calculates the origin on the Y-Axis (width) for the text in this view. * * #return */ private float calcStartDrawingPosX() { float left = getMeasuredWidth(); float centerY = left - (viewWidth / 2); return centerY; } /** * Calculates the origin on the Y-Axis (height) for the text in this view. * * #return */ private float calcStartDrawingPosY() { float bottom = getMeasuredHeight(); // The paint only centers horizontally, origin on the Y-Axis stays at // the bottom, thus we have to lift the origin additionally by the // height of the font. float centerX = bottom - (viewHeight / 2) + (textHeight / 2); return centerX; } #Override protected void onDraw(Canvas canvas) { String text = getText().toString(); if (text.length() > 0) { calcTextSize(text, canvas); canvas.drawText(text, calcStartDrawingPosX(), calcStartDrawingPosY(), paint); } }; #Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { viewWidth = w; viewHeight = h; super.onSizeChanged(w, h, oldw, oldh); } }
/* get your context */ Context c = getActivity().getApplicationContext(); LinearLayout l = new LinearLayout(c); l.setOrientation(LinearLayout.VERTICAL); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 0); l.setLayoutParams(params); l.setBackgroundResource(R.drawable.border); TextView tv=new TextView(c); tv.setText(" your text here"); /* set typeface if needed */ Typeface tf = Typeface.createFromAsset(c.getAssets(),"fonts/VERDANA.TTF"); tv.setTypeface(tf); // LayoutParams lp = new LayoutParams(); tv.setTextColor(Color.parseColor("#282828")); tv.setGravity(Gravity.CENTER | Gravity.BOTTOM); // tv.setLayoutParams(lp); tv.setTextSize(20); l.addView(tv); return l;
This should be a simple solution: public void correctWidth(TextView textView, int desiredWidth) { Paint paint = new Paint(); Rect bounds = new Rect(); paint.setTypeface(textView.getTypeface()); float textSize = textView.getTextSize(); paint.setTextSize(textSize); String text = textView.getText().toString(); paint.getTextBounds(text, 0, text.length(), bounds); while (bounds.width() > desiredWidth) { textSize--; paint.setTextSize(textSize); paint.getTextBounds(text, 0, text.length(), bounds); } textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); }
Extend TextView and override onDraw with the code below. It will keep text aspect ratio but size it to fill the space. You could easily modify code to stretch if necessary. #Override protected void onDraw(#NonNull Canvas canvas) { TextPaint textPaint = getPaint(); textPaint.setColor(getCurrentTextColor()); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.drawableState = getDrawableState(); String text = getText().toString(); float desiredWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2; float desiredHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2; float textSize = textPaint.getTextSize(); for (int i = 0; i < 10; i++) { textPaint.getTextBounds(text, 0, text.length(), rect); float width = rect.width(); float height = rect.height(); float deltaWidth = width - desiredWidth; float deltaHeight = height - desiredHeight; boolean fitsWidth = deltaWidth <= 0; boolean fitsHeight = deltaHeight <= 0; if ((fitsWidth && Math.abs(deltaHeight) < 1.0) || (fitsHeight && Math.abs(deltaWidth) < 1.0)) { // close enough break; } float adjustX = desiredWidth / width; float adjustY = desiredHeight / height; textSize = textSize * (adjustY < adjustX ? adjustY : adjustX); // adjust text size textPaint.setTextSize(textSize); } float x = desiredWidth / 2f; float y = desiredHeight / 2f - rect.top - rect.height() / 2f; canvas.drawText(text, x, y, textPaint); }
I wrote a short helper class that makes a textview fit within a certain width and adds ellipsize "..." at the end if the minimum textsize cannot be achieved. Keep in mind that it only makes the text smaller until it fits or until the minimum text size is reached. To test with large sizes, set the textsize to a large number before calling the help method. It takes Pixels, so if you are using values from dimen, you can call it like this: float minTextSizePx = getResources().getDimensionPixelSize(R.dimen.min_text_size); float maxTextWidthPx = getResources().getDimensionPixelSize(R.dimen.max_text_width); WidgetUtils.fitText(textView, text, minTextSizePx, maxTextWidthPx); This is the class I use: public class WidgetUtils { public static void fitText(TextView textView, String text, float minTextSizePx, float maxWidthPx) { textView.setEllipsize(null); int size = (int)textView.getTextSize(); while (true) { Rect bounds = new Rect(); Paint textPaint = textView.getPaint(); textPaint.getTextBounds(text, 0, text.length(), bounds); if(bounds.width() < maxWidthPx){ break; } if (size <= minTextSizePx) { textView.setEllipsize(TextUtils.TruncateAt.END); break; } size -= 1; textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); } } }
If a tranformation like allCaps is set, speedplane's approach is buggy. I fixed it, resulting in the following code (sorry, my reputation does not allow me to add this as a comment to speedplane's solution): 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 (getTransformationMethod() != null) { text = getTransformationMethod().getTransformation(text, this).toString(); } 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; 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; }
I don't known this is correct way or not bt its working ...take your view and check OnGlobalLayoutListener() and get textview linecount then set textSize. yourView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { #Override public void onGlobalLayout() { if (textView.getLineCount()>=3) { textView.setTextSize(20); }else{ //add somthing } } }); Its very simple few line code..
In my case using app:autoSize was not solving all cases, for example it doesn't prevent word breaking This is what I ended up using, it will resize down the text so that there are no word breaks on multiple lines /** * Resizes down the text size so that there are no word breaks */ class AutoFitTextView #JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr) { private val paint = Paint() private val bounds = Rect() override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) var shouldResize = false paint.typeface = typeface var textSize = textSize paint.textSize = textSize val biggestWord: String = text.split(" ").maxByOrNull { it.count() } ?: return // Set bounds equal to the biggest word bounds paint.getTextBounds(biggestWord, 0, biggestWord.length, bounds) // Iterate to reduce the text size so that it makes the biggest word fit the line while ((bounds.width() + paddingStart + paddingEnd + paint.fontSpacing) > measuredWidth) { textSize-- paint.textSize = textSize paint.getTextBounds(biggestWord, 0, biggestWord.length, bounds) shouldResize = true } if (shouldResize) { setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) } } }