textview - 界限内如何自动缩放TextView文本?

  显示原文与译文双语对照的内容

我正在寻找一个最佳方法来调整 TextView 中环绕文本的大小,这样它就能适应它的getHeight和getWidth界限。 我不是简单的寻找一种方法来包装text-我想确保它既能包装,又能很小地适应屏幕。

我见过StackOverflow上的少数情况下,自动调整是得大,但他们要么是在一些特定的条件下与被黑解决方案,已经无法实现的,或者涉及 re-drawing TextView 递归的方法,直到它是足够小( 它是内存密集的,强迫用户在每次递归时看着文本收缩) 。

但是我确信有人已经找到了一个与我不一样的好解决方案: 编写几个重的例程来分析和测量文本,调整文本大小,然后重复直到找到合适的小尺寸。

TextView 用来包装文本的例程? 不能用来预测文本是否足够小?

在它的getHeight和 getWidth bounds, tl ;dr: 是否有best-practice办法auto-resize一个 TextView 要适宜,包装?

时间:

作为一个移动开发人员,我很遗憾没有找到任何支持自动调整大小的本机。 我的搜索没有找到任何适合我的东西,最后我花了半个周末的时间,并创建了自己的自动调整文本视图。 我会在这里发布代码,希望对其他人有用。

此类使用静态布局和原始文本视图的文本绘制来测量高度。 从那里,我踏并 remeasure,直到我有一个通过 2字体像素大小那就对了 最后,如果文本仍然不匹配,我将附加一个省略号。 我要求对文本进行动画处理并重用视图,这似乎在我拥有的设备上运行得很好,而且似乎运行得足够快。


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

}

警告。 有一个重要的固定 Bug 影响 Android 3.1 - 4.04导致所有AutoResizingTextView小部件无法工作。 请阅读:http://stackoverflow.com 21851157/2075875

更新: 以下的代码也将履行要求一个理想 AutoScaleTextView具体操作如下: 用于Android的 Auto-fit TextView,标记为赢家。

更新 2 maxlines: 支持,添加API级别 16之前,现在都可以正常工作。

更新 3: 支持 android:drawableLeftandroid:drawableRightandroid:drawableTop martinh并添加 android:drawableBottom 标签后,这要归功于简单的修复这里


我的需求有点不同。 我需要从,可能是一个有效的方法来调整大小,因为我在动画一个整型数 0到 ~4000 在 TextView 在 2秒钟内,我想相应调整它的大小。 我的解决方案有一点不同。 final 结果如下所示:

enter image description here

以及生成它的代码:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 android:padding="16dp"> 

 <com.vj.widgets.AutoResizeTextView
 android:layout_width="match_parent"
 android:layout_height="100dp"
 android:ellipsize="none"
 android:maxLines="2"
 android:text="Auto Resized Text, max 2 lines"
 android:textSize="100sp"/> <!-- maximum size -->

 <com.vj.widgets.AutoResizeTextView
 android:layout_width="match_parent"
 android:layout_height="100dp"
 android:ellipsize="none"
 android:gravity="center"
 android:maxLines="1"
 android:text="Auto Resized Text, max 1 line"
 android:textSize="100sp"/> <!-- maximum size -->

 <com.vj.widgets.AutoResizeTextView
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="Auto Resized Text"
 android:textSize="500sp"/> <!-- maximum size -->

</LinearLayout>

最后是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();
 }
}
}

我开始使用chase的解决方案,但在我的设备( 银河 Nexus,安卓 4.1 ) 上按预期方式运行之前,它必须适应两个方面:

  1. 使用TextPaint用于测量的副本布局的文档 TextView.getPaint() read-only 指出,它应该是使用,所以我有备份这两个位置都可以,我们用于测量油漆对象为是在

    
    //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. 添加一个单位来设置文本大小

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

有了这两个修改,解决方案对我来说是完美的,谢谢 Chase ! 我不知道是由于 Android 4.x,原来的解决方案不工作。 如果你想让它在你的设备上运行,或者测试它是否真的在你的设备上工作,你可以看看我的答题卡卡的标签,在那里使用这个解决方案来缩放一个答题卡的文本。 文本可以有任意长度,并且显示在不同的活动中,有时更小,有时更小一些,而且在横向+ 纵向模式下,我没有发现解决方案无法正常工作的任何角落案例。。

实际上,解决方案在 DialogTitle类的中。。 尽管它不如被接受的那样有效,但它更简单,易于适应。


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

}

我开始使用AutoResizeTextView的chase类,做了一个很小的改动,所以它既可以垂直也可以水平。

我还发现了一个 Bug,它在布局编辑器( 在 Eclipse 中) 中产生了一个空指针异常,在一些相当模糊的条件下。

更改 1: 将文本安排上下左右

跟踪版本的原始版本缩小文本大小,直到它垂直匹配,但允许文本比目标宽。 在我的例子中,我需要文本来适应指定的宽度。

这里更改使它的调整大小,直到文本既垂直又水平。

在,在 resizeText( int , int ) change:


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

到:


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

然后,在文件末尾,附加 getTextWidth() 例程;它只是一个稍微修改过的getTextHeight() 。 将它们合并为一个返回高度和宽度的例程可能会更加高效。


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




在 Eclipse 安卓布局编辑器中, 更改 2: 修复与 EmptyStackException

在相当模糊和非常精确的条件下,布局编辑器将无法显示布局的图形显示;它将引发"EmptyStackException: com.android.ide.eclipse.adt 中的空"例外。

需要的条件如下:
- 创建一个AutoResizeTextView小部件
- 为该部件创建一个样式
- 指定样式中的文本项;不在部件定义中

如中所示:

res/layout/main.xml:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:orientation="vertical"> 

 <com.ajw.DemoCrashInADT.AutoResizeTextView
 android:id="@+id/resizingText"
 style="@style/myTextStyle"/>

</LinearLayout>

res/values/myStyles.xml:


<?xml version="1.0" encoding="utf-8"?>
<resources>

 <style name="myTextStyle" parent="@android:style/Widget.TextView">
 <item name="android:layout_height">wrap_content</item>
 <item name="android:layout_width">fill_parent</item>
 <item name="android:text">some message</item>
 </style>

</resources>

有了这些文件,选择了当编辑 main.xml 图形布局选项卡将显示:

错误 !
EmptyStackException: 空
异常详细信息记录在窗口> 显示视图> 错误日志中

而不是布局的图形视图。

为了缩短一个已经too-long的故事,我在下面的一行中跟踪了它:


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

问题是在特定条件下,mTextSize从未初始化;它具有值 0.

在上面,targetTextSize 被设置为零( 由于 math.min ) 。

那零被传递给 getTextHeight() ( 和 getTextWidth() ) 作为 textSize 参数。 当它到达时
layout.draw(sTextResizeCanvas);
我们得到了例外。

如果 (mTextSize == 0)resizeText()的开始而不是在 getTextHeight()getTextWidth() 中测试,那么更高效的测试是更有效的;早期测试保存所有的介入。

通过这些更新,文件( 在我的crash-demo测试应用程序中) 现在是:


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

}



非常感谢你对初始代码的跟踪。 我喜欢阅读它,看看它是如何工作的,我很高兴能够添加到它。

Android 4的解决办法:

我发现AutoResizeTextView在我的Android 2.1 模拟器 上运行良好。 我非常喜欢它。 但不幸的是它在我自己的4.0.4手机和 4.1 模拟器 上失败了。 在尝试之后,我发现可以通过在xml类中添加以下属性轻松解决它:

android: ellipsize="无"

android: singleline="真"

上面的2行,现在AutoResizeTextView在我的2.1 & 4.1仿真器和我自己的4.0.4个手机上运行完美。

希望这对你有帮助。: - )

在安卓 Honeycomb 和冰激凌三明治, 警告,Bug

机器人版本:3.1 - 4.04有一个 Bug,在TextView内的setTextSize() 只适用于 1 st时间( 1 st调用) 。

这里描述了 Bug: http://code.google.com android/issues/detail?id=22493http://code.google.com android/issues/detail?id=17343#c9

解决办法是在更改大小之前向指定给TextView的文本添加新行字符:


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

我在代码中使用它,如下所示:


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

我在文本左边和右边添加了这个"u3000"字符,以使它的居中。 如果将它与左对齐,则只向右追加。 当然它也可以嵌入AutoResizeTextView小部件,但我想在外部保持修复代码。

我希望这对你有帮助


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

注意:之所以选择MAX_TEXT_SIZE以防的文本大小是大于 20,因为我不想让大字体适用于我的观点,如果不属于这种情形,你也可以很简单的将它的删除。

我的实现有点复杂,但附带了以下好处:

  • 将可用宽度和可用高度考虑在内
  • 使用单行和多行标签
  • 在点击最小字体大小时使用省略号
  • 由于内部文本表示已经更改,请在单独的变量中记住最初设置的文本
  • 确保画布总是尽可能大,同时使用所有可用的父级高度

/**
 * 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
 * &lt;http://stackoverflow.com/a/5535672/305532>
 *
 * @author Thomas Keller &lt;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
//<http://code.google.com/p/android/issues/detail?id=22493>
//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 ]

  • 修正省略号的位置( off-by-one错误)
  • 返工尺寸计算;现在总是全文包括换行符的纯文本的行,只是增加了两个单的高度测量时测量出来的,要修复问题会导致同样的结果并不是作为文本作为一个整体的高度的测量
  • 不用循环寻找最小的可用文本大小,只需在第一次测量后计算

我的需要是调整文本的大小以完全适应视图边界。 chase的解决方案减少了文本的大小,如果有足够的空间,它也会放大文本。

resizeText() method,,使所有快速&精确我用一个分区方法,而不是一个迭代,而随着你可以看看。 这就是为什么你也有一个 MAX_TEXT_SIZE 选项。 我还包括了onoelle的一些技巧。

在 Android 4.4上测试


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

}

...