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

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

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

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

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


当前回答

你可以使用android.text.StaticLayout类。这就是TextView内部使用的。

其他回答

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

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

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

考虑到可用的宽度和高度 适用于单行和多行标签 使用省略号,以防达到最小字体大小 由于内部文本表示形式已更改,因此将最初设置的文本保存在单独的变量中 确保画布总是只有它需要的大小,而它使用所有可用的父元素高度

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

[修订于2012-11-21]

修正了省略号的位置(差一个错误) 重做文本大小计算;现在,总是测量包括换行符在内的全文,以解决当两个单独的测量线的高度相加与整个文本高度测量结果不相同时的问题 而不是循环寻找最小的可用文本大小,只需在第一次测量后计算它

我从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发布了初始代码。我很喜欢通读它,看看它是如何工作的,我很高兴能够添加它。

AppcompatTextView现在支持从支持库26.0开始自动调整大小。TextView在Android O也以同样的方式工作。更多信息可以在这里找到。一个简单的演示程序可以在这里找到。

<LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

      <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:autoSizeTextType="uniform"
        app:autoSizeMinTextSize="12sp"
        app:autoSizeMaxTextSize="100sp"
        app:autoSizeStepGranularity="2sp"
      />

</LinearLayout>

我只是借用了一些其他人的想法,并在下面写了一些代码,可能会有帮助。

import android.content.Context;
import android.graphics.Canvas;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;

public class AutoResizeTextView extends TextView {
    private static final int MAX_SIZE = 1000;

    private static final int MIN_SIZE = 5;

    private TextPaint mTextPaint;

    private float mSpacingMult = 1.0f;

    private float mSpacingAdd = 0.0f;

    private boolean needAdapt = false;

    private boolean adapting = false;

    public AutoResizeTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public AutoResizeTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public AutoResizeTextView(Context context) {
        super(context);
        init();
    }

    private void init() {
        mTextPaint = new TextPaint();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (adapting) {
            return;
        }
        if (needAdapt) {
            adaptTextSize();
        } else {
            super.onDraw(canvas);
        }
    }

    private void adaptTextSize() {
        CharSequence text = getText();
        int viewWidth = getMeasuredWidth();
        int viewHeight = getMeasuredHeight();

        if (viewWidth==0 || viewHeight==0
                || TextUtils.isEmpty(text)) {
            return;
        }

        adapting = true;
        /* binary search */
        int bottom=MIN_SIZE, top=MAX_SIZE, mid = 0;
        while (bottom <= top) {
            mid = (bottom + top)/2;
            mTextPaint.setTextSize(mid);
            int textWidth = (int) mTextPaint.measureText(text, 0, text.length());
            int textHeight = getTextHeight(text, viewWidth);
            if (textWidth<viewWidth && textHeight<viewHeight) {
                bottom = mid+1;
            } else {
                top = mid-1;
            }
        }

        int newSize = mid-1;
        setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);

        adapting=false;
        needAdapt = false;

        invalidate();
    }

    private int getTextHeight(CharSequence text, int targetWidth) {
        StaticLayout layout = new StaticLayout(text, mTextPaint, targetWidth,
                Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd, true);
        return layout.getHeight();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        needAdapt = true;
    }

    @Override
    protected void onTextChanged(CharSequence text, int start,
            int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        needAdapt = true;
    }

    @Override
    public void setLineSpacing(float add, float mult) {
        super.setLineSpacing(add, mult);
        mSpacingMult = mult;
        mSpacingAdd = add;
    }
}