Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Auto Scale TextView Text to Fit within Bounds

I'm looking for an optimal way to resize wrapping text in a TextView so that it will fit within its getHeight and getWidth bounds. I'm not simply looking for a way to wrap the text- I want to make sure it both wraps and is small enough to fit entirely on the screen.

I've seen a few cases on StackOverflow where auto resizing was needed, but they are either very special cases with hack solutions, have no solution, or involve re-drawing the TextView recursively until it is small enough (which is memory intense and forces the user to watch the text shrink step-by-step with every recursion).

But I'm sure somebody out there has found a good solution that doesn't involve what I'm doing: writing several heavy routines that parse and measure the text, resize the text, and repeat until a suitably small size has been found.

What routines does TextView use to wrap the text? Couldn't those be somehow used to predict whether text will be small enough?

tl;dr: is there a best-practice way to auto-resize a TextView to fit, wrapped, in its getHeight and getWidth bounds?

like image 331
Nathan Fig Avatar asked Feb 17 '11 18:02

Nathan Fig


2 Answers

From June 2018 Android officially started supporting this feature for Android 4.0 (API level 14) and higher.
Check it out at: Autosizing TextViews

With Android 8.0 (API level 26) and higher:

<?xml version="1.0" encoding="utf-8"?> <TextView     android:layout_width="match_parent"     android:layout_height="200dp"     android:autoSizeTextType="uniform"     android:autoSizeMinTextSize="12sp"     android:autoSizeMaxTextSize="100sp"     android:autoSizeStepGranularity="2sp" /> 

Programmatically:

setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize, int autoSizeMaxTextSize,          int autoSizeStepGranularity, int unit)  textView.setAutoSizeTextTypeUniformWithConfiguration(                 1, 17, 1, TypedValue.COMPLEX_UNIT_DIP); 


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

<?xml version="1.0" encoding="utf-8"?> <LinearLayout     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent">    <TextView       android:layout_width="match_parent"       android:layout_height="200dp"       app:autoSizeTextType="uniform"       app:autoSizeMinTextSize="12sp"       app:autoSizeMaxTextSize="100sp"       app:autoSizeStepGranularity="2sp" />  </LinearLayout> 

Programmatically:

TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration( TextView textView, int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)   TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(textView, 1, 17, 1, TypedValue.COMPLEX_UNIT_DIP); 

Attention: TextView must have layout_width="match_parent" or absolute size!

like image 45
Think Twice Code Once Avatar answered Oct 12 '22 11:10

Think Twice Code Once


As a mobile developer, I was sad to find nothing native that supports auto resizing. My searches did not turn up anything that worked for me and in the end, I spent the better half of my weekend and created my own auto resize text view. I will post the code here and hopefully it will be useful for someone else.

This class uses a static layout with the text paint of the original text view to measure the height. From there, I step down by 2 font pixels and remeasure until I have a size that fits. At the end, if the text still does not fit, I append an ellipsis. I had requirements to animate the text and reuse views and this seems to work well on the devices I have and seems to run fast enough for me.

/**  *               DO WHAT YOU WANT TO PUBLIC LICENSE  *                    Version 2, December 2004  *   * Copyright (C) 2004 Sam Hocevar <[email protected]>  *   * Everyone is permitted to copy and distribute verbatim or modified  * copies of this license document, and changing it is allowed as long  * as the name is changed.  *   *            DO WHAT YOU WANT TO PUBLIC LICENSE  *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION  *   *  0. You just DO WHAT YOU WANT TO.  */  import android.content.Context; import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.TypedValue; import android.widget.TextView;  /**  * Text view that auto adjusts text size to fit within the view.  * If the text size equals the minimum text size and still does not  * fit, append with an ellipsis.  *   * @author Chase Colburn  * @since Apr 4, 2011  */ public class AutoResizeTextView extends TextView {      // Minimum text size for this text view     public static final float MIN_TEXT_SIZE = 20;      // Interface for resize notifications     public interface OnTextResizeListener {         public void onTextResize(TextView textView, float oldSize, float newSize);     }      // Our ellipse string     private static final String mEllipsis = "...";      // Registered resize listener     private OnTextResizeListener mTextResizeListener;      // Flag for text and/or size changes to force a resize     private boolean mNeedsResize = false;      // Text size that is set from code. This acts as a starting point for resizing     private float mTextSize;      // Temporary upper bounds on the starting text size     private float mMaxTextSize = 0;      // Lower bounds for text size     private float mMinTextSize = MIN_TEXT_SIZE;      // Text view line spacing multiplier     private float mSpacingMult = 1.0f;      // Text view additional line spacing     private float mSpacingAdd = 0.0f;      // Add ellipsis to text that overflows at the smallest text size     private boolean mAddEllipsis = true;      // Default constructor override     public AutoResizeTextView(Context context) {         this(context, null);     }      // Default constructor when inflating from XML file     public AutoResizeTextView(Context context, AttributeSet attrs) {         this(context, attrs, 0);     }      // Default constructor override     public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {         super(context, attrs, defStyle);         mTextSize = getTextSize();     }      /**      * When text changes, set the force resize flag to true and reset the text size.      */     @Override     protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {         mNeedsResize = true;         // Since this view may be reused, it is good to reset the text size         resetTextSize();     }      /**      * If the text view size changed, set the force resize flag to true      */     @Override     protected void onSizeChanged(int w, int h, int oldw, int oldh) {         if (w != oldw || h != oldh) {             mNeedsResize = true;         }     }      /**      * Register listener to receive resize notifications      * @param listener      */     public void setOnResizeListener(OnTextResizeListener listener) {         mTextResizeListener = listener;     }      /**      * Override the set text size to update our internal reference values      */     @Override     public void setTextSize(float size) {         super.setTextSize(size);         mTextSize = getTextSize();     }      /**      * Override the set text size to update our internal reference values      */     @Override     public void setTextSize(int unit, float size) {         super.setTextSize(unit, size);         mTextSize = getTextSize();     }      /**      * Override the set line spacing to update our internal reference values      */     @Override     public void setLineSpacing(float add, float mult) {         super.setLineSpacing(add, mult);         mSpacingMult = mult;         mSpacingAdd = add;     }      /**      * Set the upper text size limit and invalidate the view      * @param maxTextSize      */     public void setMaxTextSize(float maxTextSize) {         mMaxTextSize = maxTextSize;         requestLayout();         invalidate();     }      /**      * Return upper text size limit      * @return      */     public float getMaxTextSize() {         return mMaxTextSize;     }      /**      * Set the lower text size limit and invalidate the view      * @param minTextSize      */     public void setMinTextSize(float minTextSize) {         mMinTextSize = minTextSize;         requestLayout();         invalidate();     }      /**      * Return lower text size limit      * @return      */     public float getMinTextSize() {         return mMinTextSize;     }      /**      * Set flag to add ellipsis to text that overflows at the smallest text size      * @param addEllipsis      */     public void setAddEllipsis(boolean addEllipsis) {         mAddEllipsis = addEllipsis;     }      /**      * Return flag to add ellipsis to text that overflows at the smallest text size      * @return      */     public boolean getAddEllipsis() {         return mAddEllipsis;     }      /**      * Reset the text to the original size      */     public void resetTextSize() {         if (mTextSize > 0) {             super.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);             mMaxTextSize = mTextSize;         }     }      /**      * Resize text after measuring      */     @Override     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {         if (changed || mNeedsResize) {             int widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();             int heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();             resizeText(widthLimit, heightLimit);         }         super.onLayout(changed, left, top, right, bottom);     }      /**      * Resize the text size with default width and height      */     public void resizeText() {          int heightLimit = getHeight() - getPaddingBottom() - getPaddingTop();         int widthLimit = getWidth() - getPaddingLeft() - getPaddingRight();         resizeText(widthLimit, heightLimit);     }      /**      * Resize the text size with specified width and height      * @param width      * @param height      */     public void resizeText(int width, int height) {         CharSequence text = getText();         // Do not resize if the view does not have dimensions or there is no text         if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) {             return;         }          if (getTransformationMethod() != null) {             text = getTransformationMethod().getTransformation(text, this);         }          // Get the text view's paint object         TextPaint textPaint = getPaint();          // Store the current text size         float oldTextSize = textPaint.getTextSize();         // If there is a max text size set, use the lesser of that and the default text size         float targetTextSize = mMaxTextSize > 0 ? Math.min(mTextSize, mMaxTextSize) : mTextSize;          // Get the required text height         int textHeight = getTextHeight(text, textPaint, width, targetTextSize);          // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes         while (textHeight > height && targetTextSize > mMinTextSize) {             targetTextSize = Math.max(targetTextSize - 2, mMinTextSize);             textHeight = getTextHeight(text, textPaint, width, targetTextSize);         }          // If we had reached our minimum text size and still don't fit, append an ellipsis         if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) {             // Draw using a static layout             // modified: use a copy of TextPaint for measuring             TextPaint paint = new TextPaint(textPaint);             // Draw using a static layout             StaticLayout layout = new StaticLayout(text, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false);             // Check that we have a least one line of rendered text             if (layout.getLineCount() > 0) {                 // Since the line at the specific vertical position would be cut off,                 // we must trim up to the previous line                 int lastLine = layout.getLineForVertical(height) - 1;                 // If the text would not even fit on a single line, clear it                 if (lastLine < 0) {                     setText("");                 }                 // Otherwise, trim to the previous line and add an ellipsis                 else {                     int start = layout.getLineStart(lastLine);                     int end = layout.getLineEnd(lastLine);                     float lineWidth = layout.getLineWidth(lastLine);                     float ellipseWidth = textPaint.measureText(mEllipsis);                      // Trim characters off until we have enough room to draw the ellipsis                     while (width < lineWidth + ellipseWidth) {                         lineWidth = textPaint.measureText(text.subSequence(start, --end + 1).toString());                     }                     setText(text.subSequence(0, end) + mEllipsis);                 }             }         }          // Some devices try to auto adjust line spacing, so force default line spacing         // and invalidate the layout as a side effect         setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize);         setLineSpacing(mSpacingAdd, mSpacingMult);          // Notify the listener if registered         if (mTextResizeListener != null) {             mTextResizeListener.onTextResize(this, oldTextSize, targetTextSize);         }          // Reset force resize flag         mNeedsResize = false;     }      // Set the text size of the text paint object and use a static layout to render text off screen before measuring     private int getTextHeight(CharSequence source, TextPaint paint, int width, float textSize) {         // modified: make a copy of the original TextPaint object for measuring         // (apparently the object gets modified while measuring, see also the         // docs for TextView.getPaint() (which states to access it read-only)         TextPaint paintCopy = new TextPaint(paint);         // Update the text paint object         paintCopy.setTextSize(textSize);         // Measure using a static layout         StaticLayout layout = new StaticLayout(source, paintCopy, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);         return layout.getHeight();     }  } 

Warning. There is an important fixed bug affecting Android 3.1 - 4.04 causing all AutoResizingTextView widgets not to work. Please read: https://stackoverflow.com/a/21851157/2075875

like image 156
19 revs, 6 users 96% Avatar answered Oct 12 '22 11:10

19 revs, 6 users 96%