【问题标题】:How to avoid character wrap of text in TextView, for auto-resizing text?如何避免 TextView 中文本的字符换行,以自动调整文本大小?
【发布时间】:2016-08-25 13:34:07
【问题描述】:

背景

经过大量搜索自动调整 TextView 大小的最佳解决方案(根据内容、大小、最小和最大行以及字体大小限制),我已经为这一切制定了一个合并的解决方案,here

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

演示它的工作原理:

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

问题

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

绿色是 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
  • 我问的不是这个。我问了算法出了什么问题,它根据 TextView 的大小(以及其他东西)改变了字体大小。将 TextView 的宽度设置为 match_parent 将无济于事,因为容器的大小可能会出现问题。您说的是使用我创建的库的人,而不是库本身的错误。

标签: android textview autoresize


【解决方案1】:

我的项目中有类似的问题。长期谷歌搜索,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
  • @androiddeveloper 实际上我想向您展示最终组件,但它是在 NDA 下开发的,我被禁止发布它...在我的实现中,我将 TextView 子类化,创建 BreakIterator 和 @ 987654325@ 作为最终私有财产,并从上面的答案传递给方法。是的,为了获得最终大小,我进行了多次类似于快速排序算法的测试,并且每次都测量文本。我在每个onDraw 勾选上执行此操作,但需要进行必要性检查(应该更改没有文本时的大小或文本/提示)。
  • 所以遗憾的是,这段代码还不够。它错过了让它工作的一个非常重要的部分......
【解决方案2】:

似乎可以使用支持库:

    <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. 并不总是很好地传播话语。举报here

  2. 需要 Android API 23 及更高版本 (here)。

更多信息here

【讨论】:

    猜你喜欢
    • 2014-10-02
    • 2019-04-05
    • 2011-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-03
    • 1970-01-01
    相关资源
    最近更新 更多