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

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

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

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

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


当前回答

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>

其他回答

我发现下面的方法对我很有用。它不循环,同时考虑高度和宽度。注意,在视图上调用setTextSize时指定PX单位是很重要的。

Paint paint = adjustTextSize(getPaint(), numChars, maxWidth, maxHeight);
setTextSize(TypedValue.COMPLEX_UNIT_PX,paint.getTextSize());

下面是我使用的例程,从视图中传入getPaint()。带有'wide'字符的10个字符的字符串用于估计独立于实际字符串的宽度。

private static final String text10="OOOOOOOOOO";
public static Paint adjustTextSize(Paint paint, int numCharacters, int widthPixels, int heightPixels) {
    float width = paint.measureText(text10)*numCharacters/text10.length();
    float newSize = (int)((widthPixels/width)*paint.getTextSize());
    paint.setTextSize(newSize);

    // remeasure with font size near our desired result
    width = paint.measureText(text10)*numCharacters/text10.length();
    newSize = (int)((widthPixels/width)*paint.getTextSize());
    paint.setTextSize(newSize);

    // Check height constraints
    FontMetricsInt metrics = paint.getFontMetricsInt();
    float textHeight = metrics.descent-metrics.ascent;
    if (textHeight > heightPixels) {
        newSize = (int)(newSize * (heightPixels/textHeight));
        paint.setTextSize(newSize);
    }

    return paint;
}

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

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

I needed a specific solution. I have got an edittext and textview in my layout. The textview is fixed height and width. When the user starts to type in the edittext, the text should immediately appear in the textview. The text in the textfield should auto - resize to fit the textview. So I updated Chase's solution to work for me. So when the text changes in the textview, resizing starts. The difference between mine and Chase's soluton: resizing is done even if the user DELETE some chars. I hope it can help someone.

public class TextFitTextView extends TextView {

// Minimum text size for this text view
public static final float MIN_TEXT_SIZE = 10;

// Maximum text size for this text view - if it is 0, then the text acts
// like match_parent
public static final float MAX_TEXT_SIZE = 0;

// Our ellipse string
private static final String mEllipsis = "...";

// Text size that is set from code. This acts as a starting point for
// resizing
private float mTextSize;

// Lower bounds for text size
private float mMinTextSize = MIN_TEXT_SIZE;

// Max bounds for text size
private float mMaxTextSize = MAX_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;

// Add ellipsis to text that overflows at the smallest text size
private int heightLimit;
private int widthLimit;

// Default constructor override
public TextFitTextView(Context context) {
    this(context, null);
}

// Default constructor when inflating from XML file
public TextFitTextView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

// Default constructor override
public TextFitTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    mTextSize = getTextSize();
}

/**
 * When text changes resize the text size.
 */
@Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
    // if we are adding new chars to text
    if (before <= after && after != 1) {
        resizeText(true);
        // now we are deleting chars
    } else {
        resizeText(false);
    }
}

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

/**
 * Get width and height limits
 */
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    if (widthLimit == 0 && heightLimit == 0) {
        widthLimit = (right - left) - getCompoundPaddingLeft() - getCompoundPaddingRight();
        heightLimit = (bottom - top) - getCompoundPaddingBottom() - getCompoundPaddingTop();
    }
    super.onLayout(changed, left, top, right, bottom);
}

/**
 * Resize the text size with specified width and height
 * 
 * @param width
 * @param height
 */
public void resizeText(boolean increase) {
    CharSequence text = getText();
    // Do not resize if the view does not have dimensions or there is no
    // text
    if (text == null || text.length() == 0 || heightLimit <= 0 || widthLimit <= 0 || mTextSize == 0) {
        return;
    }

    // Get the text view's paint object
    TextPaint textPaint = getPaint();

    // Get the required text height
    int textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);


    // If the text length is increased 
    // Until we either fit within our text view or we had reached our min
    // text size, incrementally try smaller sizes
    if (increase) {
        while (textHeight > heightLimit && mTextSize > mMinTextSize) {
            mTextSize = Math.max(mTextSize - 2, mMinTextSize);
            textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
        }
    } 
//      text length has been decreased
    else {
//          if max test size is set then add it to while condition
        if (mMaxTextSize != 0) {
            while (textHeight < heightLimit && mTextSize <= mMaxTextSize) {
                mTextSize = mTextSize + 2;
                textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
            }
        } else {
            while (textHeight < heightLimit) {
                mTextSize = mTextSize + 2;
                textHeight = getTextHeight(text, textPaint, widthLimit, mTextSize);
            }
        }
        mTextSize = textHeight > heightLimit ? mTextSize - 2 : mTextSize;
    }

    // If we had reached our minimum text size and still don't fit, append
    // an ellipsis
    if (mAddEllipsis && mTextSize == mMinTextSize && textHeight > heightLimit) {
        // Draw using a static layout
        TextPaint paint = new TextPaint(textPaint);
        StaticLayout layout = new StaticLayout(text, paint, widthLimit, 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(heightLimit) - 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 = paint.measureText(mEllipsis);

                // Trim characters off until we have enough room to draw the
                // ellipsis
                while (widthLimit < lineWidth + ellipseWidth) {
                    lineWidth = paint.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, mTextSize);
    setLineSpacing(mSpacingAdd, mSpacingMult);

}

// 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) {
    // Update the text paint object
    TextPaint paint = new TextPaint(originalPaint);
    paint.setTextSize(textSize);
    // Measure using a static layout
    StaticLayout layout = new StaticLayout(source, paint, width, Alignment.ALIGN_NORMAL, mSpacingMult, mSpacingAdd,
            true);
    return layout.getHeight();
}

}

这是一个简单的解决方案,使用TextView本身和textchangedlistens添加到它:

expressionView = (TextView) findViewById(R.id.expressionView);
expressionView.addTextChangedListener(textAutoResizeWatcher(expressionView, 25, 55));

private TextWatcher textAutoResizeWatcher(final TextView view, final int MIN_SP, final int MAX_SP) {
    return new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}

        @Override
        public void afterTextChanged(Editable editable) {

            final int widthLimitPixels = view.getWidth() - view.getPaddingRight() - view.getPaddingLeft();
            Paint paint = new Paint();
            float fontSizeSP = pixelsToSp(view.getTextSize());
            paint.setTextSize(spToPixels(fontSizeSP));

            String viewText = view.getText().toString();

            float widthPixels = paint.measureText(viewText);

            // Increase font size if necessary.
            if (widthPixels < widthLimitPixels){
                while (widthPixels < widthLimitPixels && fontSizeSP <= MAX_SP){
                    ++fontSizeSP;
                    paint.setTextSize(spToPixels(fontSizeSP));
                    widthPixels = paint.measureText(viewText);
                }
                --fontSizeSP;
            }
            // Decrease font size if necessary.
            else {
                while (widthPixels > widthLimitPixels || fontSizeSP > MAX_SP) {
                    if (fontSizeSP < MIN_SP) {
                        fontSizeSP = MIN_SP;
                        break;
                    }
                    --fontSizeSP;
                    paint.setTextSize(spToPixels(fontSizeSP));
                    widthPixels = paint.measureText(viewText);
                }
            }

            view.setTextSize(fontSizeSP);
        }
    };
}

private float pixelsToSp(float px) {
    float scaledDensity = getResources().getDisplayMetrics().scaledDensity;
    return px/scaledDensity;
}

private float spToPixels(float sp) {
    float scaledDensity = getResources().getDisplayMetrics().scaledDensity;
    return sp * scaledDensity;
}

这种方法将根据需要增加或减少字体大小以适应文本,尊重作为参数接收的MIN_SP和MAX_SP边界。

我为此写了一篇博客。

我根据Kirill Grouchnikov关于在新的android market应用中使用的自定义组件的博客文章创建了一个名为ResizableButton的组件。我把src代码放在这里。

另一方面,mosabua读了我的文章,并告诉我他将开源他的实现,这比我的更快。我希望他能尽快发布:)