Texto de AutoView TextView para ajustar dentro de límites

Estoy buscando una forma óptima de cambiar el tamaño del texto de envoltura en un TextView para que se ajuste dentro de sus límites getHeight y getWidth. No estoy simplemente buscando una forma de envolver el texto, quiero asegurarme de que se envuelve y que sea lo suficientemente pequeño como para caber completamente en la pantalla.

He visto algunos casos en StackOverflow donde era necesario cambiar el tamaño automático, pero son casos muy especiales con soluciones de TextView , no tienen solución, o implican volver a dibujar TextView recursivamente hasta que sea lo suficientemente pequeño (que es memoria intensa y fuerzas el usuario para ver el texto encogerse paso a paso con cada recursión).

Pero estoy seguro de que alguien ha encontrado una buena solución que no involucra lo que estoy haciendo: escribir varias rutinas pesadas que analizan y miden el texto, cambian el tamaño del texto y lo repiten hasta que se encuentra un tamaño adecuado.

¿Qué rutinas usa TextView para envolver el texto? ¿No podrían usarse de alguna manera para predecir si el texto será lo suficientemente pequeño?

tl; dr : ¿existe alguna forma de mejores prácticas para cambiar automáticamente el tamaño de una TextView de TextView para que quepa en sus límites getHeight y getWidth?

Con Android 8.0 (nivel 26 de Api) esta es una característica predeterminada. https://developer.android.com/guide/topics/ui/look-and-feel/autosizing-textview.html

Como desarrollador móvil, me entristeció no encontrar nada nativo que sea compatible con el cambio de tamaño automático. Mis búsquedas no encontraron nada que funcionara para mí y, al final, pasé la mitad del fin de semana y creé mi propia vista de texto con cambio de tamaño automático. Publicaré el código aquí y espero que sea útil para otra persona.

Esta clase usa un diseño estático con la pintura de texto de la vista de texto original para medir la altura. A partir de ahí, baje por 2 píxeles de fuente y vuelva a medir hasta que tenga un tamaño que encaje. Al final, si el texto aún no encaja, añado una elipsis. Tenía requisitos para animar el texto y volver a usar vistas, y esto parece funcionar bien en los dispositivos que tengo y parece funcionar lo suficientemente rápido para mí.

 /** * DO WHAT YOU WANT TO PUBLIC LICENSE * Version 2, December 2004 * * Copyright (C) 2004 Sam Hocevar  * * 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(); } } 

Advertencia. Existe un error importante que afecta a Android 3.1 - 4.04, lo que hace que todos los widgets AutoResizingTextView no funcionen. Por favor, lea: https://stackoverflow.com/a/21851157/2075875

ACTUALIZACIÓN: El código siguiente también cumple con el requisito de un AutoScaleTextView ideal como se describe aquí: ajustar automáticamente TextView para Android y se marca como ganador.

ACTUALIZACIÓN 2: El soporte de las líneas máximas agregadas, ahora funciona bien antes del nivel 16 de la API.

Actualización 3: Soporte para android:drawableLeft , android:drawableRight , android:drawableTop y android:drawableBottom tags drawableBottom agregadas, gracias a la solución simple de MartinH aquí .


Mis requisitos eran un poco diferentes. Necesitaba una forma eficiente de ajustar el tamaño porque estaba animando un número entero, puede ser de 0 a ~ 4000 en TextView en 2 segundos y quería ajustar el tamaño en consecuencia. Mi solución funciona de manera diferente. Aquí se muestra el resultado final:

enter image description here

y el código que lo produjo:

         

Y finalmente el código de Java:

 import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.RectF; import android.os.Build; import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.SparseIntArray; import android.util.TypedValue; import android.widget.TextView; public class AutoResizeTextView extends TextView { private interface SizeTester { /** * * @param suggestedSize * Size of text to be tested * @param availableSpace * available space in which text must fit * @return an integer < 0 if after applying {@code suggestedSize} to * text, it takes less space than {@code availableSpace}, > 0 * otherwise */ public int onTestSize(int suggestedSize, RectF availableSpace); } private RectF mTextRect = new RectF(); private RectF mAvailableSpaceRect; private SparseIntArray mTextCachedSizes; private TextPaint mPaint; private float mMaxTextSize; private float mSpacingMult = 1.0f; private float mSpacingAdd = 0.0f; private float mMinTextSize = 20; private int mWidthLimit; private static final int NO_LINE_LIMIT = -1; private int mMaxLines; private boolean mEnableSizeCache = true; private boolean mInitiallized; public AutoResizeTextView(Context context) { super(context); initialize(); } public AutoResizeTextView(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initialize(); } private void initialize() { mPaint = new TextPaint(getPaint()); mMaxTextSize = getTextSize(); mAvailableSpaceRect = new RectF(); mTextCachedSizes = new SparseIntArray(); if (mMaxLines == 0) { // no value was assigned during construction mMaxLines = NO_LINE_LIMIT; } mInitiallized = true; } @Override public void setText(final CharSequence text, BufferType type) { super.setText(text, type); adjustTextSize(text.toString()); } @Override public void setTextSize(float size) { mMaxTextSize = size; mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } @Override public void setMaxLines(int maxlines) { super.setMaxLines(maxlines); mMaxLines = maxlines; reAdjust(); } public int getMaxLines() { return mMaxLines; } @Override public void setSingleLine() { super.setSingleLine(); mMaxLines = 1; reAdjust(); } @Override public void setSingleLine(boolean singleLine) { super.setSingleLine(singleLine); if (singleLine) { mMaxLines = 1; } else { mMaxLines = NO_LINE_LIMIT; } reAdjust(); } @Override public void setLines(int lines) { super.setLines(lines); mMaxLines = lines; reAdjust(); } @Override public void setTextSize(int unit, float size) { Context c = getContext(); Resources r; if (c == null) r = Resources.getSystem(); else r = c.getResources(); mMaxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics()); mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } @Override public void setLineSpacing(float add, float mult) { super.setLineSpacing(add, mult); mSpacingMult = mult; mSpacingAdd = add; } /** * Set the lower text size limit and invalidate the view * * @param minTextSize */ public void setMinTextSize(float minTextSize) { mMinTextSize = minTextSize; reAdjust(); } private void reAdjust() { adjustTextSize(getText().toString()); } private void adjustTextSize(String string) { if (!mInitiallized) { return; } int startSize = (int) mMinTextSize; int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop(); mWidthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); mAvailableSpaceRect.right = mWidthLimit; mAvailableSpaceRect.bottom = heightLimit; super.setTextSize( TypedValue.COMPLEX_UNIT_PX, efficientTextSizeSearch(startSize, (int) mMaxTextSize, mSizeTester, mAvailableSpaceRect)); } private final SizeTester mSizeTester = new SizeTester() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public int onTestSize(int suggestedSize, RectF availableSPace) { mPaint.setTextSize(suggestedSize); String text = getText().toString(); boolean singleline = getMaxLines() == 1; if (singleline) { mTextRect.bottom = mPaint.getFontSpacing(); mTextRect.right = mPaint.measureText(text); } else { StaticLayout layout = new StaticLayout(text, mPaint, mWidthLimit, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); // return early if we have more lines if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines()) { return 1; } mTextRect.bottom = layout.getHeight(); int maxWidth = -1; for (int i = 0; i < layout.getLineCount(); i++) { if (maxWidth < layout.getLineWidth(i)) { maxWidth = (int) layout.getLineWidth(i); } } mTextRect.right = maxWidth; } mTextRect.offsetTo(0, 0); if (availableSPace.contains(mTextRect)) { // may be too small, don't worry we will find the best match return -1; } else { // too big return 1; } } }; /** * Enables or disables size caching, enabling it will improve performance * where you are animating a value inside TextView. This stores the font * size against getText().length() Be careful though while enabling it as 0 * takes more space than 1 on some fonts and so on. * * @param enable * enable font size caching */ public void enableSizeCache(boolean enable) { mEnableSizeCache = enable; mTextCachedSizes.clear(); adjustTextSize(getText().toString()); } private int efficientTextSizeSearch(int start, int end, SizeTester sizeTester, RectF availableSpace) { if (!mEnableSizeCache) { return binarySearch(start, end, sizeTester, availableSpace); } String text = getText().toString(); int key = text == null ? 0 : text.length(); int size = mTextCachedSizes.get(key); if (size != 0) { return size; } size = binarySearch(start, end, sizeTester, availableSpace); mTextCachedSizes.put(key, size); return size; } private static int binarySearch(int start, int end, SizeTester sizeTester, RectF availableSpace) { int lastBest = start; int lo = start; int hi = end - 1; int mid = 0; while (lo <= hi) { mid = (lo + hi) >>> 1; int midValCmp = sizeTester.onTestSize(mid, availableSpace); if (midValCmp < 0) { lastBest = lo; lo = mid + 1; } else if (midValCmp > 0) { hi = mid - 1; lastBest = hi; } else { return mid; } } // make sure to return last best // this is what should always be returned return lastBest; } @Override protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { super.onTextChanged(text, start, before, after); reAdjust(); } @Override protected void onSizeChanged(int width, int height, int oldwidth, int oldheight) { mTextCachedSizes.clear(); super.onSizeChanged(width, height, oldwidth, oldheight); if (width != oldwidth || height != oldheight) { reAdjust(); } } } 

En realidad, una solución está en la clase DialogTitle de Google … aunque no es tan efectiva como la aceptada, es mucho más simple y fácil de adaptar.

 public class SingleLineTextView extends TextView { public SingleLineTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setSingleLine(); setEllipsize(TruncateAt.END); } public SingleLineTextView(Context context, AttributeSet attrs) { super(context, attrs); setSingleLine(); setEllipsize(TruncateAt.END); } public SingleLineTextView(Context context) { super(context); setSingleLine(); setEllipsize(TruncateAt.END); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final Layout layout = getLayout(); if (layout != null) { final int lineCount = layout.getLineCount(); if (lineCount > 0) { final int ellipsisCount = layout.getEllipsisCount(lineCount - 1); if (ellipsisCount > 0) { final float textSize = getTextSize(); // textSize is already expressed in pixels setTextSize(TypedValue.COMPLEX_UNIT_PX, (textSize - 1)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } } } } 

Empecé con la solución de Chase, pero tuve que adaptar dos cosas antes de que funcionara como esperaba en mi dispositivo (Galaxy Nexus, Android 4.1):

  1. usando una copia de TextPaint para medir el diseño La documentación para TextView.getPaint () indica que debe usarse solo para lectura, así que hice una copia en ambos lugares donde usamos el objeto paint para medir:

     // 1. in resizeText() if (mAddEllipsis && targetTextSize == mMinTextSize && textHeight > height) { // Draw using a static layout // modified: use a copy of TextPaint for measuring TextPaint paint = new TextPaint(textPaint); // 2. in getTextHeight() private int getTextHeight(CharSequence source, TextPaint originalPaint, 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 paint = new TextPaint(originalPaint); // Update the text paint object paint.setTextSize(textSize); ... 
  2. agregar una unidad para configurar el tamaño del texto

     // modified: setting text size via this.setTextSize (instead of textPaint.setTextSize(targetTextSize)) setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize); setLineSpacing(mSpacingAdd, mSpacingMult); 

Con estas dos modificaciones, la solución funciona perfectamente para mí, ¡gracias Chase! No sé si es debido a Android 4.x que la solución original no funcionaba. En caso de que quiera verlo en acción o probar si realmente funciona en su dispositivo, puede echar un vistazo a mi aplicación de tarjetas de memoria Flashcards ToGo, donde utilizo esta solución para escalar el texto de una tarjeta de memoria. El texto puede tener una longitud arbitraria, y las tarjetas se muestran en diferentes actividades, a veces más pequeñas, a veces más grandes, más en modo paisaje + retrato, y no he encontrado ningún caso de esquina donde la solución no funcione correctamente …

Comencé con la clase AutoResizeTextView de Chase e hice un cambio menor para que se ajustara vertical y horizontalmente.

También descubrí un error que causa una excepción de puntero nulo en el Editor de diseño (en Eclipse) bajo algunas condiciones bastante oscuras.

Cambiar 1: ajustar el texto tanto vertical como horizontalmente

La versión original de Chase reduce el tamaño del texto hasta que se ajusta verticalmente, pero permite que el texto sea más ancho que el objective. En mi caso, necesitaba el texto para ajustarse a un ancho específico.

Este cambio hace que cambie de tamaño hasta que el texto se ajuste vertical y horizontalmente.

En resizeText( int , int ) cambie de:

 // 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); } 

a:

 // Get the required text height int textHeight = getTextHeight(text, textPaint, width, targetTextSize); int textWidth = getTextWidth(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) || (textWidth >= width) ) && targetTextSize > mMinTextSize) { targetTextSize = Math.max(targetTextSize - 2, mMinTextSize); textHeight = getTextHeight(text, textPaint, width, targetTextSize); textWidth = getTextWidth(text, textPaint, width, targetTextSize); } 

Luego, al final del archivo, agregue la rutina getTextWidth() ; es solo un getTextHeight() ligeramente modificado getTextHeight() . Probablemente sería más eficiente combinarlos en una rutina que devuelva altura y ancho.

 // Set the text size of the text paint object and use a static layout to render text off screen before measuring private int getTextWidth(CharSequence source, TextPaint paint, int width, float textSize) { // Update the text paint object paint.setTextSize(textSize); // Draw using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); layout.draw(sTextResizeCanvas); return layout.getWidth(); } 

Cambio 2: corrige una EmptyStackException en el Eclipse Android Layout Editor

En condiciones bastante oscuras y muy precisas, el Editor de diseño no podrá mostrar la presentación gráfica del diseño; arrojará una excepción “EmptyStackException: null” en com.android.ide.eclipse.adt.

Las condiciones requeridas son:
– crear un widget AutoResizeTextView
– crea un estilo para ese widget
– especificar el elemento de texto en el estilo; no en la definición del widget

como en:

res / layout / main.xml:

     

res / values ​​/ myStyles.xml:

     

Con estos archivos, al seleccionar la pestaña Diseño gráfico al editar main.xml se mostrará:

¡error!
EmptyStackException: null
Los detalles de las excepciones se registran en Ventana> Mostrar vista> Registro de errores

en lugar de la vista gráfica del diseño.

Para mantener una historia ya demasiado larga más corta, rastreé esto hasta las siguientes líneas (de nuevo en resizeText ):

 // 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; 

El problema es que bajo las condiciones específicas, mTextSize nunca se inicializa; tiene el valor 0.

Con lo anterior, targetTextSize se establece en cero (como resultado de Math.min).

Ese cero se pasa a getTextHeight() (y getTextWidth() ) como el argumento textSize . Cuando llega el momento
layout.draw(sTextResizeCanvas);
obtenemos la excepción

Es más eficiente probar si (mTextSize == 0) al principio de resizeText() lugar de probar en getTextHeight() y getTextWidth() ; probar antes guarda todo el trabajo intermedio.

With these updates, the file (as in my crash-demo test app) is now:

 // // from: http://stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds // // package com.ajw.DemoCrashInADT; import android.content.Context; import android.graphics.Canvas; 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. * * 2011-10-29 changes by Alan Jay Weiner * * change to fit both vertically and horizontally * * test mTextSize for 0 in resizeText() to fix exception in Layout Editor * * @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); } // Off screen canvas for text size rendering private static final Canvas sTextResizeCanvas = new Canvas(); // 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() { 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 // or if mTextSize has not been initialized if (text == null || text.length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) { return; } // 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); int textWidth = getTextWidth(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) || (textWidth > width)) && targetTextSize > mMinTextSize) { targetTextSize = Math.max(targetTextSize - 2, mMinTextSize); textHeight = getTextHeight(text, textPaint, width, targetTextSize); textWidth = getTextWidth(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 StaticLayout layout = new StaticLayout(text, textPaint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, false); layout.draw(sTextResizeCanvas); int lastLine = layout.getLineForVertical(height) - 1; 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 textPaint.setTextSize(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) { // Update the text paint object paint.setTextSize(textSize); // Draw using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); layout.draw(sTextResizeCanvas); return layout.getHeight(); } // Set the text size of the text paint object and use a static layout to // render text off screen before measuring private int getTextWidth(CharSequence source, TextPaint paint, int width, float textSize) { // Update the text paint object paint.setTextSize(textSize); // Draw using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); layout.draw(sTextResizeCanvas); return layout.getWidth(); } } 

A big thank you to Chase for posting the initial code. I enjoyed reading through it to see how it worked, and I'm pleased to be able to add to it.

A workaround for Android 4.x:

I found AutoResizeTextView and it works great on my Android 2.1 emulator. I loved it so much. But unfortunately it failed on my own 4.0.4 cellphone and 4.1 emulator. After trying I found it could be easily resolved by adding following attributes in AutoResizeTextView class in the xml:

android:ellipsize=”none”

android:singleLine=”true”

With the 2 lines above, now AutoResizeTextView working perfectly on my 2.1 & 4.1 emulators and my own 4.0.4 cellphone now.

Espero que esto te ayude. 🙂

Warning, bug in Android Honeycomb and Ice Cream Sandwich

Androids versions: 3.1 – 4.04 have a bug, that setTextSize() inside of TextView works only for the 1st time (1st invocation).

Bug is described here: http://code.google.com/p/android/issues/detail?id=22493 http://code.google.com/p/android/issues/detail?id=17343#c9

workaround is to add new line character to text assigned to TextView before changing size:

 final String DOUBLE_BYTE_SPACE = "\u3000"; textView.append(DOUBLE_BYTE_SPACE); 

I use it in my code as follow:

 final String DOUBLE_BYTE_SPACE = "\u3000"; AutoResizeTextView textView = (AutoResizeTextView) view.findViewById(R.id.aTextView); String fixString = ""; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1 && android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { fixString = DOUBLE_BYTE_SPACE; } textView.setText(fixString + "The text" + fixString); 

I add this "\u3000" character on left and right of my text, to keep it centered. If you have it aligned to left then append to the right only. Of course it can be also embedded with AutoResizeTextView widget, but I wanted to keep fix code outside.

My need was to resize text in order to perfectly fit view bounds. Chase’s solution only reduces text size, this one enlarges also the text if there is enough space.

To make all fast & precise i used a bisection method instead of an iterative while, as you can see in resizeText() method. That’s why you have also a MAX_TEXT_SIZE option. I also included onoelle’s tips.

Tested on Android 4.4

 /** * DO WHAT YOU WANT TO PUBLIC LICENSE * Version 2, December 2004 * * Copyright (C) 2004 Sam Hocevar  * * 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 = 26; // Maximum text size for this text view public static final float MAX_TEXT_SIZE = 128; private static final int BISECTION_LOOP_WATCH_DOG = 30; // 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 = MAX_TEXT_SIZE; // 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; } // Get the text view's paint object TextPaint textPaint = getPaint(); // Store the current text size float oldTextSize = textPaint.getTextSize(); // Bisection method: fast & precise float lower = mMinTextSize; float upper = mMaxTextSize; int loop_counter=1; float targetTextSize = (lower+upper)/2; int textHeight = getTextHeight(text, textPaint, width, targetTextSize); while(loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) { targetTextSize = (lower+upper)/2; textHeight = getTextHeight(text, textPaint, width, targetTextSize); if(textHeight > height) upper = targetTextSize; else lower = targetTextSize; loop_counter++; } targetTextSize = lower; 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 paintCopy = new TextPaint(textPaint); paintCopy.setTextSize(targetTextSize); StaticLayout layout = new StaticLayout(text, paintCopy, 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 = paintCopy.measureText(mEllipsis); // Trim characters off until we have enough room to draw the ellipsis while(width < lineWidth + ellipseWidth) { lineWidth = paintCopy.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 originalPaint, 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 paint = new TextPaint(originalPaint); // Update the text paint object paint.setTextSize(textSize); // Measure using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); return layout.getHeight(); } } 

AppcompatTextView now supports auto sizing starting from Support Library 26.0. TextView in Android O also works same way. More info can be found here. A simple demo app can be found here .

    

Since I’ve been looking for this forever, and I found a solution a while ago which is missing here, I’m gonna write it here, for future reference also.

Note: this code was taken directly from Google Android Lollipop dialer a while back, I don’t remember If changes were made at the time. Also, I don’t know which license is this under, but I have reason to think it is Apache 2.0 .

Class ResizeTextView , the actual View

 public class ResizeTextView extends TextView { private final int mOriginalTextSize; private final int mMinTextSize; private final static int sMinSize = 20; public ResizeTextView(Context context, AttributeSet attrs) { super(context, attrs); mOriginalTextSize = (int) getTextSize(); mMinTextSize = (int) sMinSize; } @Override protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { super.onTextChanged(text, start, lengthBefore, lengthAfter); ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize); } 

This ResizeTextView class could extend TextView and all its children as I undestand, so EditText as well.

Class ViewUtil with method resizeText(...)

 /* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.graphics.Paint; import android.util.TypedValue; import android.widget.TextView; public class ViewUtil { private ViewUtil() {} public static void resizeText(TextView textView, int originalTextSize, int minTextSize) { final Paint paint = textView.getPaint(); final int width = textView.getWidth(); if (width == 0) return; textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize); float ratio = width / paint.measureText(textView.getText().toString()); if (ratio <= 1.0f) { textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(minTextSize, originalTextSize * ratio)); } } } 

You should set your view as

  

¡Espero eso ayude!

I hope this helps you

 import android.content.Context; import android.graphics.Rect; import android.text.TextPaint; import android.util.AttributeSet; import android.widget.TextView; /* Based on * from http://stackoverflow.com/questions/2617266/how-to-adjust-text-font-size-to-fit-textview */ public class FontFitTextView extends TextView { private static float MAX_TEXT_SIZE = 20; public FontFitTextView(Context context) { this(context, null); } public FontFitTextView(Context context, AttributeSet attrs) { super(context, attrs); float size = this.getTextSize(); if (size > MAX_TEXT_SIZE) setTextSize(MAX_TEXT_SIZE); } private void refitText(String text, int textWidth) { if (textWidth > 0) { float availableWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight(); TextPaint tp = getPaint(); Rect rect = new Rect(); tp.getTextBounds(text, 0, text.length(), rect); float size = rect.width(); if (size > availableWidth) setTextScaleX(availableWidth / size); } } @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); } @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); } } } 

NOTE: I use MAX_TEXT_SIZE in case of text size is bigger than 20 because I don’t want to allow big fonts applies to my View, if this is not your case, you can just simply remove it.

I wrote a blog post about this.

I created a component called ResizableButton based on Kirill Grouchnikov’s blog post about custom components used in the new android market app. I placed the src code here .

On the other hand, mosabua read my post and told me he was going to open source his implementation which was faster than mine. I hope he release it soon enough 🙂

Here’s a simple solution that uses TextView itself with a TextChangedListened added to it:

 expressionView = (TextView) findViewById(R.id.expressionView); expressionView.addTextChangedListener(textAutoResizeWatcher(expressionView, 25, 55)); private TextWatcher textAutoResizeWatcher(final TextView view, final int MIN_SP, final int MAX_SP) { return new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void afterTextChanged(Editable editable) { final int widthLimitPixels = view.getWidth() - view.getPaddingRight() - view.getPaddingLeft(); Paint paint = new Paint(); float fontSizeSP = pixelsToSp(view.getTextSize()); paint.setTextSize(spToPixels(fontSizeSP)); String viewText = view.getText().toString(); float widthPixels = paint.measureText(viewText); // Increase font size if necessary. if (widthPixels < widthLimitPixels){ while (widthPixels < widthLimitPixels && fontSizeSP <= MAX_SP){ ++fontSizeSP; paint.setTextSize(spToPixels(fontSizeSP)); widthPixels = paint.measureText(viewText); } --fontSizeSP; } // Decrease font size if necessary. else { while (widthPixels > widthLimitPixels || fontSizeSP > MAX_SP) { if (fontSizeSP < MIN_SP) { fontSizeSP = MIN_SP; break; } --fontSizeSP; paint.setTextSize(spToPixels(fontSizeSP)); widthPixels = paint.measureText(viewText); } } view.setTextSize(fontSizeSP); } }; } private float pixelsToSp(float px) { float scaledDensity = getResources().getDisplayMetrics().scaledDensity; return px/scaledDensity; } private float spToPixels(float sp) { float scaledDensity = getResources().getDisplayMetrics().scaledDensity; return sp * scaledDensity; } 

This approach will increase or decrease the font size as needed to fit the text, respecting the MIN_SP and MAX_SP bounds received as parameters.

At google IO conference in 2017, google introduced autoSize property of TextView

https://youtu.be/fjUdJ2aVqE4

  

My implementation is a bit more complex, but comes with the following goodies:

  • takes the available width and available height into account
  • works with single line and multiline labels
  • uses ellipsis in case the minimum font size is hit
  • since the internal text representation is changed, remembers the originally set text in a separate variable
  • ensures that the canvas is always only as big as it needs to be, while it uses all the available height of the parent
 /** * 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. * * Based on the original work from Chase Colburn * <http://stackoverflow.com/a/5535672/305532> * * @author Thomas Keller <me@thomaskeller.biz> */ public class AutoResizeTextView extends TextView { // in dip private static final int MIN_TEXT_SIZE = 20; private static final boolean SHRINK_TEXT_SIZE = true; private static final char ELLIPSIS = '\u2026'; private static final float LINE_SPACING_MULTIPLIER_MULTILINE = 0.8f; private static final float LINE_SPACING_MULTIPLIER_SINGLELINE = 1f; private static final float LINE_SPACING_EXTRA = 0.0f; private CharSequence mOriginalText; // temporary upper bounds on the starting text size private float mMaxTextSize; // lower bounds for text size private float mMinTextSize; // determines whether we're currently in the process of measuring ourselves, // so we do not enter onMeasure recursively private boolean mInMeasure = false; // if the text size should be shrinked or if the text size should be kept // constant and only characters should be removed to hit the boundaries private boolean mShrinkTextSize; public AutoResizeTextView(Context context) { this(context, null); init(context, null); } public AutoResizeTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); init(context, attrs); } public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private void init(Context context, AttributeSet attrs) { // the current text size is used as maximum text size we can apply to // our widget mMaxTextSize = getTextSize(); if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AutoResizeTextView); mMinTextSize = a.getFloat(R.styleable.AutoResizeTextView_minFontSize, MIN_TEXT_SIZE); mShrinkTextSize = a.getBoolean(R.styleable.AutoResizeTextView_shrinkTextSize, SHRINK_TEXT_SIZE); a.recycle(); } } @Override public void setTextSize(float size) { mMaxTextSize = size; super.setTextSize(size); } /** * Returns the original, unmodified text of this widget * * @return */ public CharSequence getOriginalText() { // text has not been resized yet if (mOriginalText == null) { return getText(); } return mOriginalText; } @Override public void setText(CharSequence text, BufferType type) { if (!mInMeasure) { mOriginalText = text.toString(); } super.setText(text, type); } @SuppressLint("DrawAllocation") @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mInMeasure = true; try { int availableWidth = MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft() - getCompoundPaddingRight(); int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - getCompoundPaddingTop() - getCompoundPaddingBottom(); // Do not resize if the view does not have dimensions or there is no // text if (mOriginalText == null || mOriginalText.length() == 0 || availableWidth <= 0) { return; } TextPaint textPaint = getPaint(); // start with the recorded max text size float targetTextSize = mMaxTextSize; String originalText = mOriginalText.toString(); String finalText = originalText; Rect textSize = getTextSize(originalText, textPaint, targetTextSize); boolean textExceedsBounds = textSize.height() > availableHeight || textSize.width() > availableWidth; if (mShrinkTextSize && textExceedsBounds) { // check whether all lines can be rendered in the available // width / height without violating the bounds of the parent and // without using a text size that is smaller than the minimum // text size float heightMultiplier = availableHeight / (float) textSize.height(); float widthMultiplier = availableWidth / (float) textSize.width(); float multiplier = Math.min(heightMultiplier, widthMultiplier); targetTextSize = Math.max(targetTextSize * multiplier, mMinTextSize); // measure again textSize = getTextSize(finalText, textPaint, targetTextSize); } // we cannot shrink the height further when we hit the available // height, but we can shrink the width by applying an ellipsis on // each line if (textSize.width() > availableWidth) { StringBuilder modifiedText = new StringBuilder(); String lines[] = originalText.split(System.getProperty("line.separator")); for (int i = 0; i < lines.length; i++) { modifiedText.append(resizeLine(textPaint, lines[i], availableWidth)); // add the separator back to all but the last processed line if (i != lines.length - 1) { modifiedText.append(System.getProperty("line.separator")); } } finalText = modifiedText.toString(); // measure again textSize = getTextSize(finalText, textPaint, targetTextSize); } textPaint.setTextSize(targetTextSize); boolean isMultiline = finalText.indexOf('\n') > -1; // do not include extra font padding (for accents, ...) for // multiline texts, this will prevent proper placement with // Gravity.CENTER_VERTICAL if (isMultiline) { setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_MULTILINE); setIncludeFontPadding(false); } else { setLineSpacing(LINE_SPACING_EXTRA, LINE_SPACING_MULTIPLIER_SINGLELINE); setIncludeFontPadding(true); } // according to //  // we have to add a unicode character to trigger the text centering // in ICS. this particular character is known as "zero-width" and // does no harm. setText(finalText + "\u200B"); int measuredWidth = textSize.width() + getCompoundPaddingLeft() + getCompoundPaddingRight(); int measuredHeight = textSize.height() + getCompoundPaddingTop() + getCompoundPaddingBottom(); // expand the view to the parent's height in case it is smaller or // to the minimum height that has been set // FIXME: honor the vertical measure mode (EXACTLY vs AT_MOST) here // somehow measuredHeight = Math.max(measuredHeight, MeasureSpec.getSize(heightMeasureSpec)); setMeasuredDimension(measuredWidth, measuredHeight); } finally { mInMeasure = false; } } private Rect getTextSize(String text, TextPaint textPaint, float textSize) { textPaint.setTextSize(textSize); // StaticLayout depends on a given width in which it should lay out the // text (and optionally also split into separate lines). // Therefor we calculate the current text width manually and start with // a fake (read: maxmimum) width for the height calculation. // We do _not_ use layout.getLineWidth() here since this returns // slightly smaller numbers and therefor would lead to exceeded text box // drawing. StaticLayout layout = new StaticLayout(text, textPaint, Integer.MAX_VALUE, Alignment.ALIGN_NORMAL, 1f, 0f, true); int textWidth = 0; String lines[] = text.split(System.getProperty("line.separator")); for (int i = 0; i < lines.length; ++i) { textWidth = Math.max(textWidth, measureTextWidth(textPaint, lines[i])); } return new Rect(0, 0, textWidth, layout.getHeight()); } private String resizeLine(TextPaint textPaint, String line, int availableWidth) { checkArgument(line != null && line.length() > 0, "expected non-empty string"); int textWidth = measureTextWidth(textPaint, line); int lastDeletePos = -1; StringBuilder builder = new StringBuilder(line); while (textWidth > availableWidth && builder.length() > 0) { lastDeletePos = builder.length() / 2; builder.deleteCharAt(builder.length() / 2); // don't forget to measure the ellipsis character as well; it // doesn't matter where it is located in the line, it just has to be // there, since there are no (known) ligatures that use this glyph String textToMeasure = builder.toString() + ELLIPSIS; textWidth = measureTextWidth(textPaint, textToMeasure); } if (lastDeletePos > -1) { builder.insert(lastDeletePos, ELLIPSIS); } return builder.toString(); } // there are several methods in Android to determine the text width, namely // getBounds() and measureText(). // The latter works for us the best as it gives us the best / nearest // results without that our text canvas needs to wrap its text later on // again. private int measureTextWidth(TextPaint textPaint, String line) { return Math.round(textPaint.measureText(line)); } } 

[revised on 2012-11-21]

  • fixed the placement of the ellipsis (off-by-one error)
  • reworked text size calculation; now always the full text including line breaks is measured, to fix problems when the addition of the height of two single measured lines just didn’t lead to the same result as the measurement of the height of the text as a whole
  • instead of looping to find the smallest available text size, just calculate it after the first measurement

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.

 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; } 

Here’s an enumeration of what else I’ve found for anyone still searching:

1) Here’s a solution that recursively re-paints the textview until it fits. This means literally watching your text shrink into place, but at least it fits when it’s done. The code will need some tweaking to implement, but it’s mostly there.

2) You can try hacking together a custom solution like this , or dunni’s class in this , which is what I did using the getPaint().measureText(str) to search for the right size, but it got a lot messier since I need it to wrap only on whitespace…

3) You can keep searching- I’ve tried more alternatives than I can count. Ted’s advice on StaticLayout hasn’t paid off for me but maybe there’s something there; I tried using the StaticLayout.getEllipsis(line) to determine if text was going off screen, to no effect. See my (presently un-answered) post about that here .

I needed a specific solution. I have got an edittext and textview in my layout. The textview is fixed height and width. When the user starts to type in the edittext, the text should immediately appear in the textview. The text in the textfield should auto – resize to fit the textview. So I updated Chase’s solution to work for me. So when the text changes in the textview, resizing starts. The difference between mine and Chase’s soluton: resizing is done even if the user DELETE some chars . I hope it can help someone.

 public class TextFitTextView extends TextView { // Minimum text size for this text view public static final float MIN_TEXT_SIZE = 10; // Maximum text size for this text view - if it is 0, then the text acts // like match_parent public static final float MAX_TEXT_SIZE = 0; // Our ellipse string private static final String mEllipsis = "..."; // Text size that is set from code. This acts as a starting point for // resizing private float mTextSize; // Lower bounds for text size private float mMinTextSize = MIN_TEXT_SIZE; // Max bounds for text size private float mMaxTextSize = MAX_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; // Add ellipsis to text that overflows at the smallest text size private int heightLimit; private int widthLimit; // Default constructor override public TextFitTextView(Context context) { this(context, null); } // Default constructor when inflating from XML file public TextFitTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } // Default constructor override public TextFitTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mTextSize = getTextSize(); } /** * When text changes resize the text size. */ @Override protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { // if we are adding new chars to text if (before <= after && after != 1) { resizeText(true); // now we are deleting chars } else { resizeText(false); } } /** * 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 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; } /** * Get width and height limits */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (widthLimit == 0 && heightLimit == 0) { widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight(); heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop(); } super.onLayout(changed, left, top, right, bottom); } /** * Resize the text size with specified width and height * * @param width * @param height */ public void resizeText(boolean increase) { CharSequence text = getText(); // Do not resize if the view does not have dimensions or there is no // text if (text == null || text.length() == 0 || heightLimit <= 0 || widthLimit <= 0 || mTextSize == 0) { return; } // Get the text view's paint object TextPaint textPaint = getPaint(); // Get the required text height int textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize); // If the text length is increased // Until we either fit within our text view or we had reached our min // text size, incrementally try smaller sizes if (increase) { while (textHeight > heightLimit && mTextSize > mMinTextSize) { mTextSize = Math.max(mTextSize - 2, mMinTextSize); textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize); } } // text length has been decreased else { // if max test size is set then add it to while condition if (mMaxTextSize != 0) { while (textHeight < heightLimit && mTextSize <= mMaxTextSize) { mTextSize = mTextSize + 2; textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize); } } else { while (textHeight < heightLimit) { mTextSize = mTextSize + 2; textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize); } } mTextSize = textHeight > heightLimit ? mTextSize - 2 : mTextSize; } // If we had reached our minimum text size and still don't fit, append // an ellipsis if (mAddEllipsis && mTextSize == mMinTextSize && textHeight > heightLimit) { // Draw using a static layout TextPaint paint = new TextPaint(textPaint); StaticLayout layout = new StaticLayout(text, paint, widthLimit, 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(heightLimit) - 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 = paint.measureText(mEllipsis); // Trim characters off until we have enough room to draw the // ellipsis while (widthLimit < lineWidth + ellipseWidth) { lineWidth = paint.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, mTextSize); setLineSpacing(mSpacingAdd, mSpacingMult); } // 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 originalPaint, int width, float textSize) { // Update the text paint object TextPaint paint = new TextPaint(originalPaint); paint.setTextSize(textSize); // Measure using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); return layout.getHeight(); } } 

You can use the android.text.StaticLayout class for this. That’s what TextView uses internally.

I just created the following method (based on the ideas of Chase) which might help you if you want to draw text to any canvas:

 private static void drawText(Canvas canvas, int xStart, int yStart, int xWidth, int yHeigth, String textToDisplay, TextPaint paintToUse, float startTextSizeInPixels, float stepSizeForTextSizeSteps) { // Text view line spacing multiplier float mSpacingMult = 1.0f; // Text view additional line spacing float mSpacingAdd = 0.0f; StaticLayout l = null; do { paintToUse.setTextSize(startTextSizeInPixels); startTextSizeInPixels -= stepSizeForTextSizeSteps; l = new StaticLayout(textToDisplay, paintToUse, xWidth, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); } while (l.getHeight() > yHeigth); int textCenterX = xStart + (xWidth / 2); int textCenterY = (yHeigth - l.getHeight()) / 2; canvas.save(); canvas.translate(textCenterX, textCenterY); l.draw(canvas); canvas.restre(); } 

This could be used eg in any onDraw() method of any custom view.

Here’s yet another solution, just for kicks. It’s probably not very efficient, but it does cope with both height and width of the text, and with marked-up text.

 @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { if ((MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) && (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED)) { final float desiredWidth = MeasureSpec.getSize(widthMeasureSpec); final float desiredHeight = MeasureSpec.getSize(heightMeasureSpec); float textSize = getTextSize(); float lastScale = Float.NEGATIVE_INFINITY; while (textSize > MINIMUM_AUTO_TEXT_SIZE_PX) { // Measure how big the textview would like to be with the current text size. super.onMeasure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); // Calculate how much we'd need to scale it to fit the desired size, and // apply that scaling to the text size as an estimate of what we need. final float widthScale = desiredWidth / getMeasuredWidth(); final float heightScale = desiredHeight / getMeasuredHeight(); final float scale = Math.min(widthScale, heightScale); // If we don't need to shrink the text, or we don't seem to be converging, we're done. if ((scale >= 1f) || (scale <= lastScale)) { break; } // Shrink the text size and keep trying. textSize = Math.max((float) Math.floor(scale * textSize), MINIMUM_AUTO_TEXT_SIZE_PX); setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); lastScale = scale; } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } 

I combined some of the above suggestions to make one that scales up and down, with bisection method. It also scales within the width.

 /** * DO WHAT YOU WANT TO PUBLIC LICENSE * Version 2, December 2004 * * Copyright (C) 2004 Sam Hocevar  * * 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 = 10; // Minimum text size for this text view public static final float MAX_TEXT_SIZE = 128; private static final int BISECTION_LOOP_WATCH_DOG = 30; // 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 = MAX_TEXT_SIZE; // 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() { // Height and width with a padding as a percentage of height 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; } // Get the text view's paint object TextPaint textPaint = getPaint(); // Store the current text size float oldTextSize = textPaint.getTextSize(); // Bisection method: fast & precise float lower = mMinTextSize; float upper = mMaxTextSize; int loop_counter = 1; float targetTextSize = (lower + upper) / 2; int textHeight = getTextHeight(text, textPaint, width, targetTextSize); int textWidth = getTextWidth(text, textPaint, width, targetTextSize); while (loop_counter < BISECTION_LOOP_WATCH_DOG && upper - lower > 1) { targetTextSize = (lower + upper) / 2; textHeight = getTextHeight(text, textPaint, width, targetTextSize); textWidth = getTextWidth(text, textPaint, width, targetTextSize); if (textHeight > (height) || textWidth > (width)) upper = targetTextSize; else lower = targetTextSize; loop_counter++; } targetTextSize = lower; 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 paintCopy = new TextPaint(textPaint); paintCopy.setTextSize(targetTextSize); StaticLayout layout = new StaticLayout(text, paintCopy, 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 = paintCopy.measureText(mEllipsis); // Trim characters off until we have enough room to draw the // ellipsis while (width < lineWidth + ellipseWidth) { lineWidth = paintCopy.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 originalPaint, 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 paint = new TextPaint(originalPaint); // Update the text paint object paint.setTextSize(textSize); // Measure using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); return layout.getHeight(); } // Set the text size of the text paint object and use a static layout to // render text off screen before measuring private int getTextWidth(CharSequence source, TextPaint originalPaint, int width, float textSize) { // Update the text paint object TextPaint paint = new TextPaint(originalPaint); // Draw using a static layout paint.setTextSize(textSize); StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); return (int) layout.getLineWidth(0); } } 

I have use code from chase and M-WaJeEh and I found some advantage & disadvantage here

from chase

Advantage:

  • it’s perfect for 1 line TextView

Disadvantage:

  • if it’s more than 1 line with custom font some of text will disappear

  • if it’s enable ellipse, it didn’t prepare space for ellipse

  • if it’s custom font (typeface), it didn’t support

from M-WaJeEh

Advantage:

  • it’s perfect for multi-line

Disadvantage:

  • if set height as wrap-content, this code will start from minimum size and it will reduce to smallest as it can, not from the setSize and reduce by the limited width

  • if it’s custom font (typeface), it didn’t support

If anyone needs it, here is the same code snippet but for Xamarin.Android.

 /** * DO WHAT YOU WANT TO PUBLIC LICENSE * Version 1, December 2017 * * Copyright (C) 2017 Nathan Westfall * * 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. */ using Android.Content; using Android.Runtime; using Android.Widget; using Android.Util; using Android.Text; using Java.Lang; namespace My.Text { public class AutoResizeTextView : TextView { public const float MIN_TEXT_SIZE = 20; public interface OnTextResizeListener { void OnTextResize(TextView textView, float oldSize, float newSize); } private const string mEllipsis = "..."; private OnTextResizeListener mTextResizeListener; private bool mNeedsResize = false; private float mTextSize; private float mMaxTextSize = 0; private float mMinTextSize = MIN_TEXT_SIZE; private float mSpacingMult = 1.0f; private float mSpacingAdd = 0.0f; public bool AddEllipsis { get; set; } = true; public AutoResizeTextView(Context context) : this(context, null) { } public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0) { } public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyle): base(context, attrs, defStyle) { mTextSize = TextSize; } protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter) { base.OnTextChanged(text, start, lengthBefore, lengthAfter); mNeedsResize = true; ResetTextSize(); } protected override void OnSizeChanged(int w, int h, int oldw, int oldh) { base.OnSizeChanged(w, h, oldw, oldh); if (w != oldw || h != oldh) mNeedsResize = true; } public void SetOnResizeListener(OnTextResizeListener listener) { mTextResizeListener = listener; } public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size) { base.SetTextSize(unit, size); mTextSize = TextSize; } public override void SetLineSpacing(float add, float mult) { base.SetLineSpacing(add, mult); mSpacingMult = mult; mSpacingAdd = add; } public void SetMaxTextSize(float maxTextSize) { mMaxTextSize = maxTextSize; RequestLayout(); Invalidate(); } public float GetMaxTextSize() { return mMaxTextSize; } public void SetMinTextSize(float minTextSize) { mMinTextSize = minTextSize; RequestLayout(); Invalidate(); } public float GetMinTextSize() { return mMinTextSize; } public void ResetTextSize() { if(mTextSize > 0) { base.SetTextSize(ComplexUnitType.Px, mTextSize); mMaxTextSize = mTextSize; } } protected override void OnLayout(bool changed, int left, int top, int right, int bottom) { if(changed || mNeedsResize) { int widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight; int heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop; ResizeText(widthLimit, heightLimit); } base.OnLayout(changed, left, top, right, bottom); base.OnLayout(changed, left, top, right, bottom); } public void ResizeText() { int heightLimit = Height - PaddingBottom - PaddingTop; int widthLimit = Width - PaddingLeft - PaddingRight; ResizeText(widthLimit, heightLimit); } public void ResizeText(int width, int height) { var text = TextFormatted; if (text == null || text.Length() == 0 || height <= 0 || width <= 0 || mTextSize == 0) return; if (TransformationMethod != null) text = TransformationMethod.GetTransformationFormatted(TextFormatted, this); TextPaint textPaint = Paint; float oldTextSize = textPaint.TextSize; float targetTextSize = mMaxTextSize > 0 ? System.Math.Min(mTextSize, mMaxTextSize) : mTextSize; int textHeight = GetTextHeight(text, textPaint, width, targetTextSize); while(textHeight > height && targetTextSize > mMinTextSize) { targetTextSize = System.Math.Max(targetTextSize - 2, mMinTextSize); textHeight = GetTextHeight(text, textPaint, width, targetTextSize); } if(AddEllipsis && targetTextSize == mMinTextSize && textHeight > height) { TextPaint paint = new TextPaint(textPaint); StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false); if(layout.LineCount > 0) { int lastLine = layout.GetLineForVertical(height) - 1; if (lastLine < 0) SetText("", BufferType.Normal); else { int start = layout.GetLineStart(lastLine); int end = layout.GetLineEnd(lastLine); float lineWidth = layout.GetLineWidth(lastLine); float ellipseWidth = textPaint.MeasureText(mEllipsis); while (width < lineWidth + ellipseWidth) lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1).ToString()); SetText(text.SubSequence(0, end) + mEllipsis, BufferType.Normal); } } } SetTextSize(ComplexUnitType.Px, targetTextSize); SetLineSpacing(mSpacingAdd, mSpacingMult); mTextResizeListener?.OnTextResize(this, oldTextSize, targetTextSize); mNeedsResize = false; } private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize) { TextPaint paintCopy = new TextPaint(paint); paintCopy.TextSize = textSize; StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false); return layout.Height; } } } 

Providing this version of top answer rewritten on C# for those who codes on Xamarin.Android. Worked for me well.

  /** * DO WHAT YOU WANT TO PUBLIC LICENSE * Version 2, December 2004 * * Copyright (C) 2004 Sam Hocevar  * * 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. */ using System; using Android.Content; using Android.Runtime; using Android.Text; using Android.Util; using Android.Widget; using Java.Lang; namespace App.GuestGuide.Droid.Controls { public class OnTextResizeEventArgs : EventArgs { public TextView TextView { get; set; } public float OldSize { get; set; } public float NewSize { get; set; } } ///  ///  /// 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. ///  public class AutoResizeTextView : TextView { ///  /// Minimum text size for this text view ///  public static float MIN_TEXT_SIZE = 10; ///  /// Our ellipse string ///  private const string Ellipsis = "..."; private float _mMaxTextSize; private float _mMinTextSize = MIN_TEXT_SIZE; ///  /// Register subscriber to receive resize notifications ///  public event EventHandler OnTextResize; ///  /// Flag for text and/or size changes to force a resize ///  private bool _needsResize; ///  /// Text size that is set from code. This acts as a starting point for resizing ///  private float _textSize; ///  /// Text view line spacing multiplier ///  private float _spacingMult = 1.0f; ///  /// Text view additional line spacing ///  private float _spacingAdd; ///  /// Add ellipsis to text that overflows at the smallest text size ///  public bool ShouldAddEllipsis { get; set; } ///  ///  /// Override the set text size to update our internal reference values ///  public override float TextSize { get => base.TextSize; set { base.TextSize = value; _textSize = TextSize; } } ///  /// Temporary upper bounds on the starting text size ///  public float MaxTextSize { get => _mMaxTextSize; // Set the upper text size limit and invalidate the view set { _mMaxTextSize = value; RequestLayout(); Invalidate(); } } ///  /// Lower bounds for text size ///  public float MinTextSize { get => _mMinTextSize; //Set the lower text size limit and invalidate the view set { _mMinTextSize = value; RequestLayout(); Invalidate(); } } public AutoResizeTextView(Context context) : this(context, null) { } public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0) { } public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) { _textSize = TextSize; } public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes) { _textSize = TextSize; } protected AutoResizeTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { _textSize = TextSize; } ///  ///  /// When text changes, set the force resize flag to true and reset the text size. ///  ///  ///  ///  ///  protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter) { _needsResize = 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 ///  ///  ///  ///  ///  protected override void OnSizeChanged(int w, int h, int oldw, int oldh) { if (w != oldw || h != oldh) { _needsResize = true; } } public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size) { base.SetTextSize(unit, size); _textSize = TextSize; } ///  ///  /// Override the set line spacing to update our internal reference values ///  ///  ///  public override void SetLineSpacing(float add, float mult) { base.SetLineSpacing(add, mult); _spacingMult = mult; _spacingAdd = add; } ///  /// Reset the text to the original size ///  public void ResetTextSize() { if (_textSize > 0) { base.SetTextSize(ComplexUnitType.Px, _textSize); _mMaxTextSize = _textSize; } } ///  ///  /// Resize text after measuring ///  ///  ///  ///  ///  ///  protected override void OnLayout(bool changed, int left, int top, int right, int bottom) { if (changed || _needsResize) { var widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight; var heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop; ResizeText(widthLimit, heightLimit); } base.OnLayout(changed, left, top, right, bottom); } ///  /// Resize the text size with default width and height ///  public void ResizeText() { var heightLimit = Height - PaddingBottom - PaddingTop; var widthLimit = Width - PaddingLeft - PaddingRight; ResizeText(widthLimit, heightLimit); } ///  /// Resize the text size with specified width and height ///  ///  ///  public void ResizeText(int width, int height) { ICharSequence text = null; if (!string.IsNullOrEmpty(Text)) { text = new Java.Lang.String(Text); } // 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 || _textSize == 0) { return; } if (TransformationMethod != null) { text = TransformationMethod.GetTransformationFormatted(text, this); } // Get the text view's paint object var textPaint = Paint; // Store the current text size var oldTextSize = textPaint.TextSize; // If there is a max text size set, use the lesser of that and the default text size var targetTextSize = _mMaxTextSize > 0 ? System.Math.Min(_textSize, _mMaxTextSize) : _textSize; // Get the required text height var 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 = System.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 (ShouldAddEllipsis && targetTextSize == _mMinTextSize && textHeight > height) { // Draw using a static layout // modified: use a copy of TextPaint for measuring var paint = new TextPaint(textPaint); // Draw using a static layout var layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, false); // Check that we have a least one line of rendered text if (layout.LineCount > 0) { // Since the line at the specific vertical position would be cut off, // we must trim up to the previous line var lastLine = layout.GetLineForVertical(height) - 1; // If the text would not even fit on a single line, clear it if (lastLine < 0) { Text = string.Empty; } // Otherwise, trim to the previous line and add an ellipsis else { var start = layout.GetLineStart(lastLine); var end = layout.GetLineEnd(lastLine); var lineWidth = layout.GetLineWidth(lastLine); var ellipseWidth = textPaint.MeasureText(Ellipsis); // Trim characters off until we have enough room to draw the ellipsis while (width < lineWidth + ellipseWidth) { lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1)); } Text = text.SubSequence(0, end) + Ellipsis; } } } // Some devices try to auto adjust line spacing, so force default line spacing // and invalidate the layout as a side effect SetTextSize(ComplexUnitType.Px, targetTextSize); SetLineSpacing(_spacingAdd, _spacingMult); var notifyArgs = new OnTextResizeEventArgs { TextView = this, NewSize = targetTextSize, OldSize = oldTextSize }; // Notify the listener if registered OnTextResize?.Invoke(this, notifyArgs); // Reset force resize flag _needsResize = 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(ICharSequence 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) // Update the text paint object var paintCopy = new TextPaint(paint) { TextSize = textSize }; // Measure using a static layout var layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true); return layout.Height; } } } 

This solutions works for us:

 public class CustomFontButtonTextFit extends CustomFontButton { private final float DECREMENT_FACTOR = .1f; public CustomFontButtonTextFit(Context context) { super(context); } public CustomFontButtonTextFit(Context context, AttributeSet attrs) { super(context, attrs); } public CustomFontButtonTextFit(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } private synchronized void refitText(String text, int textWidth) { if (textWidth > 0) { float availableWidth = textWidth - this.getPaddingLeft() - this.getPaddingRight(); TextPaint tp = getPaint(); Rect rect = new Rect(); tp.getTextBounds(text, 0, text.length(), rect); float size = rect.width(); while(size > availableWidth) { setTextSize( getTextSize() - DECREMENT_FACTOR ); tp = getPaint(); tp.getTextBounds(text, 0, text.length(), rect); size = rect.width(); } } } @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); if(parentWidth < getSuggestedMinimumWidth()) parentWidth = getSuggestedMinimumWidth(); if(parentHeight < getSuggestedMinimumHeight()) parentHeight = getSuggestedMinimumHeight(); this.setMeasuredDimension(parentWidth, parentHeight); } @Override protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) { super.onTextChanged(text, start, before, after); refitText(text.toString(), this.getWidth()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (w != oldw) refitText(this.getText().toString(), w); } } 

Thanks to Chase and onoelle, for the lazy programmers, let me post here a working version of their fantastic merged code, adapted on a Button, instead of a TextView.

Substitute all your Buttons (not ImageButtons) with AutoResizeTextButtons and the same boring problem is fixed for them too.

Aquí está el código. I just removed the imports.

 /** * DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE * Version 2, December 2004 * * Copyright (C) 2004 Sam Hocevar  * * 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 THE FUCK YOU WANT TO PUBLIC LICENSE * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION * * 0. You just DO WHAT THE FUCK YOU WANT TO. * made better by onoelle * adapted for button by beppi */ /** * Text Button 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 AutoResizeTextButton extends Button { // 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(Button 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 AutoResizeTextButton(Context context) { this(context, null); } // Default constructor when inflating from XML file public AutoResizeTextButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } // Default constructor override public AutoResizeTextButton(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; } // 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); 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 // textPaint.setTextSize(targetTextSize); // modified: setting text size via this.setTextSize (instead of textPaint.setTextSize(targetTextSize)) 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 originalPaint, 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) // Update the text paint object TextPaint paint = new TextPaint(originalPaint); paint.setTextSize(textSize); // Measure using a static layout StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true); return layout.getHeight(); } } 

Uso:

put a AutoResizeTextButton inside your xml in replace of a normal Button, without changing anything else. Inside the onCreate() put (for example):

  myButton = (AutoResizeTextButton)getView().findViewById(id.myButton); myButton.setMinTextSize(8f); myButton.resizeText(); 

Here is the approach I take. Es muy sencillo. It uses successive approximation to zero in on the fontsize and can generally have it figured out in less than 10 iterations. Just replace “activityWidth” with the width of whatever view you are using to display the text in. In my example, it’s set as a private field to the screen’s width. The inital fontsize of 198 is only set in the event the method generates an exception (which really should never happen):

  private float GetFontSizeForScreenWidth(String text) { float fontsize = 198; try { Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.RED); Typeface typeface = Typeface.create("Helvetica", Typeface.BOLD); paint.setTypeface(typeface); paint.setTextAlign(Align.CENTER); int lowVal = 0; int highVal = 2000; int currentVal = highVal; /* * Successively approximate the screen size until it is * within 2 pixels of the maximum screen width. Generally * this will get you to the closest font size within about 10 * iterations. */ do { paint.setTextSize(currentVal); float textWidth = paint.measureText(text); float diff = activityWidth - textWidth; if ((diff >= 0) && (diff <= 2)) { fontsize = paint.getTextSize(); return fontsize; } if (textWidth > activityWidth) highVal = currentVal; else if (textWidth < activityWidth) lowVal = currentVal; else { fontsize = paint.getTextSize(); return fontsize; } currentVal = (highVal - lowVal) / 2 + lowVal; } while (true); } catch (Exception ex) { return fontsize; } } 

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); }