我正在寻找一种最佳的方法来调整包装文本在一个TextView,使它将适合它的getHeight和getWidth界限。我不是简单地寻找一种方法来包装文本-我想确保它既包装,又足够小,完全适合在屏幕上。

我在StackOverflow上看到了一些需要自动调整大小的情况,但它们要么是非常特殊的情况下的hack解决方案,没有解决方案,或涉及重新绘制TextView递归直到它足够小(这是内存紧张,迫使用户观看文本收缩一步一步与每次递归)。

但我相信有人已经找到了一个很好的解决方案,它不涉及我正在做的事情:编写几个繁重的例程来解析和测量文本,调整文本的大小,然后重复,直到找到一个合适的小尺寸。

TextView使用什么例程来包装文本?难道这些不能用来预测文本是否足够小吗?

是否有一个最佳实践的方法来自动调整TextView的大小,以适应,包装,在它的getHeight和getWidth边界?


当前回答

作为一名手机开发者,我很遗憾地发现没有任何原生的支持自动调整大小的功能。我的搜索没有找到任何对我有用的东西,最后,我花了大半个周末的时间创建了我自己的自动调整文本视图。我将在这里发布代码,希望它对其他人有用。

这个类使用带有原始文本视图的文本绘制的静态布局来测量高度。从那里,我后退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();
    }

}

警告。有一个影响Android 3.1 - 4.04的重要修复错误,导致所有AutoResizingTextView小部件无法工作。请阅读:https://stackoverflow.com/a/21851157/2075875

其他回答

更新:下面的代码也满足了一个理想的AutoScaleTextView的要求,如这里所述:自动适合Android的TextView,并被标记为赢家。

更新2:增加了对maxlines的支持,现在在API级别16之前工作正常。

更新3:支持android: drawablleft, android:drawableRight, android:drawableTop和android:drawableBottom标签添加,感谢MartinH在这里的简单修复。


我的要求有点不同。我需要一种有效的方法来调整大小,因为我是动画一个整数从,可能是0 ~4000在TextView在2秒内,我想相应地调整大小。我的解决方法有点不同。这是最终的结果:

以及产生它的代码:

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

这里还有另一个解决方案,只是为了好玩。它可能不是很有效,但它确实处理了文本的高度和宽度,以及有标记的文本。

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

我需要调整文本的大小,以完美地适应视图边界。Chase的解决方案只是减小文本大小,如果有足够的空间,这个方案也会扩大文本。

为了使所有的快速和精确,我使用了平分方法而不是迭代while,正如你可以在resizeText()方法中看到的那样。这就是为什么你也有一个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();
    }

}

我从Chase的AutoResizeTextView类开始,并做了一个微小的更改,以便它可以垂直和水平地适应。

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

更改1:垂直和水平对齐文本

Chase的原始版本减小了文本大小,直到垂直对齐,但允许文本比目标更宽。在我的例子中,我需要文本符合指定的宽度。

这一更改使它调整大小,直到文本垂直和水平都适合。

在resizeText(int,int)中更改为:

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

to:

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

变更2:修复Eclipse Android布局编辑器中的EmptyStackException

在相当模糊和非常精确的条件下,布局编辑器将无法显示布局的图形显示;它会在com.android.ide.eclipse.adt中抛出一个"EmptyStackException: null"异常。

所需的条件是: -创建一个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 /价值/ 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:零 “> Show View > Error Log”记录异常详细信息

而不是布局的图形视图。

为了让已经太长的故事变得更短,我追踪到以下几行(同样是在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;

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

通过上述操作,targetTextSize被设置为零(这是Math.min的结果)。

这个零被传递给getTextHeight()(和getTextWidth())作为textSize参数。当它到达 layout.draw (sTextResizeCanvas); 我们得到了异常。

在resizeText()的开始测试if (mTextSize == 0)比在getTextHeight()和getTextWidth()测试更有效;更早地测试可以节省所有的中间工作。

通过这些更新,文件(就像在我的崩溃演示测试应用程序中一样)现在是:

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

}

非常感谢Chase发布了初始代码。我很喜欢通读它,看看它是如何工作的,我很高兴能够添加它。

以下是我采取的方法。这很简单。它使用连续逼近来集中于字体大小,通常可以在不到10次迭代中计算出来。只需将“activityWidth”替换为你用来显示文本的视图的宽度。在我的示例中,它被设置为与屏幕宽度相同的私有字段。初始fontsize 198只在该方法产生异常的情况下设置(这真的不应该发生):

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