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

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

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

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

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


当前回答

如果有人需要它,这里是相同的代码片段,但Xamarin.Android。

/**
 *               DO WHAT YOU WANT TO PUBLIC LICENSE
 *                    Version 1, December 2017
 * 
 * Copyright (C) 2017 Nathan Westfall
 * 
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 * 
 *            DO WHAT YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 * 
 *  0. You just DO WHAT YOU WANT TO.
 */

using Android.Content;
using Android.Runtime;
using Android.Widget;
using Android.Util;
using Android.Text;

using Java.Lang;

namespace My.Text
{
    public class AutoResizeTextView : TextView
    {
        public const float MIN_TEXT_SIZE = 20;

        public interface OnTextResizeListener
        {
            void OnTextResize(TextView textView, float oldSize, float newSize);
        }

        private const string mEllipsis = "...";

        private OnTextResizeListener mTextResizeListener;

        private bool mNeedsResize = false;

        private float mTextSize;

        private float mMaxTextSize = 0;

        private float mMinTextSize = MIN_TEXT_SIZE;

        private float mSpacingMult = 1.0f;

        private float mSpacingAdd = 0.0f;

        public bool AddEllipsis { get; set; } = true;

        public AutoResizeTextView(Context context) : this(context, null) { }

        public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0) { }

        public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyle): base(context, attrs, defStyle)
        {
            mTextSize = TextSize;
        }

        protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
        {
            base.OnTextChanged(text, start, lengthBefore, lengthAfter);

            mNeedsResize = true;

            ResetTextSize();
        }

        protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
        {
            base.OnSizeChanged(w, h, oldw, oldh);

            if (w != oldw || h != oldh)
                mNeedsResize = true;
        }

        public void SetOnResizeListener(OnTextResizeListener listener)
        {
            mTextResizeListener = listener;
        }

        public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
        {
            base.SetTextSize(unit, size);

            mTextSize = TextSize;
        }

        public override void SetLineSpacing(float add, float mult)
        {
            base.SetLineSpacing(add, mult);

            mSpacingMult = mult;
            mSpacingAdd = add;
        }

        public void SetMaxTextSize(float maxTextSize)
        {
            mMaxTextSize = maxTextSize;
            RequestLayout();
            Invalidate();
        }

        public float GetMaxTextSize()
        {
            return mMaxTextSize;
        }

        public void SetMinTextSize(float minTextSize)
        {
            mMinTextSize = minTextSize;
            RequestLayout();
            Invalidate();
        }

        public float GetMinTextSize()
        {
            return mMinTextSize;
        }

        public void ResetTextSize()
        {
            if(mTextSize > 0)
            {
                base.SetTextSize(ComplexUnitType.Px, mTextSize);
                mMaxTextSize = mTextSize;
            }
        }

        protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
        {
            if(changed || mNeedsResize)
            {
                int widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
                int heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
                ResizeText(widthLimit, heightLimit);
            }
            base.OnLayout(changed, left, top, right, bottom);

            base.OnLayout(changed, left, top, right, bottom);
        }

        public void ResizeText()
        {
            int heightLimit = Height - PaddingBottom - PaddingTop;
            int widthLimit = Width - PaddingLeft - PaddingRight;
            ResizeText(widthLimit, heightLimit);
        }

        public void ResizeText(int width, int height)
        {
            var text = TextFormatted;
            if (text == null || text.Length() == 0 || height <= 0 || width <= 0 || mTextSize == 0)
                return;

            if (TransformationMethod != null)
                text = TransformationMethod.GetTransformationFormatted(TextFormatted, this);

            TextPaint textPaint = Paint;

            float oldTextSize = textPaint.TextSize;
            float targetTextSize = mMaxTextSize > 0 ? System.Math.Min(mTextSize, mMaxTextSize) : mTextSize;

            int textHeight = GetTextHeight(text, textPaint, width, targetTextSize);

            while(textHeight > height && targetTextSize > mMinTextSize)
            {
                targetTextSize = System.Math.Max(targetTextSize - 2, mMinTextSize);
                textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
            }

            if(AddEllipsis && targetTextSize == mMinTextSize && textHeight > height)
            {
                TextPaint paint = new TextPaint(textPaint);

                StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false);
                if(layout.LineCount > 0)
                {
                    int lastLine = layout.GetLineForVertical(height) - 1;
                    if (lastLine < 0)
                        SetText("", BufferType.Normal);
                    else
                    {
                        int start = layout.GetLineStart(lastLine);
                        int end = layout.GetLineEnd(lastLine);
                        float lineWidth = layout.GetLineWidth(lastLine);
                        float ellipseWidth = textPaint.MeasureText(mEllipsis);

                        while (width < lineWidth + ellipseWidth)
                            lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1).ToString());
                        SetText(text.SubSequence(0, end) + mEllipsis, BufferType.Normal);
                    }
                }
            }

            SetTextSize(ComplexUnitType.Px, targetTextSize);
            SetLineSpacing(mSpacingAdd, mSpacingMult);

            mTextResizeListener?.OnTextResize(this, oldTextSize, targetTextSize);

            mNeedsResize = false;
        }

        private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize)
        {
            TextPaint paintCopy = new TextPaint(paint);
            paintCopy.TextSize = textSize;
            StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false);
            return layout.Height;
        }
    }
}

其他回答

因为我一直在寻找这个,而我刚刚找到了一个解决方案,但这里没有,我将把它写在这里,以供将来参考。

注意:这段代码是直接从谷歌Android棒棒糖拨号器一段时间后,我不记得如果改变在当时。此外,我不知道这是在哪个许可证下,但我有理由认为它是Apache 2.0。

类ResizeTextView,实际的视图

public class ResizeTextView extends TextView {

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

这个ResizeTextView类可以扩展TextView和它所有的孩子,我的理解,所以EditText以及。

ViewUtil类带有resizeText方法(…)

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.graphics.Paint;
import android.util.TypedValue;
import android.widget.TextView;

public class ViewUtil {

    private ViewUtil() {}

    public static void resizeText(TextView textView, int originalTextSize, int minTextSize) {
        final Paint paint = textView.getPaint();
        final int width = textView.getWidth();
        if (width == 0) return;
        textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
        float ratio = width / paint.measureText(textView.getText().toString());
        if (ratio <= 1.0f) {
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                    Math.max(minTextSize, originalTextSize * ratio));
        }
    }
}

您应该将您的视图设置为

<yourpackage.yourapp.ResizeTextView
            android:layout_width="match_parent"
            android:layout_height="64dp"
            android:gravity="center"
            android:maxLines="1"/>

希望能有所帮助!

如果有人需要它,这里是相同的代码片段,但Xamarin.Android。

/**
 *               DO WHAT YOU WANT TO PUBLIC LICENSE
 *                    Version 1, December 2017
 * 
 * Copyright (C) 2017 Nathan Westfall
 * 
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 * 
 *            DO WHAT YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 * 
 *  0. You just DO WHAT YOU WANT TO.
 */

using Android.Content;
using Android.Runtime;
using Android.Widget;
using Android.Util;
using Android.Text;

using Java.Lang;

namespace My.Text
{
    public class AutoResizeTextView : TextView
    {
        public const float MIN_TEXT_SIZE = 20;

        public interface OnTextResizeListener
        {
            void OnTextResize(TextView textView, float oldSize, float newSize);
        }

        private const string mEllipsis = "...";

        private OnTextResizeListener mTextResizeListener;

        private bool mNeedsResize = false;

        private float mTextSize;

        private float mMaxTextSize = 0;

        private float mMinTextSize = MIN_TEXT_SIZE;

        private float mSpacingMult = 1.0f;

        private float mSpacingAdd = 0.0f;

        public bool AddEllipsis { get; set; } = true;

        public AutoResizeTextView(Context context) : this(context, null) { }

        public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0) { }

        public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyle): base(context, attrs, defStyle)
        {
            mTextSize = TextSize;
        }

        protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
        {
            base.OnTextChanged(text, start, lengthBefore, lengthAfter);

            mNeedsResize = true;

            ResetTextSize();
        }

        protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
        {
            base.OnSizeChanged(w, h, oldw, oldh);

            if (w != oldw || h != oldh)
                mNeedsResize = true;
        }

        public void SetOnResizeListener(OnTextResizeListener listener)
        {
            mTextResizeListener = listener;
        }

        public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
        {
            base.SetTextSize(unit, size);

            mTextSize = TextSize;
        }

        public override void SetLineSpacing(float add, float mult)
        {
            base.SetLineSpacing(add, mult);

            mSpacingMult = mult;
            mSpacingAdd = add;
        }

        public void SetMaxTextSize(float maxTextSize)
        {
            mMaxTextSize = maxTextSize;
            RequestLayout();
            Invalidate();
        }

        public float GetMaxTextSize()
        {
            return mMaxTextSize;
        }

        public void SetMinTextSize(float minTextSize)
        {
            mMinTextSize = minTextSize;
            RequestLayout();
            Invalidate();
        }

        public float GetMinTextSize()
        {
            return mMinTextSize;
        }

        public void ResetTextSize()
        {
            if(mTextSize > 0)
            {
                base.SetTextSize(ComplexUnitType.Px, mTextSize);
                mMaxTextSize = mTextSize;
            }
        }

        protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
        {
            if(changed || mNeedsResize)
            {
                int widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
                int heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
                ResizeText(widthLimit, heightLimit);
            }
            base.OnLayout(changed, left, top, right, bottom);

            base.OnLayout(changed, left, top, right, bottom);
        }

        public void ResizeText()
        {
            int heightLimit = Height - PaddingBottom - PaddingTop;
            int widthLimit = Width - PaddingLeft - PaddingRight;
            ResizeText(widthLimit, heightLimit);
        }

        public void ResizeText(int width, int height)
        {
            var text = TextFormatted;
            if (text == null || text.Length() == 0 || height <= 0 || width <= 0 || mTextSize == 0)
                return;

            if (TransformationMethod != null)
                text = TransformationMethod.GetTransformationFormatted(TextFormatted, this);

            TextPaint textPaint = Paint;

            float oldTextSize = textPaint.TextSize;
            float targetTextSize = mMaxTextSize > 0 ? System.Math.Min(mTextSize, mMaxTextSize) : mTextSize;

            int textHeight = GetTextHeight(text, textPaint, width, targetTextSize);

            while(textHeight > height && targetTextSize > mMinTextSize)
            {
                targetTextSize = System.Math.Max(targetTextSize - 2, mMinTextSize);
                textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
            }

            if(AddEllipsis && targetTextSize == mMinTextSize && textHeight > height)
            {
                TextPaint paint = new TextPaint(textPaint);

                StaticLayout layout = new StaticLayout(text, paint, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false);
                if(layout.LineCount > 0)
                {
                    int lastLine = layout.GetLineForVertical(height) - 1;
                    if (lastLine < 0)
                        SetText("", BufferType.Normal);
                    else
                    {
                        int start = layout.GetLineStart(lastLine);
                        int end = layout.GetLineEnd(lastLine);
                        float lineWidth = layout.GetLineWidth(lastLine);
                        float ellipseWidth = textPaint.MeasureText(mEllipsis);

                        while (width < lineWidth + ellipseWidth)
                            lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1).ToString());
                        SetText(text.SubSequence(0, end) + mEllipsis, BufferType.Normal);
                    }
                }
            }

            SetTextSize(ComplexUnitType.Px, targetTextSize);
            SetLineSpacing(mSpacingAdd, mSpacingMult);

            mTextResizeListener?.OnTextResize(this, oldTextSize, targetTextSize);

            mNeedsResize = false;
        }

        private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize)
        {
            TextPaint paintCopy = new TextPaint(paint);
            paintCopy.TextSize = textSize;
            StaticLayout layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, mSpacingMult, mSpacingAdd, false);
            return layout.Height;
        }
    }
}

Android 4.x的一个变通方案:

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

android:ellipsize=“none” android:singleLine=“true”

有了上面的两行,现在AutoResizeTextView在我的2.1和4.1模拟器和我自己的4.0.4手机上完美地工作。

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

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

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

/**
 * 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的解决方案开始,但在我的设备(Galaxy Nexus, Android 4.1)上正常工作之前,我必须调整两件事:

using a copy of TextPaint for measuring layout The documentation for TextView.getPaint() states that it should be used read-only, so I made a copy in both places where we use the paint object for measuring: // 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); ... adding a unit to setting the text size // modified: setting text size via this.setTextSize (instead of textPaint.setTextSize(targetTextSize)) setTextSize(TypedValue.COMPLEX_UNIT_PX, targetTextSize); setLineSpacing(mSpacingAdd, mSpacingMult);

With these two modifications the solution is working perfectly for me, thanks Chase! I don't know whether it is due to Android 4.x that the original solution was not working. In case you want to see it in action or test whether it really works on your device, you can have a look at my flashcard app Flashcards ToGo where I use this solution to scale the text of a flashcard. The text can have arbitrary length, and the flashcards are displayed in different activities, sometimes smaller sometimes bigger, plus in landscape + portrait mode, and I haven't found any corner case where the solution would not work properly...