如何避免TextView中的文本字符换行,以实现自动调整文本大小?

5

背景

经过大量搜索,寻找最佳的自适应TextView解决方案(根据内容、大小、最小和最大行数以及字体大小限制),我已经将所有内容合并为一个方案,在这里

注意:我不使用其他解决方案,因为它们效果不好,每个都有自己的问题(某些内容不受支持,文本超出TextView范围,文本被截断等)。

演示如下:

https://raw.githubusercontent.com/AndroidDeveloperLB/AutoFitTextView/master/animationPreview.gif

问题

在某些情况下,一行的最后一个字符会换到下一行,如下所示:

enter image description here

绿色是TextView的边界,红色是超出TextView范围的部分。

代码

基本上,给定TextView的大小、最小和最大字体大小、最小和最大行数以及应该在其中的内容(文本),它会找到(使用二分搜索)适合TextView边界内的字体大小。

代码已经在Github上可用,但为了方便起见,在这里也提供一下:

public class AutoResizeTextView extends AppCompatTextView {
    private static final int NO_LINE_LIMIT = -1;
    private final RectF _availableSpaceRect = new RectF();
    private final SizeTester _sizeTester;
    private float _maxTextSize, _spacingMult = 1.0f, _spacingAdd = 0.0f, _minTextSize;
    private int _widthLimit, _maxLines;
    private boolean _initialized = false;
    private TextPaint _paint;

    private interface SizeTester {
        /**
         * @param suggestedSize  Size of text to be tested
         * @param availableSpace available space in which text must fit
         * @return an integer < 0 if after applying {@code suggestedSize} to
         * text, it takes less space than {@code availableSpace}, > 0
         * otherwise
         */
        int onTestSize(int suggestedSize, RectF availableSpace);
    }

    public AutoResizeTextView(final Context context) {
        this(context, null, android.R.attr.textViewStyle);
    }

    public AutoResizeTextView(final Context context, final AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

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

        // using the minimal recommended font size
        _minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics());
        _maxTextSize = getTextSize();
        _paint = new TextPaint(getPaint());
        if (_maxLines == 0)
            // no value was assigned during construction
            _maxLines = NO_LINE_LIMIT;
        // prepare size tester:
        _sizeTester = new SizeTester() {
            final RectF textRect = new RectF();

            @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
            @Override
            public int onTestSize(final int suggestedSize, final RectF availableSPace) {
                _paint.setTextSize(suggestedSize);
                final TransformationMethod transformationMethod = getTransformationMethod();
                final String text;
                if (transformationMethod != null)
                    text = transformationMethod.getTransformation(getText(), AutoResizeTextView.this).toString();
                else
                    text = getText().toString();

                final boolean singleLine = getMaxLines() == 1;
                if (singleLine) {
                    textRect.bottom = _paint.getFontSpacing();
                    textRect.right = _paint.measureText(text);
                } else {
                    final StaticLayout layout = new StaticLayout(text, _paint, _widthLimit, Alignment.ALIGN_NORMAL, _spacingMult, _spacingAdd, true);
                    // return early if we have more lines
                    if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines())
                        return 1;
                    textRect.bottom = layout.getHeight();
                    int maxWidth = -1;
                    for (int i = 0; i < layout.getLineCount(); i++)
                        if (maxWidth < layout.getLineRight(i) - layout.getLineLeft(i))
                            maxWidth = (int) layout.getLineRight(i) - (int) layout.getLineLeft(i);
                    textRect.right = maxWidth;
                }
                textRect.offsetTo(0, 0);
                if (availableSPace.contains(textRect))
                    // may be too small, don't worry we will find the best match
                    return -1;
                // else, too big
                return 1;
            }
        };
        _initialized = true;
    }

    @Override
    public void setAllCaps(boolean allCaps) {
        super.setAllCaps(allCaps);
        adjustTextSize();
    }

    @Override
    public void setTypeface(final Typeface tf) {
        super.setTypeface(tf);
        adjustTextSize();
    }

    @Override
    public void setTextSize(final float size) {
        _maxTextSize = size;
        adjustTextSize();
    }

    @Override
    public void setMaxLines(final int maxlines) {
        super.setMaxLines(maxlines);
        _maxLines = maxlines;
        adjustTextSize();
    }

    @Override
    public int getMaxLines() {
        return _maxLines;
    }

    @Override
    public void setSingleLine() {
        super.setSingleLine();
        _maxLines = 1;
        adjustTextSize();
    }

    @Override
    public void setSingleLine(final boolean singleLine) {
        super.setSingleLine(singleLine);
        if (singleLine)
            _maxLines = 1;
        else _maxLines = NO_LINE_LIMIT;
        adjustTextSize();
    }

    @Override
    public void setLines(final int lines) {
        super.setLines(lines);
        _maxLines = lines;
        adjustTextSize();
    }

    @Override
    public void setTextSize(final int unit, final float size) {
        final Context c = getContext();
        Resources r;
        if (c == null)
            r = Resources.getSystem();
        else r = c.getResources();
        _maxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics());
        adjustTextSize();
    }

    @Override
    public void setLineSpacing(final float add, final float mult) {
        super.setLineSpacing(add, mult);
        _spacingMult = mult;
        _spacingAdd = add;
    }

    /**
     * Set the lower text size limit and invalidate the view
     *
     * @param minTextSize
     */
    public void setMinTextSize(final float minTextSize) {
        _minTextSize = minTextSize;
        adjustTextSize();
    }

    private void adjustTextSize() {
        // This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14
        // TODO think of a nicer, elegant solution.
//    post(new Runnable()
//    {
//    @Override
//    public void run()
//      {
        if (!_initialized)
            return;
        final int startSize = (int) _minTextSize;
        final int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
        _widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
        if (_widthLimit <= 0)
            return;
        _paint = new TextPaint(getPaint());
        _availableSpaceRect.right = _widthLimit;
        _availableSpaceRect.bottom = heightLimit;
        superSetTextSize(startSize);
//      }
//    });
    }

    private void superSetTextSize(int startSize) {
        int textSize = binarySearch(startSize, (int) _maxTextSize, _sizeTester, _availableSpaceRect);
        super.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
    }

    private int binarySearch(final int start, final int end, final SizeTester sizeTester, final RectF availableSpace) {
        int lastBest = start, lo = start, hi = end - 1, mid;
        while (lo <= hi) {
            mid = lo + hi >>> 1;
            final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
            if (midValCmp < 0) {
                lastBest = lo;
                lo = mid + 1;
            } else if (midValCmp > 0) {
                hi = mid - 1;
                lastBest = hi;
            } else return mid;
        }
        // make sure to return last best
        // this is what should always be returned
        return lastBest;
    }

    @Override
    protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
        super.onTextChanged(text, start, before, after);
        adjustTextSize();
    }

    @Override
    protected void onSizeChanged(final int width, final int height, final int oldwidth, final int oldheight) {
        super.onSizeChanged(width, height, oldwidth, oldheight);
        if (width != oldwidth || height != oldheight)
            adjustTextSize();
    }
}

问题

为什么会出现这种情况?我该怎么解决?


如果您想将文本显示到末尾,则将TextView的宽度设置为match_parent。 - Ameer Faisal
这不是我问的问题。我问的是算法有什么问题,它会根据TextView的大小(以及其他因素)改变字体大小。将TextView的宽度设置为match_parent是没有帮助的,因为容器可能具有问题的大小。你谈论的是谁使用了我制作的库,而不是库本身的错误。 - android developer
2个回答

0

看起来可以使用支持库:

    <TextView
        android:layout_width="250dp" android:layout_height="wrap_content" android:background="#f00"
        android:breakStrategy="balanced" android:hyphenationFrequency="none"
        android:text="This is an example text" android:textSize="30dp" app:autoSizeTextType="uniform"/>

遗憾的是,它有两个缺点:

  1. 不能总是很好地展示单词。在此处报告

  2. 需要 Android API 23 及以上版本 (在此处)。

更多信息在此处


0

我在我的项目中遇到了类似的问题。长时间的谷歌搜索,StackOverflow(其中包括你的问题)。什么都没有找到。

最后,我的“解决方案”是使用BreakIterator来分割单词并测量它们以检查这种情况。

更新(2018-08-10):

static public boolean isTextFitWidth(final @Nullable String source, final @NonNull BreakIterator bi, final @NonNull TextPaint paint, final int width, final float textSize)
{
    if (null == source || source.length() <= 0) {
        return true;
    }

    TextPaint paintCopy = new TextPaint();
    paintCopy.set(paint);
    paintCopy.setTextSize(textSize);

    bi.setText(source);
    int start = bi.first();
    for (int end = bi.next(); BreakIterator.DONE != end; start = end, end = bi.next()) {
        int wordWidth = (int)Math.ceil(paintCopy.measureText(source, start, end));
        if (wordWidth > width) {
            return false;
        }
    }
    return true;
}

static public boolean isTextFitWidth(final @NonNull TextView textView, final @NonNull BreakIterator bi, final int width, final @Nullable Float textSize)
{
    final int textWidth = width - textView.getPaddingLeft() - textView.getPaddingRight();
    return isTextFitWidth(textView.getText().toString(), bi, textView.getPaint(), textWidth,
            null != textSize ? textSize : textView.getTextSize());
}

我认为它需要更多的工作,以便在TextView本身内使用,这样它将考虑所有TextView属性。但是这个想法似乎还不错。你可能会尝试多次测试,直到它适合宽度,对吧?但是你如何使用BreakIterator - android developer
@androiddeveloper,事实上我想向您展示最终组件,但它是在NDA下开发的,我被禁止发布... 在我的实现中,我对TextView进行了子类化,创建了BreakIterator,使用BreakIterator.getWordInstance()作为最终私有属性,并将其传递给上面答案中的方法。是的,为了获得最终大小,我执行了多个类似于Quicksort算法的测试,并在每次测量文本时进行测量。我在每个onDraw tick上执行此操作,但需要检查(当没有文本时,大小或文本/提示应更改)。 - kpower
很遗憾,这段代码还不够。它缺少一个非常重要的部分才能使其正常工作... - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接