package com.lb.auto_fit_textview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.RectF; import android.graphics.Typeface; import android.os.Build; import android.support.v7.widget.AppCompatTextView; import android.text.Layout.Alignment; import android.text.StaticLayout; import android.text.TextPaint; import android.text.method.TransformationMethod; import android.util.AttributeSet; import android.util.TypedValue; /** * a textView that is able to self-adjust its font size depending on the min and max size of the font, and its own size.<br/> * code is heavily based on this StackOverflow thread: * http://stackoverflow.com/questions/16017165/auto-fit-textview-for-android/21851239#21851239 <br/> * It should work fine with most Android versions, but might have some issues on Android 3.1 - 4.04, as setTextSize will only work for the first time. <br/> * More info here: https://code.google.com/p/android/issues/detail?id=22493 and here in case you wish to fix it: http://stackoverflow.com/a/21851239/878126 */ public class AutoResizeTextView extends AppCompatTextView { private static final int NO_LINE_LIMIT = -1; private final RectF _availableSpaceRect = new RectF(); private final SizeTester _sizeTester; private float _maxTextSize, _spacingMult = 1.0f, _spacingAdd = 0.0f, _minTextSize; private int _widthLimit, _maxLines; private boolean _initialized = false; private TextPaint _paint; 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 */ int onTestSize(int suggestedSize, RectF availableSpace); } public AutoResizeTextView(final Context context) { this(context, null, android.R.attr.textViewStyle); } public AutoResizeTextView(final Context context, final AttributeSet attrs) { this(context, attrs, android.R.attr.textViewStyle); } public AutoResizeTextView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); // using the minimal recommended font size _minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics()); _maxTextSize = getTextSize(); _paint = new TextPaint(getPaint()); if (_maxLines == 0) // no value was assigned during construction _maxLines = NO_LINE_LIMIT; // prepare size tester: _sizeTester = new SizeTester() { final RectF textRect = new RectF(); @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public int onTestSize(final int suggestedSize, final RectF availableSpace) { _paint.setTextSize(suggestedSize); final TransformationMethod transformationMethod = getTransformationMethod(); final String text; if (transformationMethod != null) text = transformationMethod.getTransformation(getText(), AutoResizeTextView.this).toString(); else text = getText().toString(); final boolean singleLine = getMaxLines() == 1; if (singleLine) { textRect.bottom = _paint.getFontSpacing(); textRect.right = _paint.measureText(text); } else { final StaticLayout layout = new StaticLayout(text, _paint, _widthLimit, Alignment.ALIGN_NORMAL, _spacingMult, _spacingAdd, true); // return early if we have more lines if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines()) return 1; textRect.bottom = layout.getHeight(); int maxWidth = -1; int lineCount = layout.getLineCount(); for (int i = 0; i < lineCount; i++) { int end = layout.getLineEnd(i); if (i < lineCount - 1 && end > 0 && !isValidWordWrap(text.charAt(end - 1), text.charAt(end))) return 1; if (maxWidth < layout.getLineRight(i) - layout.getLineLeft(i)) maxWidth = (int) layout.getLineRight(i) - (int) layout.getLineLeft(i); } //for (int i = 0; i < layout.getLineCount(); i++) // if (maxWidth < layout.getLineRight(i) - layout.getLineLeft(i)) // maxWidth = (int) layout.getLineRight(i) - (int) layout.getLineLeft(i); textRect.right = maxWidth; } textRect.offsetTo(0, 0); if (availableSpace.contains(textRect)) // may be too small, don't worry we will find the best match return -1; // else, too big return 1; } }; _initialized = true; } public boolean isValidWordWrap(char before, char after) { return before == ' ' || before == '-'; } @Override public void setAllCaps(boolean allCaps) { super.setAllCaps(allCaps); adjustTextSize(); } @Override public void setTypeface(final Typeface tf) { super.setTypeface(tf); adjustTextSize(); } @Override public void setTextSize(final float size) { _maxTextSize = size; adjustTextSize(); } @Override public void setMaxLines(final int maxLines) { super.setMaxLines(maxLines); _maxLines = maxLines; adjustTextSize(); } @Override public int getMaxLines() { return _maxLines; } @Override public void setSingleLine() { super.setSingleLine(); _maxLines = 1; adjustTextSize(); } @Override public void setSingleLine(final boolean singleLine) { super.setSingleLine(singleLine); if (singleLine) _maxLines = 1; else _maxLines = NO_LINE_LIMIT; adjustTextSize(); } @Override public void setLines(final int lines) { super.setLines(lines); _maxLines = lines; adjustTextSize(); } @Override public void setTextSize(final int unit, final float size) { final Context c = getContext(); Resources r; if (c == null) r = Resources.getSystem(); else r = c.getResources(); _maxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics()); adjustTextSize(); } @Override public void setLineSpacing(final float add, final float mult) { super.setLineSpacing(add, mult); _spacingMult = mult; _spacingAdd = add; } /** * Set the lower text size limit and invalidate the view * * @param minTextSize */ public void setMinTextSize(final float minTextSize) { _minTextSize = minTextSize; adjustTextSize(); } private void adjustTextSize() { // This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14 // TODO think of a nicer, elegant solution. // post(new Runnable() // { // @Override // public void run() // { if (!_initialized) return; final int startSize = (int) _minTextSize; final int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop(); _widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(); if (_widthLimit <= 0) return; _paint = new TextPaint(getPaint()); _availableSpaceRect.right = _widthLimit; _availableSpaceRect.bottom = heightLimit; superSetTextSize(startSize); // } // }); } private void superSetTextSize(int startSize) { int textSize = binarySearch(startSize, (int) _maxTextSize, _sizeTester, _availableSpaceRect); super.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); } private int binarySearch(final int start, final int end, final SizeTester sizeTester, final RectF availableSpace) { int lastBest = start, lo = start, hi = end - 1, mid; while (lo <= hi) { mid = lo + hi >>> 1; final 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); adjustTextSize(); } @Override protected void onSizeChanged(final int width, final int height, final int oldwidth, final int oldheight) { super.onSizeChanged(width, height, oldwidth, oldheight); if (width != oldwidth || height != oldheight) adjustTextSize(); } }